跳至内容

使用 Turbo Frames 分解

Turbo Frames 允许根据请求更新页面的预定义部分。帧内的任何链接和表单都会被捕获,并且在收到响应后自动更新帧内容。无论服务器提供的是完整文档,还是仅包含请求帧的更新版本的一个片段,只有该特定帧将从响应中提取出来以替换现有内容。

通过将页面的一部分包装在 <turbo-frame> 元素中来创建帧。每个元素都必须具有一个唯一的 ID,该 ID 用于在从服务器请求新页面时匹配要替换的内容。一个页面可以有多个帧,每个帧都建立自己的上下文

<body>
<div id="navigation">Links targeting the entire page</div>

<turbo-frame id="message_1">
<h1>My message title</h1>
<p>My message content</p>
<a href="/messages/1/edit">Edit this message</a>
</turbo-frame>

<turbo-frame id="comments">
<div id="comment_1">One comment</div>
<div id="comment_2">Two comments</div>

<form action="/messages/comments">...</form>
</turbo-frame>
</body>

此页面有两个帧:一个用于显示消息本身,并带有编辑它的链接。一个用于列出所有评论,并带有添加另一个评论的表单。每个帧都创建自己的导航上下文,捕获链接和提交表单。

当单击编辑消息的链接时,/messages/1/edit 提供的响应会提取其 <turbo-frame id="message_1"> 片段,并且该内容会替换单击源自的帧。编辑响应可能如下所示

<body>
<h1>Editing message</h1>

<turbo-frame id="message_1">
<form action="/messages/1">
<input name="message[name]" type="text" value="My message title">
<textarea name="message[content]">My message content</textarea>
<input type="submit">
</form>
</turbo-frame>
</body>

请注意,<h1> 不在 <turbo-frame> 内。这意味着在编辑时表单替换消息的显示时,它将保持不变。仅在更新帧时使用匹配的 <turbo-frame> 中的内容。

因此,你的页面可以轻松地发挥双重作用:在帧内进行编辑,或在帧外进行编辑,其中整个页面都专门用于该操作。

帧服务于一个特定目的:对文档片段的内容和导航进行划分。它们的存在对包含在其子内容中的任何 <a> 元素或 <form> 元素产生影响,并且不应不必要地引入它们。Turbo Frames 不支持使用 Turbo Stream。如果你的应用程序为了 <turbo-stream> 元素而使用 <turbo-frame> 元素,请将 <turbo-frame> 更改为另一个 内置元素

急切加载框架

页面加载时不必填充框架。如果 turbo-frame 标记上存在 src 属性,则标记出现在页面上时,会自动加载引用的 URL

<body>
<h1>Imbox</h1>

<div id="emails">
...
</div>

<turbo-frame id="set_aside_tray" src="/emails/set_aside">
</turbo-frame>

<turbo-frame id="reply_later_tray" src="/emails/reply_later">
</turbo-frame>
</body>

此页面在加载页面后会立即列出您的 收件箱 中的所有电子邮件,但随后会发出两个后续请求,以便在页面底部显示已搁置或等待稍后回复的电子邮件的小托盘。这些托盘是通过对 src 中引用的 URL 发出的单独 HTTP 请求创建的。

在上面的示例中,托盘一开始是空的,但也可以用初始内容填充急切加载框架,然后在从 src 获取内容时覆盖该内容

<turbo-frame id="set_aside_tray" src="/emails/set_aside">
<img src="/icons/spinner.gif">
</turbo-frame>

在加载收件箱页面时,搁置托盘从 /emails/set_aside 加载,并且响应必须包含一个对应的 <turbo-frame id="set_aside_tray"> 元素,如原始示例中所示

<body>
<h1>Set Aside Emails</h1>

<p>These are emails you've set aside</p>

<turbo-frame id="set_aside_tray">
<div id="emails">
<div id="email_1">
<a href="/emails/1">My important email</a>
</div>
</div>
</turbo-frame>
</body>

