跳至内容

使用 Turbo Streams 焕发生机

Turbo Streams 以包装在 <turbo-stream> 元素中的 HTML 片段形式提供页面更改。每个流元素都指定一个操作以及一个目标 ID,以声明其内部 HTML 应如何处理。这些元素可以作为经典 HTTP 响应同步传递到浏览器,也可以通过 WebSocket、SSE 等传输方式异步传递,以使用其他用户或进程所做的更新使应用程序焕发生机。

它们可用于在用户操作后对 DOM 进行外科手术更新,例如从列表中删除元素而不重新加载整个页面,或实现实时功能,例如在远程用户发送时将新消息追加到实时对话中。

流消息和操作

Turbo Streams 消息是由 <turbo-stream> 元素组成的 HTML 片段。下面的流消息演示了八种可能的流操作

<turbo-stream action="append" target="messages">
<template>
<div id="message_1">
This div will be appended to the element with the DOM ID "messages".
</div>
</template>
</turbo-stream>

<turbo-stream action="prepend" target="messages">
<template>
<div id="message_1">
This div will be prepended to the element with the DOM ID "messages".
</div>
</template>
</turbo-stream>

<turbo-stream action="replace" target="message_1">
<template>
<div id="message_1">
This div will replace the existing element with the DOM ID "message_1".
</div>
</template>
</turbo-stream>

<turbo-stream action="update" target="unread_count">
<template>
<!-- The contents of this template will replace the
contents of the element with ID "unread_count" by
setting innerHtml to "" and then switching in the
template contents. Any handlers bound to the element
"unread_count" would be retained. This is to be
contrasted with the "replace" action above, where
that action would necessitate the rebuilding of
handlers. -->

1
</template>
</turbo-stream>

<turbo-stream action="remove" target="message_1">
<!-- The element with DOM ID "message_1" will be removed.
The contents of this stream element are ignored. -->

</turbo-stream>

<turbo-stream action="before" target="current_step">
<template>
<!-- The contents of this template will be added before the
the element with ID "current_step". -->

<li>New item</li>
</template>
</turbo-stream>

<turbo-stream action="after" target="current_step">
<template>
<!-- The contents of this template will be added after the
the element with ID "current_step". -->

<li>New item</li>
</template>
</turbo-stream>

<turbo-stream action="morph" target="current_step">
<template>
<!-- The contents of this template will replace the
element with ID "current_step" via morph. -->

<li>New item</li>
</template>
</turbo-stream>

<turbo-stream action="morph" target="current_step" children-only>
<template>
<!-- The contents of this template will replace the
children of the element with ID "current_step" via morph. -->

<li>New item</li>
</template>
</turbo-stream>

<turbo-stream action="refresh" request-id="abcd-1234"></turbo-stream>

请注意,每个 <turbo-stream> 元素都必须将其包含的 HTML 包装在 <template> 元素中。

Turbo Stream 可以与文档中可以通过 id 属性或 CSS 选择器<template> 元素或 <iframe> 元素内容除外)解析的任何元素集成。无需将目标元素更改为 <turbo-frame> 元素。如果应用程序为了 <turbo-stream> 元素而利用 <turbo-frame> 元素,请将 <turbo-frame> 更改为另一个 内置元素

您可以在 WebSocket、SSE 或响应表单提交时在单个流消息中呈现任意数量的流元素。

具有多个目标的操作

可以使用 targets 属性和 CSS 查询选择器针对多个目标应用操作,而不是使用常规 target 属性(该属性使用 dom ID 引用)。示例

<turbo-stream action="remove" targets=".old_records">
<!-- The element with the class "old_records" will be removed.
The contents of this stream element are ignored. -->

</turbo-stream>

<turbo-stream action="after" targets="input.invalid_field">
<template>
<!-- The contents of this template will be added after the
all elements that match "inputs.invalid_field". -->

<span>Incorrect</span>
</template>
</turbo-stream>

从 HTTP 响应中进行流式传输

Turbo 知道在响应声明 MIME 类型text/vnd.turbo-stream.html<form> 提交时自动附加 <turbo-stream> 元素。在提交 <form> 元素时,其 method 属性设置为 POSTPUTPATCHDELETE,Turbo 会将 text/vnd.turbo-stream.html 注入到请求的 Accept 标头中的一组响应格式中。在响应包含其 Accept 标头中该值的请求时,服务器可以定制其响应来处理 Turbo 流、HTTP 重定向或其他不支持流的类型的客户端(例如原生应用程序)。

在 Rails 控制器中,这将如下所示

def destroy
@message = Message.find(params[:id])
@message.destroy

respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.remove(@message) }
format.html { redirect_to messages_url }
end
end

默认情况下,Turbo 在提交链接或方法类型为 GET 的表单时不会添加 text/vnd.turbo-stream.html MIME 类型。要在应用程序中使用带有 GET 请求的 Turbo 流响应,你可以通过向链接或表单添加 data-turbo-stream 属性来指示 Turbo 包含 MIME 类型。

重用服务器端模板

Turbo 流的关键在于能够重用现有的服务器端模板来执行实时部分页面更改。在首次加载页面时用于呈现此类列表中每条消息的 HTML 模板与稍后用于将一条新消息动态添加到列表的模板相同。这是 HTML-over-the-wire 方法的精髓:你不需要将新消息序列化为 JSON,在 JavaScript 中接收它,呈现客户端模板。它只是重用的标准服务器端模板。

