编程 php-via:用 OpenSwoole + Datastar 写不碰 JavaScript 的响应式 PHP 页面

2026-09-29 21:31:24

php-via:用 OpenSwoole + Datastar 写不碰 JavaScript 的响应式 PHP 页面

项目信息

  • 仓库:
  • 文档与在线示例:
  • Packagist:mbolli/php-via
  • License:MIT
  • 客户端实时层:Datastar(SSE + DOM morphing)
  • 异步运行时:OpenSwoole
  • 模板引擎:Twig
  • 设计上受 go-via/via 启发

php-via 把响应式状态留在服务端:Datastar 负责浏览器端响应性、SSE 与 DOM morphing,Twig 负责模板,PHP 侧由 OpenSwoole 提供异步能力。整个流程没有构建步骤——无转译、无打包、无 node_modules。每个页面默认带一条 SSE 长连接,配合 Brotli 压缩开销很低。

状态作用域是它比较特别的地方:TAB、ROUTE、SESSION、GLOBAL 以及自定义作用域决定谁共享状态、谁收到广播。

环境要求

  • PHP 8.4+
  • OpenSwoole PHP 扩展
  • Composer
  • Brotli PHP 扩展(可选,Config::withBrotli() 需要)
composer require mbolli/php-via

Quick Start

withTemplateDir(__DIR__ . '/templates');

$app = new Via($config);

$app->page('/', function (Context $c): void {
$count = $c->signal(0, 'count');
$step  = $c->signal(1, 'step');

$c->action(function () use ($count, $step, $c): void {
$count->setValue($count->int() + $step->int());
$c->syncSignals();
}, 'increment');

$c->view('counter.html.twig');
});

$app->start();

counter.html.twig:


Count: {{ count.int }}

Step:
Increment

跑 php app.php,然后访问 。

核心概念

Signals

服务端与客户端同步的响应式状态。$name = $c->signal('Alice', 'name'); 读取用 $name->string(),写入用 $name->setValue('Bob'),写操作会自动推送到浏览器。

Actions

由客户端事件触发的服务端函数:

$save = $c->action(function () use ($c) {
$c->sync();
}, 'save');

模板里用 Save 触发。

触发 action 必须用 @post()(或 @patch/@put/@delete)。@get() 在 /_action/… 上被禁用,GET 请求返回 405 Method Not Allowed——允许 action 走 GET 等于开放顶层跨站导航 CSRF。

Scopes

Scope共享范围典型用途
Scope::TAB每个标签页隔离(默认)个人表单、设置
Scope::ROUTE同一路由下所有用户共享看板、多人协作
Scope::SESSION同一会话的所有标签页跨标签页状态
Scope::GLOBAL全局所有用户通知、公告
"room:lobby"该自定义作用域内所有上下文聊天室、游戏大厅

Views 与路径参数

$c->view('dashboard.html.twig', ['user' => $user]);,模板可以是文件也可以是内联字符串。

路径参数按名字自动注入:

$app->page('/blog/{year}/{slug}', function (Context $c, string $year, string $slug): void {
// ...
});

组件与生命周期钩子

组件是可复用的子上下文,状态隔离:$a = $c->component($counterWidget, 'a');

钩子示例:

$c->onDisconnect(fn () => /* ... */);
$c->setInterval(fn () => $c->sync(), 2000);
$app->onClientConnect(fn (string $id) => /* ... */);
$app->setInterval(fn () => $app->broadcast(Scope::GLOBAL), 5000);

onDisconnect 有一个宽限期(默认 5 秒)后才触发,用来容忍页面跳转和短暂重连,用 Config::withContextCleanupDelay() 调整。标签页被放到后台足够久导致上下文销毁后,重新返回时上下文会「复活」:服务端重建等价上下文(同 ID),并重新播种客户端仍持有的信号值,而不是硬刷新丢掉本地信号、滚动位置和焦点。该行为默认开启(10 分钟窗口),用 Config::withContextRevivalWindow() 调整或关闭。复活会重跑 page handler,因此服务端 #[Persist] 状态会重置,生命周期钩子会重新触发。

路由分组与中间件

$app->group('/admin', function (Via $app) {
// ...
})->middleware(new AuthMiddleware());

广播

$c->broadcast();                    // 同作用域
$app->broadcast(Scope::GLOBAL);
$app->broadcast('room:lobby');

多节点广播

默认的 InMemoryBroker 对单进程部署是正确的。要把 broadcast() 扇出到多台服务器/容器,换成 RedisBroker 或 NatsBroker:

$config->withBroker(new RedisBroker('127.0.0.1', 6379)); // 需要 ext-redis + SWOOLE_HOOK_ALL

Redis 带认证与 TLS:

new RedisBroker(host: 'redis.internal', password: $_ENV['REDIS_PASSWORD'], tls: true)

NATS 走原生 OpenSwoole socket,不需要额外扩展:

new NatsBroker('127.0.0.1', 4222);

带 token 和 TLS 同理。两个 broker 都以指数退避自动重连,1s 起、30s 封顶。错误观测:

$config->onBrokerError(fn (\Throwable $e) => error_log('Broker: ' . $e->getMessage()));

健康端点

每个 php-via 服务都自带 GET /_health,无需配置:

{"status":"ok","version":"0.12.0","broker":{"driver":"RedisBroker","connected":true},"connections":{"contexts":42,"sse":38}}

broker 处于重连退避窗口时返回 HTTP 503。

工作原理

  1. 浏览器请求页面 → 服务端渲染 HTML,打开 SSE 流
  2. 用户点击按钮 → Datastar POST 信号值 + action ID
  3. 服务端执行 action → 修改信号/状态
  4. 服务端推送补丁 → HTML 片段 + 信号更新,通过 SSE
  5. Datastar morphs DOM → UI 无刷新更新

开发

git clone https://github.com/mbolli/php-via
composer install
composer run dev          # 网站 + PHP 热重载 + CSS watcher,需 entr
composer run test
composer run watch-test
composer phpstan          # PHPStan level 6
composer cs-fix

热 PHP 重载:改动 website/src/ 中的文件,worker 自动重启(约 1s),不断开其他连接;Twig 模板本身就是实时的,无需重启。

部署

建议单一 OpenSwoole 进程加反向代理(如 Caddy)。仓库 deploy/ 目录里有 systemd 服务文件和 Caddy 配置示例。典型链路:浏览器 → Caddy(TLS + Brotli 压缩)→ OpenSwoole :3000。

适合需要实时协作、看板、通知这类交互,但不希望引入前端构建链和 JS 状态管理的团队。要注意 Scope::GLOBAL 与广播的使用范围意味着状态都压在服务端进程里,多节点部署必须换 Redis/NATS broker;另外生命周期钩子和上下文复活的重跑语义,决定了哪些状态该放在服务端持久化里。

复制全文 生成海报 PHP OpenSwoole Datastar SSE 响应式 Twig php-via

推荐文章

程序员茄子在线接单