此页面现在以其最小化形式运行,其中仅将包含各个电子邮件的 div 加载到收件箱页面上的托盘框架中,还可以作为提供标题和描述的直接目标。就像编辑消息表单中的示例一样。

请注意,/emails/set_aside 上的 <turbo-frame> 不包含 src 属性。该属性仅添加到需要延迟加载内容的框架,而不是提供内容的已呈现框架。

在导航期间,框架会在获取新内容时在 <turbo-frame> 元素上设置 [aria-busy="true"]。当导航完成时,框架将移除 [aria-busy] 属性。当通过 <form> 提交导航 <turbo-frame> 时,Turbo 将与框架一起切换表单的 [aria-busy="true"] 属性。

导航完成后,框架将在 <turbo-frame> 元素上设置 [complete] 属性。

延迟加载框架

页面首次加载时不可见的框架可以用 loading="lazy" 标记,这样它们在变得可见之前不会开始加载。这与 img 上的 lazy=true 属性完全相同。这是一种延迟加载位于 summary/detail 对或模态框或任何其他一开始隐藏然后显示的内容的框架的好方法。

缓存加载框架的好处

将页面段转换为框架有助于简化页面的实现,但这样做同样重要的原因是改善缓存动态。具有许多段的复杂页面难以有效缓存,特别是如果它们将许多人共享的内容与针对个别用户专门化内容混合在一起时。段越多,缓存查找所需的依赖键越多,缓存翻转的频率就越高。

框架非常适合分隔在不同时间范围内和针对不同受众而更改的段。有时,如果页面的其余部分很容易在所有用户之间共享,则将页面的每个用户元素转换为框架是有意义的。其他时候,执行相反的操作是有意义的,其中高度个性化的页面将一个共享段转换为框架,以便从共享缓存中提供它。

虽然获取加载框架的开销通常非常低,但你仍然应该谨慎地加载多少框架,特别是如果这些框架会在页面上创建加载抖动时。但是,如果内容在加载页面时不可见,则框架基本上是免费的。要么是因为它们隐藏在模态框后面,要么是因为它们在折叠下面。

针对进入或退出框架的导航

默认情况下,框架内的导航将仅针对该框架。对于跟随链接和提交表单,都是如此。但是,通过将目标设置为 _top,导航可以驱动整个页面而不是封闭框架。或者,它可以通过将目标设置为该框架的 ID 来驱动另一个已命名框架。

在带有备用托盘的示例中,托盘中的链接指向各个电子邮件。你不希望这些链接查找与 set_aside_tray ID 匹配的框架标记。你希望直接导航到该电子邮件。这是通过使用 target 属性标记托盘框架来完成的

<body>
<h1>Imbox</h1>
...
<turbo-frame id="set_aside_tray" src="/emails/set_aside" target="_top">
</turbo-frame>
</body>

<body>
<h1>Set Aside Emails</h1>
...
<turbo-frame id="set_aside_tray" target="_top">
...
</turbo-frame>
</body>

有时你希望大多数链接在框架上下文中操作,但其他链接则不然。表单也是如此。你可以在非框架元素上添加 data-turbo-frame 属性来控制此操作

<body>
<turbo-frame id="message_1">
...
<a href="/messages/1/edit">
Edit this message (within the current frame)
</a>

<a href="/messages/1/permission" data-turbo-frame="_top">
Change permissions (replace the whole page)
</a>
</turbo-frame>

<form action="/messages/1/delete" data-turbo-frame="message_1">
<a href="/messages/1/warning" data-turbo-frame="_self">
Load warning within current frame
</a>

<input type="submit" value="Delete this message">
(with a confirmation shown in a specific frame)
</form>
</body>

将框架导航提升为页面访问

导航框架为应用程序提供了一个机会,可以在保留文档其余状态(例如,其当前滚动位置或焦点元素)的同时更改页面内容的一部分。有时,我们希望对框架的更改也影响浏览器的 历史记录

要将框架导航提升为访问,请使用 [data-turbo-action] 属性呈现元素。该属性支持所有 访问 值,并且可以在

例如,考虑一个呈现分页文章列表并转换导航为 “前进”操作 的框架

