8: 实时

还有一个空白:当其他人投票时,你的标签不会移动。在最后一章中,服务器通过 phloWS 将结果推送到每个打开的浏览器,然后你将整个内容发布出去。实时部分是可选的:它需要单独的 phlo-websocket Node 服务,没有它投票也可以完成。发布部分则是必不可少的。

8.1: phloWS,可选的 Node 服务

WebSockets 在 Phlo 中通过 phloWS 运行,这是一个小型的 Node.js 代理,位于框架之外。一个 phloWS 进程为您所有的应用程序提供服务,使用一个端口并通过 Host 头路由连接。对于每个传入的消息,它会对您的应用程序进行一次性 PHP 调用,因此您的处理程序可以获得完整的请求生命周期:数据库、会话、所有内容。

git clone https://github.com/q-ainl/phlo-websocket.git <ws>
cd <ws>
npm install

<ws>/websocket.js 中,将您的虚拟主机映射到应用程序入口点,然后运行它:

require('./phloWS.js')(3001, 'php', {
    'localhost': '<app>/www/app.php',
})
node <ws>/websocket.js

在应用程序一侧,在 www/app.php 中为运行时提供其 WebSocket 端口,并将实时资源添加到 data/app.json

phlo_app(
    id:    'Poll',
    host:  'localhost',
    build: true,
    debug: true,
    app:   dirname(__DIR__).'/',
    langs: dirname(__DIR__).'/langs/',
    websocket: 3001,
);
{
    "resources": ["...", "websocket", "DOM/websocket", "wsCast", "HTTP"]
}

(... 代表您列表中已经存在的所有内容。) websocket 将传入事件分发到您的钩子,DOM/websocket 是自动连接的浏览器客户端,并通过您在第 6 章中使用的相同 apply() 机制传递传入消息,而 wsCastHTTP 允许 PHP 广播。在生产环境中,您通过反向代理将 wss://.../websocket 传递到 3001 端口。重新构建;lint 保持为 []

8.2: 钩子

您的应用通过普通函数对套接字事件做出反应。创建 poll.ws.phlo

function wsConnect($host, $token, $socket){
    return true
}

function wsReceive($host, $token, $socket, ...$data){
    if (($data['type'] ?? null) === 'ping') return wsCast(wsTarget: $socket, pong: time())
}

wsConnect 在握手后运行;返回 true 以接受。wsReceive 对每个客户端消息运行,消息被 JSON 解码并展开到 $data 中。还有两个钩子存在(wsAuth 用于令牌认证,wsClose 用于清理);轮询不需要这两个,因为结果是公开的。$socket 是一个不透明的 ID,您可以直接针对它,正如 ping 回复所示。

一个命名陷阱:不要将此文件命名为 websocket.phlo。该类名已被引擎的 websocket 资源占用,构建会因重复类错误而失败。poll.ws.phlo 可以很好地将函数编译到应用中。

8.3: 在投票时广播

wsCast() 发布到 phloWS,后者将其推送到连接的浏览器。有效负载与 apply() 使用相同的命令语言。在投票路由中的一行:

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)
    wsCast(wsTarget: 'all', outer: ['#results' => $this->results])
    if (%req->async) return apply(
        outer: ['#results' => $this->results],
    )
    location('/poll')
}

wsTarget: 'all' 到达每个连接的客户端;一个 socket id 或一个 id 数组可以缩小范围。浏览器接收 outer: {'#results': ...} 并更新条形图,就像他们自己投票一样。

在两个窗口中打开 http://localhost/poll 并在一个窗口中投票:两个窗口都会移动。如果 phloWS 关闭了呢?wsCast() 会静默失败,投票者仍然会收到他们的 apply() 响应,投票仍然可以继续。实时性在这里是一个层,而不是一个依赖。

8.4: 发货

是时候生成可部署的构建了。向 data/app.json 添加一个发布目标:

{
    "resources": ["..."],
    "release": "%app/release/"
}

然后构建它:

php www/app.php build::run
php www/app.php build::lint
php www/app.php build::release

build::release 编译一个压缩的生产构建:PHP 到 release/,网页资产到 release/www/。为发布提供一个独立的入口点,release/www/app.php,构建模式关闭,并将路径指向发布输出:

<?php
require '/phlo/phlo.php';

phlo_app(
    id:    'Poll',
    host:  'poll.example.com',
    build: false,
    app:   dirname(__DIR__, 2).'/',
    php:   dirname(__DIR__).'/',
    www:   __DIR__.'/',
    data:  dirname(__DIR__, 2).'/data/',
    langs: dirname(__DIR__, 2).'/langs/',
    websocket: 3001,
);

datalangs 指向共享的应用文件夹:投票和翻译是状态,而不是构建输出。使用您开始时的相同 Docker 镜像进行部署,现在将 release/www 作为 webroot,并启用自动 HTTPS:

docker run -v $(pwd)/app:/app -p 80:80 -p 443:443 -p 443:443/udp \
    -e SERVER_NAME=poll.example.com ghcr.io/q-ainl/phlo

8.5: 下一步是什么

您在八个简短的文件中构建了一个全栈应用程序:一个模型,一个控制器,一个样式表,一个钩子文件,以及四个配置文件。路由、视图、CSS、ORM、异步 DOM 更新、翻译和实时功能,全部使用一种语言。

从这里开始:

发布一些东西。

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