6: Async

现在让投票变得即时。Phlo 的前端运行时拦截标记为 async 的表单和链接,在后台发送它们,并将服务器的 DOM 命令应用到页面上。无需重新加载,无需客户端状态,无需 API 层:同一路由同时响应两个世界。

6.1: Switch on the frontend

Add two resources to data/app.json:

{
    "resources": [
        "DB/DB",
        "DB/model",
        "DB/JSONDB",
        "DB/JSON.result",
        "payload",
        "phlo.async",
        "DOM/form"
    ]
}

phlo.async is the async request engine in www/app.js (which view() already includes on every page), and DOM/form teaches it to submit forms. A reload regenerates the bundle on the request; the CLI shows it explicitly:

php www/app.php build::run

The output lists *app.js: the frontend bundle was regenerated.

6.2: 路由有同步侧和异步侧

每个 route 默认是仅同步的,除非你另有说明:

将投票 route 更改为同时服务两种请求,并根据请求类型进行分支:

route both POST poll vote $id {
	if (!$option = type_poll::record(id: (int)$id)) return false
	type_poll::change('id=?', (int)$id, votes: $option->votes + 1)
	if (%req->async) return apply(
		outer: ['#results' => $this->results],
	)
	location('/poll')
}

当 Phlo 前端发起请求时,%req->async 为 true。Async 请求会得到一个 apply(...) 响应;普通浏览器仍会得到重定向。一个 route,两种传输,零重复。

注意 apply(...) 内部的尾随逗号:在 Phlo 中,每个多行参数列表的行都以逗号结尾,包括最后一行。这个逗号告诉解析器语句仍在继续。

6.3: Mark the form as async

The frontend only intercepts elements with the async class. One character in the choices view:

view choices:
<section.card>
	<foreach type_poll::records() AS $option>
		<form.async method=post action="/poll/vote/$option->id">
			<button>$option->option</button>
		</form>
	</foreach>
</section>

<form.async ...> is the dot shorthand for class="async". The same works for navigation: <a.async href="/poll"> loads a page through the async pipeline with a view transition instead of a full reload.

Reload http://localhost/poll once (to pick up the new markup), then vote. The bar animates to its new width, the counts update, and the page never reloads. The transition: width .4s from chapter 4 is doing the easing.

6.4: apply() 实际上发送了什么

apply() 返回 JSON 命令,前端在 DOM 上执行这些命令。看看它是如何发生的:

curl -s -X POST -H "X-Requested-With: phlo" http://localhost/poll/vote/1
{"outer": {"#results": "<section id=\"results\" class=\"card\">..."}}

outer 替换元素的 outerHTML;服务器渲染了 results view 并将其作为字符串发送。其他命令的工作方式相同:innerappendremoveclassvaluetitlepathscroll 等,都是一个 apply() 调用的命名参数。命令名称没有构建时检查,因此像 innr: 这样的拼写错误会被静默忽略;请随时参考指南中的命令表。

从两个不同的浏览器标签页投票,你会发现最后一个间隙:另一个标签页在你重新加载之前不会移动。记住这个想法,留到第 8 章。首先:更多语言。

我们使用必要的cookie来使该网站正常工作。在您的许可下,我们还使用分析工具来改善网站。