关于 turbo
面向使用 Hotwire Turbo 的 Symfony 开发者,介绍如何为服务端渲染页面添加类似 SPA 的导航和更新。内容涵盖 Drive、Frames 和 Streams,分别用于导航、局部更新及多元素或实时变更。
主要功能
- 01使用 Turbo Drive 进行 AJAX 导航
- 02使用 Frames 局部更新页面
- 03使用 Streams 更新多个 DOM 元素
- 04延迟加载 Frames
- 05通过 SSE 发送 Mercure 广播
适用场景
- 01提交链接和表单时避免整页加载
- 02通过表单更新页面的单个区域
- 03操作后更新多个页面元素
- 04向已连接的浏览器广播实时变更
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ smnandre/ symfony-ux-skills --skill turbo安装后在 Claude Code 中输入/turbo确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/api.mdgotchas.mdpatterns.md详细介绍
Turbo
Hotwire Turbo 通过服务器渲染的 HTML 提供类似 SPA 的速度。无需编写 JavaScript。三个组件协同工作:
- Drive -- 为所有链接和表单自动执行 AJAX 导航(无需配置)
- Frames -- 限定范围的导航,只更新页面的一个区域
- Streams -- 由服务器推送 DOM 变更(追加、替换、移除等)
决策树
Need to update the page?
+-- Full page navigation -> Turbo Drive (automatic, already active)
+-- Single section from user click -> Turbo Frame
+-- Multiple sections from action -> Turbo Stream (HTTP response)
+-- Real-time from server/others -> Turbo Stream (Mercure / SSE)
安装
composer require symfony/ux-turbo
就这样。Turbo Drive 会立即启用——所有链接和表单都会变为 AJAX 请求。
Turbo Drive
自动实现类似 SPA 的导航。每次点击 <a> 和提交 <form> 都会被拦截,通过 AJAX 获取内容,并替换 <body>。浏览器 URL 和历史记录会正常更新。
针对特定元素禁用
<a href="/external" data-turbo="false">External Link</a>
<div data-turbo="false">
<a href="/normal">Normal link (no Turbo)</a>
</div>
历史记录和缓存
<a href="/page" data-turbo-action="replace">Replace History</a>
<link rel="stylesheet" href="/app.css" data-turbo-track="reload">
<script src="/app.js" data-turbo-track="reload"></script>
Turbo Frames
将导航范围限定在页面的某个区域内。Frame 中的链接和表单只会更新该 Frame 的内容。页面其余部分保持不变。
基本 Frame
<turbo-frame id="messages">
<h2>Messages</h2>
<a href="/messages/1">View Message 1</a>
</turbo-frame>
<turbo-frame id="messages">
<h2>Message 1</h2>
<p>Content here...</p>
<a href="/messages">Back to list</a>
</turbo-frame>
服务器响应是一个完整的 HTML 页面,但 Turbo 只会提取匹配的 <turbo-frame> 并将其替换到页面中。
延迟加载
在页面渲染后异步加载 Frame 内容:
<turbo-frame id="notifications" src="/notifications" loading="lazy">
<p>Loading...</p>
</turbo-frame>
定向更新其他 Frame
一个 Frame 中的链接可以更新另一个 Frame:
<turbo-frame id="sidebar">
<a href="/item/1" data-turbo-frame="main-content">View Item</a>
</turbo-frame>
<turbo-frame id="main-content">
</turbo-frame>
跳出 Frame
在 Frame 中导航整个页面:
<turbo-frame id="modal">
<a href="/dashboard" data-turbo-frame="_top">Go to Dashboard</a>
</turbo-frame>
带表单的 Frame
Frame 中的表单会在该 Frame 内提交并更新内容:
<turbo-frame id="search-results">
<form action="/search" method="get">
<input type="search" name="q">
<button>Search</button>
</form>
<ul>
{% for item in results %}
<li>{{ item.name }}</li>
{% endfor %}
</ul>
</turbo-frame>
同步 URL
Frame 导航时更新浏览器 URL(适用于可加入书签的状态):
<turbo-frame id="products" data-turbo-action="advance">
</turbo-frame>
Turbo Streams
通过单个服务器响应更新多个 DOM 元素。提供八种操作(append、prepend、replace、update、remove、before、after、refresh),每种操作都可通过 ID 或 CSS 选择器定位元素。
Stream 操作
<turbo-stream action="append" target="messages">
<template><div id="msg_1">New message</div></template>
</turbo-stream>
<turbo-stream action="prepend" target="messages">
<template><div id="msg_0">First!</div></template>
</turbo-stream>
<turbo-stream action="replace" target="notification">
<template><div id="notification">Updated!</div></template>
</turbo-stream>
<turbo-stream action="update" target="counter">
<template>42</template>
</turbo-stream>
<turbo-stream action="remove" target="msg_5"></turbo-stream>
<turbo-stream action="before" target="msg_3">
<template><div id="msg_2">Inserted before</div></template>
</turbo-stream>
<turbo-stream action="after" target="msg_3">
<template><div id="msg_4">Inserted after</div></template>
</turbo-stream>
<turbo-stream action="refresh"></turbo-stream>
replace 和 update 支持可选的 method="morph" 属性,可平滑变更 DOM,而非完全替换:
<turbo-stream action="replace" method="morph" target="user-card">
<template><div id="user-card">Updated content</div></template>
</turbo-stream>
定位多个元素(CSS 选择器)
使用 targets(复数形式)和 CSS 选择器来影响多个元素:
<turbo-stream action="remove" targets=".notification.read"></turbo-stream>
<turbo-stream action="update" targets=".price">
<template>99.00 EUR</template>
</turbo-stream>
Stream 的 Twig 组件语法
从 Symfony UX 2.22+ 开始,可以使用 <twig:Turbo:Stream:*> 组件,而不是原始 HTML:
<twig:Turbo:Stream:Append target="comments">
{{ include('comment/_comment.html.twig') }}
</twig:Turbo:Stream:Append>
<twig:Turbo:Stream:Update target="comment-count">
{{ count }}
</twig:Turbo:Stream:Update>
<twig:Turbo:Stream:Remove target="msg_5" />
Symfony 集成
从控制器返回 Stream 响应
use Symfony\UX\Turbo\TurboBundle;
#[Route('/messages', name: 'message_create', methods: ['POST'])]
public function create(Request $request): Response
{
$message = new Message();
// ... handle form
$this->em->persist($message);
$this->em->flush();
// Return stream response for Turbo requests
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
return $this->render('message/create.stream.html.twig', [
'message' => $message,
'count' => $count,
]);
}
你也可以使用 TurboStreamResponse 助手或 TurboStream 助手方法,以编程方式构建 Stream。
Stream 模板
{# templates/message/create.stream.html.twig #}
<turbo-stream action="append" target="messages">
<template>
{{ include('message/_message.html.twig', {message: message}) }}
</template>
</turbo-stream>
<turbo-stream action="update" target="message-count">
<template>{{ count }}</template>
</turbo-stream>
<turbo-stream action="replace" target="new-message-form">
<template>
{{ include('message/_form.html.twig', {message: null}) }}
</template>
</turbo-stream>
检测 Frame 请求
public function show(Request $request, int $id): Response
{
if ($request->headers->has('Turbo-Frame')) {
$frameId = $request->headers->get('Turbo-Frame');
// Return only the frame content (or a full page -- Turbo extracts the frame)
}
return $this->render('page/show.html.twig');
}
Mercure 广播(实时)
通过 SSE 向所有已连接的浏览器推送变更:
use Symfony\UX\Turbo\Attribute\Broadcast;
#[Broadcast]
class Message
{
// Entity changes broadcast automatically to subscribed clients
}
{# Subscribe to Mercure topic #}
<turbo-stream-source src="{{ mercure('chat-room-1')|escape('html_attr') }}">
</turbo-stream-source>
<div id="messages">
{# Messages appear here in real-time #}
</div>
常见模式
行内编辑
<turbo-frame id="task_{{ task.id }}">
<span>{{ task.title }}</span>
<a href="/tasks/{{ task.id }}/edit">Edit</a>
</turbo-frame>
<turbo-frame id="task_1">
<form action="/tasks/1" method="post">
<input name="title" value="Task title">
<button>Save</button>
<a href="/tasks/1">Cancel</a>
</form>
</turbo-frame>
Frame 中的模态框
<turbo-frame id="modal"></turbo-frame>
<a href="/items/1/delete" data-turbo-frame="modal">Delete</a>
<turbo-frame id="modal">
<dialog open>
<p>Confirm delete?</p>
<form method="post">
<button>Delete</button>
</form>
<a href="/items" data-turbo-frame="modal">Cancel</a>
</dialog>
</turbo-frame>
使用 Stream 显示 Flash 消息
<turbo-stream action="prepend" target="flash-messages">
<template>
<div class="alert alert-success" role="alert">
Item saved successfully!
</div>
</template>
</turbo-stream>
核心原则
服务器返回完整的 HTML 页面。 Turbo 最适合服务器始终返回完整且有效的 HTML 页面。Turbo Drive 会替换 body,Turbo Frames 会提取匹配的 Frame。不要尝试返回部分 HTML 片段(Stream 模板除外)。
Frame ID 必须匹配。 响应中的 Frame 必须与页面上的 Frame 具有相同的 id。如果不匹配,Turbo 会显示错误。
Streams 用于处理副作用。 当单个操作需要更新页面中多个互不相关的部分时,请使用 Streams。如果只更新一个区域,Frame 更简单。
Stimulus 与 Turbo 相辅相成。 Turbo 负责导航和服务器通信。Stimulus 负责客户端行为(动画、切换、剪贴板)。二者可以协同工作——在其作用范围内,Stimulus 控制器在 Turbo Frame 替换内容时会继续运行,并会在 Drive 导航时正确重新连接。
参考资料
- 完整 API(Drive 事件、Frame 属性、Stream 操作、Mercure):references/api.md
- 模式(表单、模态框、搜索、分页、无限滚动):references/patterns.md
- 注意事项(缓存问题、表单处理、Stimulus 集成):references/gotchas.md
另请参阅
- UX Map 可在 Turbo Frames 内运行。地图的 Stimulus 控制器会在 Frame 交换时正确重新连接。
- UX Icons 是内联 SVG,无需特殊处理即可在 Turbo Drive 导航和 Frame 交换后继续使用。