另一个在 Rails 中的示例

<!-- app/views/messages/_message.html.erb -->
<div id="<%= dom_id message %>">
<%= message.content %>
</div>

<!-- app/views/messages/index.html.erb -->
<h1>All the messages</h1>
<%= render partial: "messages/message", collection: @messages %>
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
def index
@messages = Message.all
end

def create
message = Message.create!(params.require(:message).permit(:content))

respond_to do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.append(:messages, partial: "messages/message",
locals: { message: message })
end

format.html { redirect_to messages_url }
end
end
end

当创建新消息的表单提交到 MessagesController#create 操作时,用于在 MessagesController#index 中呈现消息列表的相同部分模板用于呈现 turbo-stream 操作。这将作为如下所示的响应出现

Content-Type: text/vnd.turbo-stream.html; charset=utf-8

<turbo-stream action="append" target="messages">
<template>
<div id="message_1">
The content of the message.
</div>
</template>
</turbo-stream>

然后,此 messages/message 模板部分还可以用于在编辑/更新操作后重新呈现消息。或提供由其他用户通过 WebSocket 或 SSE 连接创建的新消息。能够在整个使用范围内重用相同的模板非常强大,并且是减少创建这些现代、快速应用程序所需工作量的关键。

必要时渐进增强

在不使用 Turbo Streams 的情况下开始交互设计是一个好习惯。让整个应用程序正常工作,就像在没有 Turbo Streams 的情况下一样,然后将它们作为升级版进行分层。这意味着您不会依赖于在没有 Turbo Streams 的情况下,在原生应用程序或其他地方需要工作的流程的更新。

对于 WebSocket 更新尤其如此。在连接不良或服务器出现问题的情况下,您的 WebSocket 很可能会断开连接。如果应用程序设计为可以在没有 WebSocket 的情况下工作,它将更具弹性。

但是运行 JavaScript 怎么办?

Turbo Streams 故意将您限制为八个操作:追加、前置、(插入)之前、(插入)之后、替换、更新、删除和刷新。如果您希望在执行这些操作时触发其他行为,您应该使用 Stimulus 控制器附加行为。此限制允许 Turbo Streams 专注于通过网络传递 HTML 的基本任务,将其他逻辑保留在专用的 JavaScript 文件中。

接受这些限制将使您避免将各个响应变成无法重用且使应用程序难以遵循的行为混乱。Turbo Streams 的主要好处是能够在所有后续更新中重用模板以进行页面的初始渲染。

自定义操作

默认情况下,Turbo Streams 支持 action 属性的八个值。如果您的应用程序需要支持其他行为,您可以覆盖 event.detail.render 函数。

例如,如果您希望扩展这八个操作以支持带有 [action="alert"][action="log"]<turbo-stream> 元素,您可以声明一个 turbo:before-stream-render 侦听器来提供自定义行为

addEventListener("turbo:before-stream-render", ((event) => {
const fallbackToDefaultActions = event.detail.render

event.detail.render = function (streamElement) {
if (streamElement.action == "alert") {
// ...
} else if (streamElement.action == "log") {
// ...
} else {
fallbackToDefaultActions(streamElement)
}
}
}))

除了侦听 turbo:before-stream-render 事件之外,应用程序还可以直接在 StreamActions 上将操作声明为属性

import { StreamActions } from "@hotwired/turbo"

// <turbo-stream action="log" message="Hello, world"></turbo-stream>
//
StreamActions.log = function () {
console.log(this.getAttribute("message"))
}

与服务器端框架集成

在 Turbo 中包含的所有技术中,Turbo Streams 可让你从与后端框架的紧密集成中看到最大的优势。作为官方 Hotwire 套件的一部分,我们为这种集成在 turbo-rails gem 中可能是什么样子创建了一个参考实现。此 gem 依赖于 Rails 中通过 Action Cable 和 Active Job 框架分别提供的对 WebSocket 和异步渲染的内置支持。

使用混合到 Active Record 中的 Broadcastable 关注点,你可以直接从你的域模型触发 WebSocket 更新。并且使用 Turbo::Streams::TagBuilder,你可以在内联控制器响应或专用模板中渲染 <turbo-stream> 元素,通过简单的 DSL 调用具有关联渲染的五个操作。

不过,Turbo 本身是完全与后端无关的。因此,我们鼓励其他生态系统中的其他框架查看为 Rails 提供的参考实现,以创建自己的紧密集成。

Turbo 的 <turbo-stream-source> 自定义元素通过其 [src] 属性连接到流源。当使用 ws://wss:// URL 声明时,底层流源将是 WebSocket 实例。否则,连接是通过 EventSource

当元素连接到文档时,流源将连接。当元素断开连接时,流将断开连接。

由于文档的 <head> 在 Turbo 导航中是持久的,因此将 <turbo-stream-source> 作为文档 <body> 元素的后代非常重要。

由 Turbo 驱动的典型全页导航将导致 <body> 内容被丢弃并替换为结果文档。服务器负责确保该元素存在于需要流的任何页面上。

或者,将任何后端应用程序与 Turbo Streams 集成的简单方法是依赖 Mercure 协议。Mercure 为服务器应用程序定义了一种便利的方式,通过 服务器发送事件 (SSE) 向每个连接的客户端广播页面更改。了解如何将 Mercure 与 Turbo Streams 一起使用

下一步:在 iOS 和 Android 上使用原生应用程序