使用 Turbo Drive 导航
Turbo Drive 是 Turbo 中增强页面级导航的部分。它监视链接点击和表单提交,在后台执行它们,并在不进行完全重新加载的情况下更新页面。它是以前称为 Turbolinks 的库的演变。
﹟ 页面导航基础
Turbo Drive 将页面导航建模为对具有操作的位置(URL)的访问。
访问代表从点击到渲染的整个导航生命周期。其中包括更改浏览器历史记录、发出网络请求、从缓存恢复页面副本、渲染最终响应以及更新滚动位置。
在渲染期间,Turbo Drive 使用响应文档的 <body>
内容替换请求文档的 <body>
内容,同时合并其 <head>
的内容,并根据需要更新 <html>
元素的 lang
属性。合并而不是替换 <head>
元素的目的是,如果 <title>
或 <meta>
标记发生变化,它们将按预期更新,但如果对资产的链接相同,它们将保持不变,因此浏览器不会再次处理它们。
有两种类型的访问:操作为 advance 或 replace 的 应用程序访问,以及操作为 restore 的 恢复访问。
﹟ 应用程序访问
应用程序访问是通过单击启用了 Turbo Drive 的链接或通过编程方式调用 Turbo.visit(location)
来启动的。
应用程序访问始终会发出网络请求。当响应到达时,Turbo Drive 渲染其 HTML 并完成访问。
如果可能,Turbo Drive 会在访问开始后立即从缓存中呈现页面的预览。这提高了在同一页面之间频繁导航的感知速度。
如果访问的位置包含锚点,Turbo Drive 将尝试滚动到锚定元素。否则,它将滚动到页面顶部。
应用程序访问会导致浏览器历史记录发生变化;访问的 操作 决定了如何变化。
默认访问操作为 advance。在高级访问期间,Turbo Drives 使用 history.pushState
将新条目推送到浏览器的历史记录堆栈。
使用 Turbo Drive iOS 适配器 的应用程序通常通过将新视图控制器推送到导航堆栈来处理高级访问。类似地,使用 Android 适配器 的应用程序通常将新活动推送到后退堆栈。
您可能希望访问某个位置,而无需将新的历史记录项推送到堆栈中。replace 访问操作使用 history.replaceState
丢弃最顶层历史记录项,并用新位置替换它。
要指定在点击链接后触发替换访问,请使用 data-turbo-action="replace"
注释链接
<a href="/edit" data-turbo-action="replace">Edit</a>
要以替换操作方式以编程方式访问某个位置,请将 action: "replace"
选项传递给 Turbo.visit
Turbo.visit("/edit", { action: "replace" })
使用 Turbo Drive iOS 适配器 的应用程序通常通过关闭最顶层视图控制器并在导航堆栈中推送一个新视图控制器(无动画)来处理替换访问。
﹟ 还原访问
当您使用浏览器的后退或前进按钮导航时,Turbo Drive 会自动启动还原访问。使用 iOS 或 Android 适配器的应用程序在导航堆栈中向后移动时会启动还原访问。
如果可能,Turbo Drive 将从缓存中呈现页面的副本,而无需发出请求。否则,它将通过网络检索页面的新副本。有关更多详细信息,请参阅 了解缓存。
Turbo Drive 在导航离开之前保存每个页面的滚动位置,并在还原访问时自动返回到此已保存位置。
还原访问的操作为 restore,Turbo Drive 将其保留供内部使用。您不应尝试使用 restore
操作注释链接或调用 Turbo.visit
。
﹟ 在访问开始前取消访问
无论应用程序访问是由点击链接还是调用 Turbo.visit
发起的,都可以取消访问。
监听 turbo:before-visit
事件,以便在访问即将开始时收到通知,并使用 event.detail.url
(或在使用 jQuery 时使用 $event.originalEvent.detail.url
)检查访问的位置。然后通过调用 event.preventDefault()
取消访问。
还原访问无法取消,并且不会触发 turbo:before-visit
。Turbo Drive 根据已发生的历史记录导航(通常通过浏览器的后退或前进按钮)发出还原访问。
﹟ 自定义呈现
应用程序可以通过添加文档范围的 turbo:before-render
事件侦听器并覆盖 event.detail.render
属性来自定义呈现过程。
例如,您可以使用 morphdom 将响应文档的 <body>
元素合并到请求文档的 <body>
元素中
import morphdom from "morphdom"
addEventListener("turbo:before-render", (event) => {
event.detail.render = (currentElement, newElement) => {
morphdom(currentElement, newElement)
}
})
﹟ 暂停渲染
应用程序可以在继续之前暂停渲染并进行其他准备。
监听 `turbo:before-render` 事件,以便在渲染即将开始时收到通知,并使用 `event.preventDefault()` 暂停渲染。准备完成后,通过调用 `event.detail.resume()` 继续渲染。
一个示例用例是为访问添加退出动画
document.addEventListener("turbo:before-render", async (event) => {
event.preventDefault()
await animateOut()
event.detail.resume()
})
﹟ 暂停请求
应用程序可以在请求执行之前暂停请求并进行其他准备。
监听 `turbo:before-fetch-request` 事件,以便在请求即将开始时收到通知,并使用 `event.preventDefault()` 暂停请求。准备完成后,通过调用 `event.detail.resume()` 继续请求。
一个示例用例是为请求设置 `Authorization` 头
document.addEventListener("turbo:before-fetch-request", async (event) => {
event.preventDefault()
const token = await getSessionToken(window.app)
event.detail.fetchOptions.headers["Authorization"] = `Bearer ${token}`
event.detail.resume()
})
﹟ 使用不同的方法执行访问
默认情况下,链接点击会向服务器发送 `GET` 请求。但你可以使用 `data-turbo-method` 更改此设置
<a href="/articles/54" data-turbo-method="delete">Delete the article</a>
链接将被转换为 DOM 中 `a` 元素旁边的隐藏表单。这意味着链接不能出现在另一个表单中,因为你不能嵌套表单。
你还应考虑出于辅助功能原因,最好对非 GET 内容使用实际表单和按钮。
﹟ 要求访问确认
使用 `data-turbo-confirm` 修饰链接,访问将需要确认才能继续。
<a href="/articles" data-turbo-confirm="Do you want to leave this page?">Back to articles</a>
<a href="/articles/54" data-turbo-method="delete" data-turbo-confirm="Are you sure you want to delete the article?">Delete the article</a>
使用 `Turbo.setConfirmMethod` 更改用于确认的方法。默认方法是浏览器的内置 `confirm`。
﹟ 在特定链接或表单上禁用 Turbo Drive
可以通过使用 `data-turbo="false"` 注释元素或其任何祖先元素,逐个元素禁用 Turbo Drive。
<a href="/" data-turbo="false">Disabled</a>
<form action="/messages" method="post" data-turbo="false">
...
</form>
<div data-turbo="false">
<a href="/">Disabled</a>
<form action="/messages" method="post">
...
</form>
</div>
要在祖先元素选择退出时重新启用,请使用 `data-turbo="true"`
<div data-turbo="false">
<a href="/" data-turbo="true">Enabled</a>
</div>
禁用 Turbo Drive 的链接或表单将由浏览器正常处理。
如果你希望 Drive 是选择加入而不是选择退出,则可以设置 `Turbo.session.drive = false`;然后,`data-turbo="true"` 用于逐个元素启用 Drive。如果你在 JavaScript 包中导入 Turbo,则可以在全局范围内执行此操作
import { Turbo } from "@hotwired/turbo-rails"
Turbo.session.drive = false
﹟ 视图过渡
在 支持 视图过渡 API 的浏览器中,Turbo 可以在页面之间导航时触发视图过渡。
当当前页面和下一页都有此元标记时,Turbo 会触发视图转换
<meta name="view-transition" content="same-origin" />
Turbo 还会向 <html>
元素添加 data-turbo-visit-direction
属性,以指示转换的方向。该属性可以具有以下值之一
- 预先访问中的
forward
。 - 恢复访问中的
back
。 - 替换访问中的
none
。
您可以在转换期间使用此属性自定义执行的动画
html[data-turbo-visit-direction="forward"]::view-transition-old(sidebar):only-child {
animation: slide-to-right 0.5s ease-out;
}
﹟ 显示进度
在 Turbo Drive 导航期间,浏览器不会显示其本机进度指示器。Turbo Drive 安装基于 CSS 的进度条,以便在发出请求时提供反馈。
默认情况下启用进度条。对于加载时间超过 500 毫秒的任何页面,它都会自动出现。(您可以使用 Turbo.setProgressBarDelay
方法更改此延迟。)
进度条是一个 <div>
元素,其类名为 turbo-progress-bar
。其默认样式首先出现在文档中,并且可以被后续的规则覆盖。
例如,以下 CSS 将生成一个粗绿色的进度条
.turbo-progress-bar {
height: 5px;
background-color: green;
}
要完全禁用进度条,请将其 visibility
样式设置为 hidden
.turbo-progress-bar {
visibility: hidden;
}
与进度条串联,Turbo Drive 还会在从访问或表单提交启动的页面导航期间切换页面 <html>
元素上的 [aria-busy]
属性。导航开始时,Turbo Drive 将设置 [aria-busy="true"]
,并在导航完成后移除 [aria-busy]
属性。
﹟ 资产更改时重新加载
如上所述,Turbo Drive 合并 <head>
元素的内容。但是,如果 CSS 或 JavaScript 发生更改,该合并将在现有 CSS 或 JavaScript 的基础上对它们进行评估。通常,这会导致不希望的冲突。在这种情况下,有必要通过标准的非 Ajax 请求获取一个全新的文档。
要实现此目的,只需使用 data-turbo-track="reload"
注释那些资产元素,并在您的资产 URL 中包含版本标识符。标识符可以是数字、上次修改时间戳,或者更好的是,资产内容的摘要,如下例所示。
<head>
...
<link rel="stylesheet" href="/application-258e88d.css" data-turbo-track="reload">
<script src="/application-cbd3cd4.js" data-turbo-track="reload"></script>
</head>
﹟ 确保特定页面触发完全重新加载
您可以通过在页面的 <head>
中包含 <meta name="turbo-visit-control">
元素来确保对某个页面的访问始终触发完全重新加载。
<head>
...
<meta name="turbo-visit-control" content="reload">
</head>
此设置可能有助于解决与 Turbo Drive 页面更改交互不佳的第三方 JavaScript 库。
﹟ 设置根位置
默认情况下,Turbo Drive 仅加载与当前文档具有相同来源(即相同的协议、域名和端口)的 URL。访问任何其他 URL 都会回退到完整页面加载。
在某些情况下,你可能希望将 Turbo Drive 进一步限定到相同来源上的路径。例如,如果你的 Turbo Drive 应用程序位于 /app
,非 Turbo Drive 帮助站点位于 /help
,则从应用程序到帮助站点的链接不应使用 Turbo Drive。
在页面的 <head>
中包含一个 <meta name="turbo-root">
元素,以将 Turbo Drive 限定到特定根位置。Turbo Drive 将仅加载以该路径为前缀的同源 URL。
<head>
...
<meta name="turbo-root" content="/app">
</head>
﹟ 表单提交
Turbo Drive 处理表单提交的方式类似于链接点击。关键区别在于,表单提交可以使用 HTTP POST 方法发出有状态请求,而链接点击仅发出无状态 HTTP GET 请求。
在整个提交过程中,Turbo Drive 将分派一系列 事件,这些事件针对 <form>
元素,并通过文档 冒泡。
turbo:submit-start
turbo:before-fetch-request
turbo:before-fetch-response
turbo:submit-end
在提交过程中,Turbo Drive 将在提交开始时设置“提交者”元素的 disabled 属性,然后在提交结束后删除该属性。提交 <form>
元素时,浏览器会将启动提交的 <input type="submit">
或 <button>
元素视为 提交者。要以编程方式提交 <form>
元素,请调用 HTMLFormElement.requestSubmit() 方法,并传递 <input type="submit">
或 <button>
元素作为可选参数。
如果你希望在 <form>
提交期间进行其他更改(例如,禁用已提交 <form>
中的所有 字段),你可以声明你自己的事件侦听器。
addEventListener("turbo:submit-start", ({ target }) => {
for (const field of target.elements) {
field.disabled = true
}
})
﹟ 表单提交后重定向
在表单提交的有状态请求之后,Turbo Drive 期望服务器返回 HTTP 303 重定向响应,然后它将遵循该响应并使用它来导航和更新页面,而无需重新加载。
此规则的例外情况是,当响应以 4xx 或 5xx 状态代码呈现时。这允许通过让服务器以422 不可处理实体
进行响应来呈现表单验证错误,并让损坏的服务器在500 内部服务器错误
上显示“出现问题”屏幕。
Turbo 不允许在 POST 请求的 200 上进行常规呈现的原因是,浏览器具有内置行为来处理 POST 访问上的重新加载,其中它们会显示“您确定要再次提交此表单吗?”Turbo 无法复制的对话。相反,Turbo 将在尝试呈现的表单提交时停留在当前 URL 上,而不是将其更改为表单操作,因为重新加载会针对该操作 URL 发出 GET,而该 URL 甚至可能不存在。
如果表单提交是 GET 请求,您可以通过为表单提供data-turbo-frame
目标来呈现直接呈现的响应。如果您希望 URL 在呈现过程中更新,还应传递data-turbo-action
属性。
﹟ 表单提交后的流传输
服务器还可以通过发送标头Content-Type: text/vnd.turbo-stream.html
,后跟响应正文中的一个或多个<turbo-stream>
元素,使用Turbo 流消息来响应表单提交。这使您可以更新页面的多个部分,而无需导航。
﹟ 悬停时预取链接
Turbo 还可以通过在mouseenter
事件上自动加载链接,并在用户单击链接之前,加快感知到的链接导航延迟。这通常会导致每次点击导航速度提高 500-800 毫秒。
自 Turbo v8 以来,默认情况下已启用预取链接,但您可以通过将此元标记添加到您的页面来禁用它
<meta name="turbo-prefetch" content="false">
为了避免预取用户短暂悬停的链接,Turbo 会在用户将鼠标悬停在链接上后等待 100 毫秒,然后再预取它。但您可能希望在指向具有昂贵渲染页面的某些链接上禁用预取行为。
您可以通过使用data-turbo-prefetch="false"
注释元素或其任何祖先来逐个元素禁用此行为。
<html>
<head>
<meta name="turbo-prefetch" content="true">
</head>
<body>
<a href="/articles">Articles</a> <!-- This link is prefetched -->
<a href="/about" data-turbo-prefetch="false">About</a> <!-- Not prefetched -->
<div data-turbo-prefetch="false"`>
<!-- Links inside this div will not be prefetched -->
</div>
</body>
</html>
您还可以通过拦截turbo:before-prefetch
事件并调用event.preventDefault()
来以编程方式禁用此行为。
document.addEventListener("turbo:before-prefetch", (event) => {
if (isSavingData() || hasSlowInternet()) {
event.preventDefault()
}
})
function isSavingData() {
return navigator.connection?.saveData
}
function hasSlowInternet() {
return navigator.connection?.effectiveType === "slow-2g" ||
navigator.connection?.effectiveType === "2g"
}
﹟ 将链接预加载到缓存中
使用data-turbo-preload布尔属性将预加载链接到 Turbo Drive 的缓存中。
这将通过在第一次访问之前提供页面的预览,让页面过渡感觉快如闪电。使用它来预加载应用程序中最重要的页面。避免过度使用,因为它会导致加载不需要的内容。
并非每个<a>
元素都可以预加载。[data-turbo-preload]
属性对以下链接没有任何影响
- 导航到其他域
- 具有
[data-turbo-frame]
属性,可驱动<turbo-frame>
元素 - 驱动祖先
<turbo-frame>
元素 - 具有
[data-turbo="false"]
属性 - 具有
[data-turbo-stream]
属性 - 具有
[data-turbo-method]
属性 - 具有具有
[data-turbo="false"]
属性的祖先 - 具有具有
[data-turbo-prefetch="false"]
属性的祖先
它还与利用 急切加载框架 或 延迟加载框架 的页面很好地契合。由于你可以预加载页面的结构,并在加载有趣内容时向用户显示有意义的加载状态。
请注意,预加载的 <a>
元素将分派 turbo:before-fetch-request 和 turbo:before-fetch-response 事件。要区分由预加载 turbo:before-fetch-request
发起的事件与由其他机制发起的事件,请检查请求的 X-Sec-Purpose
标头(从 event.detail.fetchOptions.headers["X-Sec-Purpose"]
属性中读取)是否设置为 "prefetch"
addEventListener("turbo:before-fetch-request", (event) => {
if (event.detail.fetchOptions.headers["X-Sec-Purpose"] === "prefetch") {
// do additional preloading setup…
} else {
// do something else…
}
})