<turbo-frame id="articles" data-turbo-action="advance">
<a href="/articles?page=2" rel="next">Next page</a>
</turbo-frame>

点击 <a rel="next"> 元素将同时设置 <turbo-frame> 元素的 [src] 属性浏览器路径为 /articles?page=2

注意:在刷新浏览器后呈现页面时,由应用程序负责呈现第二页文章以及从 URL 路径和搜索参数派生的任何其他状态。

从框架中“跳出”

在大多数情况下,来自 <turbo-frame> 的请求会获取该框架的内容(或页面的其他部分,具体取决于 targetdata-turbo-frame 属性的使用)。这意味着响应应始终包含预期的 <turbo-frame> 元素。如果响应缺少 Turbo 预期的 <turbo-frame> 元素,则认为是错误;当这种情况发生时,Turbo 会向框架中写入一条信息消息并抛出异常。

在某些特定情况下,您可能希望将对 <turbo-frame> 请求的响应视为新的全页导航,从而有效地“跳出”框架。经典示例是当丢失或过期的会话导致应用程序重定向到登录页面时。在这种情况下,Turbo 最好显示该登录页面,而不是将其视为错误。

实现此目的最简单的方法是指定登录页面需要完全重新加载,方法是包含 turbo-visit-control 元标记

<head>
<meta name="turbo-visit-control" content="reload">
...
</head>

如果您使用的是 Turbo Rails,则可以使用 turbo_page_requires_reload 帮助程序来完成相同的事情。

指定 turbo-visit-control reload 的页面将始终导致全页导航,即使请求来自框架内部。

如果您的应用程序需要以其他方式处理丢失的框架,您可以拦截 turbo:frame-missing 事件,例如,转换响应或访问其他位置。

防伪支持 (CSRF)

Turbo 通过检查 DOM 中是否存在 name 值为 csrf-paramcsrf-token<meta> 标记来提供 CSRF 保护。例如

<meta name="csrf-token" content="[your-token]">

在提交表单时,令牌将自动添加到请求的标头中,作为 X-CSRF-TOKEN。使用 data-turbo="false" 发出的请求将跳过向标头添加令牌。

自定义渲染

Turbo 的默认 <turbo-frame> 渲染流程会用响应中匹配的 <turbo-frame> 元素的内容替换请求的 <turbo-frame> 元素的内容。实际上,<turbo-frame> 元素的内容会像由 <turbo-stream action="update"> 元素操作一样进行渲染。底层渲染器会提取响应中 <turbo-frame> 的内容,并用它们替换请求的 <turbo-frame> 元素的内容。<turbo-frame> 元素本身保持不变,除了 [src][busy][complete] 属性,Turbo Drive 会在元素的请求-响应生命周期的各个阶段管理这些属性。

应用可以通过添加 turbo:before-frame-render 事件侦听器并覆盖 event.detail.render 属性来自定义 <turbo-frame> 渲染流程。

例如,你可以使用 morphdom 将响应 <turbo-frame> 元素合并到请求的 <turbo-frame> 元素中

import morphdom from "morphdom"

addEventListener("turbo:before-frame-render", (event) => {
event.detail.render = (currentElement, newElement) => {
morphdom(currentElement, newElement, { childrenOnly: true })
}
})

由于 turbo:before-frame-render 事件会冒泡到文档中,因此你可以通过将事件侦听器直接附加到元素来覆盖一个 <turbo-frame> 元素的渲染,或者通过将侦听器附加到 document 来覆盖所有 <turbo-frame> 元素的渲染。

暂停渲染

应用可以在继续之前暂停渲染并进行其他准备。

侦听 turbo:before-frame-render 事件以在渲染即将开始时收到通知,并使用 event.preventDefault() 暂停它。准备完成后,通过调用 event.detail.resume() 继续渲染。

一个示例用例是添加退出动画

document.addEventListener("turbo:before-frame-render", async (event) => {
event.preventDefault()

await animateOut()

event.detail.resume()
})

下一步:使用 Turbo Streams 焕发生机