构建您的 Turbo 应用程序
Turbo 速度快,因为它在您点击链接或提交表单时阻止整个页面重新加载。您的应用程序成为浏览器中持久的、长期运行的进程。这需要您重新考虑您构建 JavaScript 的方式。
特别是,您不能再依赖每次导航时都进行一次完整的页面加载来重置您的环境。JavaScript window
和 document
对象在页面更改中保留其状态,并且您留在内存中的任何其他对象都将保留在内存中。
通过认识到这一点并多加小心,您可以在不将其与 Turbo 紧密耦合的情况下设计您的应用程序来优雅地处理此约束。
﹟ 使用脚本元素
您的浏览器在初始页面加载时自动加载并评估任何 <script>
元素。
当您导航到新页面时,Turbo Drive 会在当前页面上不存在的新页面的 <head>
中查找任何 <script>
元素。然后,它将它们附加到当前 <head>
,浏览器会在其中加载并评估它们。您可以使用此功能按需加载其他 JavaScript 文件。
Turbo Drive 在每次呈现页面时都会评估页面 <body>
中的 <script>
元素。您可以使用内联正文脚本设置每个页面的 JavaScript 状态或引导客户端模型。要安装行为或在页面更改时执行更复杂的操作,请避免使用脚本元素,而应使用 turbo:load
事件。
如果您不希望 Turbo 在呈现后评估 <script>
元素,请使用 data-turbo-eval="false"
对其进行注释。请注意,此注释不会阻止您的浏览器在初始页面加载时评估脚本。
﹟ 加载您的应用程序的 JavaScript 捆绑包
务必始终使用 <script>
元素在文档的 <head>
中加载您的应用程序的 JavaScript 捆绑包。否则,Turbo Drive 将在每次页面更改时重新加载捆绑包。
<head>
...
<script src="/application-cbd3cd4.js" defer></script>
</head>
您还应考虑配置您的资产打包系统,以便对每个脚本进行指纹识别,以便在内容更改时使用新的 URL。然后,您可以使用 data-turbo-track
属性在部署新的 JavaScript 捆绑包时强制进行完整的页面重新加载。有关信息,请参阅 在资产更改时重新加载。
﹟ 了解缓存
Turbo Drive 维护最近访问的页面的缓存。此缓存有两个目的:在还原访问期间在不访问网络的情况下显示页面,以及通过在应用程序访问期间显示临时预览来提高感知性能。
通过历史记录导航(通过 还原访问),如果可能,Turbo Drive 将从缓存中还原页面,而不会从网络加载新副本。
否则,在标准导航(通过 应用程序访问)期间,Turbo Drive 将立即从缓存中恢复页面并将其显示为预览,同时从网络加载新副本。这给频繁访问的位置带来了瞬间页面加载的错觉。
Turbo Drive 在呈现新页面之前将当前页面的副本保存到其缓存中。请注意,Turbo Drive 使用 cloneNode(true)
复制页面,这意味着任何附加的事件侦听器和关联数据都会被丢弃。
﹟ 准备要缓存的页面
如果您需要在 Turbo Drive 缓存文档之前准备文档,请侦听 turbo:before-cache
事件。您可以使用此事件重置表单、折叠展开的 UI 元素或拆除任何第三方小部件,以便页面可以再次显示。
document.addEventListener("turbo:before-cache", function() {
// ...
})
某些页面元素本质上是临时的,例如闪存消息或警报。如果它们与文档一起缓存,则在恢复文档时它们将重新显示,这通常是不可取的。您可以使用 data-turbo-temporary
注释此类元素,以便 Turbo Drive 在缓存页面之前自动将它们从页面中删除。
<body>
<div class="flash" data-turbo-temporary>
Your cart was updated!
</div>
...
</body>
﹟ 检测预览何时可见
当 Turbo Drive 从缓存中显示预览时,它会向 <html>
元素添加 data-turbo-preview
属性。您可以检查此属性的存在,以便在预览可见时有选择地启用或禁用行为。
if (document.documentElement.hasAttribute("data-turbo-preview")) {
// Turbo Drive is displaying a preview
}
﹟ 选择不缓存
您可以通过在页面的 <head>
中包含 <meta name="turbo-cache-control">
元素并声明缓存指令,逐页控制缓存行为。
使用 no-preview
指令指定在应用程序访问期间不应将缓存版本的页面显示为预览。标记为 no-preview 的页面将仅用于恢复访问。
要指定不应缓存页面,请使用 no-cache
指令。标记为 no-cache 的页面将始终通过网络获取,包括在恢复访问期间。
<head>
...
<meta name="turbo-cache-control" content="no-cache">
</head>
要完全禁用应用程序中的缓存,请确保每个页面都包含 no-cache 指令。
﹟ 从客户端选择不缓存
<meta name="turbo-cache-control">
元素的值还可以通过 Turbo.cache
公开的客户端 API 来控制。
// Set cache control of current page to `no-cache`
Turbo.cache.exemptPageFromCache()
// Set cache control of current page to `no-preview`
Turbo.cache.exemptPageFromPreview()
这两个函数都会在 <head>
中创建一个 <meta name="turbo-cache-control">
元素(如果该元素不存在)。
可以通过以下方式重置先前设置的缓存控制值
Turbo.cache.resetCacheControl()
﹟ 安装 JavaScript 行为
您可能习惯于响应 window.onload
、DOMContentLoaded
或 jQuery ready
事件来安装 JavaScript 行为。使用 Turbo 时,这些事件只会响应初始页面加载,而不会响应任何后续页面更改。我们比较了以下连接 JavaScript 行为到 DOM 的两种策略。
﹟ 观察导航事件
Turbo Drive 在导航期间会触发一系列事件。其中最重要的事件是 turbo:load
事件,它在初始页面加载时触发一次,并在每次 Turbo Drive 访问后再次触发。
您可以观察 turbo:load
事件来代替 DOMContentLoaded
,以便在每次页面更改后设置 JavaScript 行为
document.addEventListener("turbo:load", function() {
// ...
})
请记住,当触发此事件时,您的应用程序并不总是处于原始状态,您可能需要清理为前一页安装的行为。
另请注意,Turbo Drive 导航可能不是应用程序中页面更新的唯一来源,因此您可能希望将初始化代码移到一个单独的函数中,您可以从 turbo:load
和您可能更改 DOM 的任何其他位置调用该函数。
如果可能,请避免使用 turbo:load
事件直接向页面正文中的元素添加其他事件侦听器。相反,请考虑使用 事件委托 在 document
或 window
上一次注册事件侦听器。
有关更多信息,请参阅 事件完整列表。
﹟ 使用 Stimulus 附加行为
新 DOM 元素可以通过帧导航、流消息或客户端渲染操作随时出现在页面上,这些元素通常需要初始化,就像它们来自新的页面加载一样。
您可以通过 Turbo 的姊妹框架 Stimulus 提供的约定和生命周期回调,在一个地方处理所有这些更新,包括 Turbo Drive 页面加载的更新。
Stimulus 允许您使用控制器、操作和目标属性对 HTML 进行注释
<div data-controller="hello">
<input data-hello-target="name" type="text">
<button data-action="click->hello#greet">Greet</button>
</div>
实现一个兼容的控制器,Stimulus 会自动连接它
// hello_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
greet() {
console.log(`Hello, ${this.name}!`)
}
get name() {
return this.targets.find("name").value
}
}
Stimulus 使用 MutationObserver API 在文档更改时连接和断开这些控制器及其关联的事件处理程序。因此,它处理 Turbo Drive 页面更改、Turbo Frames 导航和 Turbo Streams 消息的方式与处理任何其他类型的 DOM 更新的方式相同。
﹟ 使转换具有幂等性
通常,您会希望对从服务器接收的 HTML 执行客户端转换。例如,您可能希望利用浏览器对用户当前时区的了解,按日期对元素集合进行分组。
假设您已使用 data-timestamp
属性注释了一组元素,这些属性指示元素在 UTC 中的创建时间。您有一个 JavaScript 函数,用于查询文档中所有此类元素,将时间戳转换为本地时间,并在新的一天中出现的每个元素之前插入日期标题。
考虑一下,如果您已将此函数配置为在 turbo:load
上运行,会发生什么。当您导航到该页面时,您的函数会插入日期标题。导航离开,Turbo Drive 会将转换后的页面副本保存到其缓存中。现在按后退按钮——Turbo Drive 会还原页面,再次触发 turbo:load
,您的函数会插入第二组日期标题。
为避免此问题,请使您的转换函数幂等。幂等转换可以安全地多次应用,而不会在首次应用后更改结果。
使转换幂等的一种技术是通过在每个已处理元素上设置 data
属性来跟踪您是否已执行该转换。当 Turbo Drive 从缓存还原您的页面时,这些属性仍将存在。在您的转换函数中检测这些属性,以确定哪些元素已处理。
更健壮的技术只是检测转换本身。在上面的日期分组示例中,这意味着在插入新日期分隔符之前检查是否存在日期分隔符。此方法可以很好地处理未经原始转换处理的新插入元素。
﹟ 在页面加载中持久化元素
Turbo Drive 允许您将某些元素标记为永久。永久元素在页面加载中持久存在,因此您对这些元素所做的任何更改在导航后不需要重新应用。
考虑一个带有购物车的 Turbo Drive 应用程序。在每个页面的顶部是一个图标,其中显示了当前购物车中的商品数量。随着商品的添加和删除,此计数器会通过 JavaScript 动态更新。
现在想象一下,用户已导航到此应用程序中的多个页面。她在购物车中添加了一个商品,然后按浏览器中的后退按钮。导航后,Turbo Drive 会从缓存中还原前一页的状态,并且购物车商品数量会错误地从 1 更改为 0。
您可以通过将计数器元素标记为永久来避免此问题。通过为永久元素指定 HTML id
并使用 data-turbo-permanent
对其进行注释来指定永久元素。
<div id="cart-counter" data-turbo-permanent>1 item</div>
在每次渲染之前,Turbo Drive 会通过 ID 匹配所有永久元素,并将它们从原始页面传输到新页面,保留它们的数据和事件侦听器。