Single Fetch
单次获取
Single Fetch 是一种新的数据加载策略和流式传输格式。启用 Single Fetch 后,Remix 将在客户端转换时向您的服务器发出单个 HTTP 调用,而不是并行发出多个 HTTP 调用(每个加载器一个)。此外,Single Fetch 还允许您从加载器
和操作
中发送裸对象,例如 Date
、Error
、Promise
、RegExp
等。
概述
Remix 在 v2.9.0
中的 future.unstable_singleFetch
标志后面引入了对 Single Fetch
(RFC)的支持(后来在 v2.13.0
中稳定为 future.v3_singleFetch
),这允许您选择加入此行为。Single Fetch 将成为 React Router v7 中的默认设置。
启用 Single Fetch 旨在降低前期工作量,然后允许您随着时间的推移迭代地采用所有重大更改。您可以先对 启用 Single Fetch 应用所需的最少更改,然后使用 迁移指南 在您的应用程序中进行增量更改,以确保顺利、无中断地升级到 React Router v7。
另请查看重大变化,以便您了解一些底层行为变化,特别是围绕序列化和状态 / 标头行为的变化。
启用单次获取
1. 启用未来标志
2. 弃用 fetch
polyfill
单一提取需要使用 undici
作为 fetch
polyfill,或者使用 Node 20+ 上的内置 fetch
,因为它依赖于 @remix-run/web-fetch
polyfill 中没有的 API。请参阅下面 2.9.0 发行说明中的 Undici 部分以了解更多详细信息。
-
如果您使用的是 Node 20+,请删除对
installGlobals()
的任何调用并使用 Node 的内置fetch
(这与undici
相同)。 -
如果您正在管理自己的服务器并调用
installGlobals()
,则需要调用installGlobals({nativeFetch:true})
来使用undici
。
-
如果您使用
remix-serve
,如果启用了单一获取,它将自动使用undici
。 -
如果您在 remix 项目中使用 miniflare/cloudflare worker,请确保您的 兼容性标志 也设置为
2023-03-01
或更高版本。
3. 调整 headers
实现(如有必要)
启用单一提取
后,即使需要运行多个加载器,客户端导航也只会发出一个请求。为了处理调用的处理程序的合并标头,headers
导出现在也将应用于加载器
/ 操作
数据请求。在许多情况下,您已经在其中为文档请求设置的逻辑应该足以满足您的新单一提取
数据请求。
4. 添加 nonce
(如果你使用 CSP)
如果您有一个带有 nonce-sources 的 脚本内容安全策略,则需要将该 nonce
添加到两个位置以实现流式单一提取实现:
<RemixServer nonce={yourNonceValue}>
- 这会将nonce
添加到此组件呈现的内联脚本中,用于处理客户端的流数据- 在您的
entry.server.tsx
中将options.nonce
参数添加到renderToPipeableStream
/renderToReadableStream
。另请参阅 Remix Streaming docs
5. 替换 renderToString
(如果您正在使用它)
对于大多数 Remix 应用来说,您不太可能使用 renderToString
,但如果您选择在 entry.server.tsx
中使用它,请继续阅读,否则您可以跳过此步骤。
为了保持文档和数据请求之间的一致性,turbo-stream
还用作初始文档请求中向下发送数据的格式。这意味着一旦选择使用 Single Fetch,您的应用程序就不能再使用 renderToString
,而必须使用 React 流渲染器 API,例如 renderToPipeableStream
或 renderToReadableStream
)(在 entry.server.tsx
中)。
这并不意味着您必须流式传输 HTTP 响应,您仍然可以通过利用 renderToPipeableStream
中的 onAllReady
选项或 renderToReadableStream
中的 allReady
承诺一次发送完整文档。
在客户端,这也意味着您需要将客户端 hydrateRoot
调用包装在 startTransition
调用中,因为流数据将被包裹在 Suspense
边界中。
重大变化
Single Fetch 引入了一些重大更改 - 其中一些需要在启用标志时预先处理,而另一些则可以在启用标志后逐步处理。您需要确保在更新到下一个主要版本之前已处理所有这些更改。
需要提前解决的变化:
- 弃用的
fetch
polyfill:旧的installGlobals()
polyfill 不适用于单一提取,您必须使用本机 Node 20fetch
API 或在自定义服务器中调用installGlobals({ nativeFetch: true })
以获取 基于 undici 的 polyfill headers
导出应用于数据请求:headers
函数现在将应用于文档和数据请求
您可能需要加班处理的变更:
- 新的流式数据格式:单次获取通过
turbo-stream
在后台使用一种新的流式格式,这意味着我们可以传输比 JSON 更复杂的数据 - 不再自动序列化:从
loader
和action
函数返回的裸对象不再自动转换为 JSONResponse
,而是通过网络按原样序列化 - 类型推断更新:为了获得最准确的类型推断,您应该使用
v3_singleFetch: true
[增强][增强] Remix 的Future
接口 - GET 导航上的默认重新验证行为更改为选择退出:正常导航上的默认重新验证行为从选择加入更改为选择退出,并且您的服务器加载器将默认重新运行
- 选择加入
action
重新验证:在操作``4xx
/5xx``响应
之后重新验证现在是选择加入,而不是选择退出
使用单次获取添加新路由
启用单一获取后,您可以继续创作利用更强大的流格式的路由。
v3_singleFetch: true
来增强 Remix 的 Future
接口。您可以在 类型推断部分 中阅读更多相关信息。
使用单一获取,您可以从加载器返回以下数据类型:BigInt
、Date
、Error
、Map
、Promise
、RegExp
、Set
、Symbol
和 URL
。
使用单次获取迁移路由
如果您当前正在从加载器返回 Response
实例(即 json
/defer
),那么您不需要对应用程序代码进行太多更改即可利用单一获取。
但是,为了更好地准备将来升级到 React Router v7,我们建议您开始逐个路由进行以下更改,因为这是验证更新标题和数据类型不会破坏任何内容的最简单方法。
类型推断
如果没有单一提取,任何从加载器
或操作
返回的纯 Javascript 对象都会自动序列化为 JSON 响应(就像您通过 json
返回它一样)。类型推断假定情况如此,并推断裸对象返回,就好像它们是 JSON 序列化的一样。
使用单次提取时,裸对象将直接进行流式传输,因此一旦您选择单次提取,内置类型推断就不再准确。例如,他们会假设日期
将在客户端上序列化为字符串😕。
启用单一获取类型
要切换到单一提取类型,您应该使用 v3_singleFetch:true
增强 Remix 的 Future
接口。
您可以在 tsconfig.json
>include
涵盖的任何文件中执行此操作。
我们建议您在 vite.config.ts
中执行此操作,以使其与 Remix 插件中的 future.v3_singleFetch
未来标志保持在同一位置:
现在,useLoaderData
、useActionData
以及任何其他使用 typeof loader
泛型的实用程序都应该使用单一获取类型:
函数和类实例
一般来说,函数无法通过网络可靠地发送,因此它们被序列化为未定义
:
方法也不可序列化,因此类实例被精简为仅具有可序列化的属性:
clientLoader
和 clientAction
clientLoader
参数和 clientAction
参数的类型,因为这是我们的类型检测客户端数据函数的方式。
来自客户端加载器和操作的数据永远不会被序列化,因此这些数据的类型会被保留:
标题
当启用单一提取
时,headers
函数现在可用于文档和数据请求。您应该使用该函数合并并行执行的加载器返回的任何标头,或返回任何给定的 actionHeaders
。
返回的响应
使用 Single Fetch,您不再需要返回 Response
实例,而只需通过裸对象返回直接返回数据即可。因此,在使用 Single Fetch 时,应将 json
/defer
实用程序视为已弃用。这些实用程序将保留到 v2 的持续时间,因此您无需立即删除它们。它们可能会在下一个主要版本中被删除,因此我们建议从现在开始逐步删除它们。
对于 v2,您仍然可以继续返回正常的 Response
实例,并且它们的 status
/headers
将以与文档请求相同的方式生效(通过 headers()
函数合并标题)。
随着时间的推移,您应该开始从加载器和操作中消除返回的响应。
- 如果您的
loader
/action
返回json
/defer
而没有设置任何status
/headers
,那么您只需删除对json
/defer
的调用并直接返回数据即可 - 如果您的
loader
/action
通过json
/defer
返回自定义status
/headers
,那么您应该切换它们以使用新的data()
实用程序。
客户端加载器
如果您的应用有使用 clientLoader
函数的路由,请务必注意,Single Fetch 的行为会略有变化。因为 clientLoader
旨在为您提供一种选择不调用服务器 loader
函数的方法 - Single Fetch 调用执行该服务器加载器是不正确的。但我们并行运行所有加载器,我们不想等到知道哪些 clientLoader
实际请求服务器数据后才进行调用。
例如,考虑以下 /a/b/c
路由:
如果用户从 / -> /a/b/c
导航,那么我们需要运行 a
和 b
的服务器加载器,以及 c
的 clientLoader
- 最终可能会(也可能不会)调用它自己的服务器 loader
。当我们想要获取 a
/b
loader
时,我们无法决定在单个获取调用中包含 c
服务器 loader
,我们也无法延迟到 c
实际进行 serverLoader
调用(或返回)而不引入瀑布。
因此,当您导出 clientLoader
时,该路由将退出单次提取,而当您调用 serverLoader
时,它将进行单次提取以仅获取其路由服务器 loader
。所有未导出 clientLoader
的路由都将在单个 HTTP 请求中进行提取。
因此,在上述路由设置中,从 / -> /a/b/c
导航将导致对路由 a
和 b
进行一次单一获取调用:
然后当 c
调用 serverLoader
时,它将仅对 c
服务器加载器
进行自己的调用:
资源路由
由于 Single Fetch 使用新的 流格式,从 loader
和 action
函数返回的原始 JavaScript 对象不再通过 json()
实用程序自动转换为 Response
实例。相反,在导航数据加载中,它们与其他加载器数据相结合,并在 turbo-stream
响应中向下传输。
这对 资源路由 提出了一个有趣的难题,因为它们是独一无二的,因为它们旨在单独访问 - 并且并不总是通过 Remix API。它们也可以通过任何其他 HTTP 客户端(fetch
、cURL
等)访问。
如果资源路由旨在供内部 Remix API 使用,我们希望能够利用 turbo-stream
编码来解锁流式传输更复杂结构(例如 Date
和 Promise
实例)的能力。但是,当从外部访问时,我们可能更愿意返回更易于使用的 JSON 结构。因此,如果您在 v2 中返回原始对象,则行为会略显模糊 - 应该通过 turbo-stream
还是 json()
对其进行序列化?
为了简化向后兼容性并简化未来单一提取标志的采用,Remix v2 将根据是从 Remix API 还是外部访问来处理此问题。将来,如果您不希望原始对象被流式传输以供外部使用,Remix 将要求您返回自己的 [JSON 响应][返回响应]。
启用单一获取后的 Remix v2 行为如下:
-
当从 Remix API(例如
useFetcher
)访问时,原始 Javascript 对象将作为turbo-stream
响应返回,就像普通的加载器和操作一样(这是因为useFetcher
会将.data
后缀附加到请求) -
当从
fetch
或cURL
等外部工具访问时,我们将继续自动转换为json()
,以实现 v2 中的向后兼容性: -
遇到这种情况时,Remix 将记录弃用警告
-
您可以根据需要更新受影响的资源路由处理程序以返回
Response
对象 -
解决这些弃用警告将让您更好地为最终的 Remix v3 升级做好准备
其他详细信息
流数据格式
以前,Remix 使用 JSON.stringify
通过网络序列化您的加载器 / 动作数据,并且需要实现自定义流格式来支持 defer
响应。
借助 Single Fetch,Remix 现在在底层使用 turbo-stream
,它为流式传输提供一流的支持,并允许您自动序列化 / 反序列化比 JSON 更复杂的数据。以下数据类型可以通过 turbo-stream
直接流式传输:BigInt
、Date
、Error
、Map
、Promise
、RegExp
、Set
、Symbol
和 URL
。只要 Error
的子类型在客户端上具有全局可用的构造函数(SyntaxError
、TypeError
等),它们也受支持。
一旦启用单次提取,这可能会或可能不会需要立即更改您的代码:
- ✅ 从
loader
/action
函数返回的json
响应仍将通过JSON.stringify
进行序列化,因此如果您返回Date
,您将从useLoaderData
/useActionData
收到一个string
- ⚠️ 如果您返回
defer
实例或裸对象,它现在将通过turbo-stream
进行序列化,因此如果您返回Date
,您将从useLoaderData
/useActionData
收到一个Date
- 如果您希望保持当前行为(不包括流式
defer
响应),您可以将任何现有的裸对象返回包装在json
中
这也意味着您不再需要使用 defer
实用程序通过网络发送 Promise
实例!您可以在裸对象中的任何位置包含 Promise
,并在 useLoaderData().whatever
上拾取它。如果需要,您还可以嵌套 Promise
- 但要注意潜在的用户体验影响。
一旦采用单一获取,建议您在整个应用程序内逐步删除 json
/defer
的使用,转而返回原始对象。
流超时
此前,Remix 在默认的 entry.server.tsx
文件中内置了一个 ABORT_TIMEOUT
概念,它会终止 React 渲染器,但它并没有采取任何特别的措施来清理任何待处理的延期承诺。
现在 Remix 正在内部流式传输,我们可以取消 turbo-stream
处理并自动拒绝任何待处理的承诺并将这些错误流式传输到客户端。默认情况下,这种情况会在 4950 毫秒后发生 - 该值被选择为略低于大多数 entry.server.tsx 文件中当前的 5000 毫秒 ABORT_DELAY
- 因为我们需要取消承诺并让拒绝在中止 React 方面之前通过 React 渲染器流式传输。
您可以通过从 entry.server.tsx
导出 streamTimeout
数值来控制这一点,Remix 将使用该值作为拒绝 loader
/action
中任何未完成的 Promises 的毫秒数。建议将此值与您中止 React 渲染器的超时时间分离 - 并且您应该始终将 React 超时时间设置为更高的值,以便有时间从 streamTimeout
中流出底层拒绝。
重新验证
正常导航行为
除了更简单的思维模型以及文档和数据请求的一致性之外,单次获取的另一个好处是更简单(希望更好)的缓存行为。通常,与之前的多次获取行为相比,单次获取会发出更少的 HTTP 请求,并且希望更频繁地缓存这些结果。
为了减少缓存碎片,单一获取会更改 GET 导航的默认重新验证行为。以前,除非您通过 shouldRevalidate
选择加入,否则 Remix 不会为重复使用的祖先路由重新运行加载器。现在,对于 GET /a/b/c.data
等单一获取请求的简单情况,Remix 将默认重新运行这些加载器。如果您没有任何 shouldRevalidate
或 clientLoader
函数,这将是您的应用的行为。
向任何活动路由添加 shouldRevalidate
或 clientLoader
将触发细粒度的单一获取调用,其中包括一个_routes
参数,用于指定要运行的路由子集。
如果 clientLoader
在内部调用 serverLoader()
,则会为该特定路由触发单独的 HTTP 调用,类似于旧的行为。
例如,如果您在 /a/b
并且导航到 /a/b/c
:
- 当不存在
shouldRevalidate
或clientLoader
函数时:GET /a/b/c.data
- 如果所有路由都有加载器,但
routes/a
通过shouldRevalidate
选择退出: GET /a/b/c.data?_routes=root,routes/b,routes/c
- 如果所有路由都有加载器,但
routes/b
有clientLoader
: GET /a/b/c.data?_routes=root,routes/a,routes/c
- 然后,如果 B 的
clientLoader
调用serverLoader()
: GET /a/b/c.data?_routes=routes/b
如果这种新行为对于您的应用程序来说不是最优的,您应该能够通过向您的父路由添加在所需场景中返回 false
的 shouldRevalidate
来选择回到不重新验证的旧行为。
另一种选择是利用服务器端缓存进行昂贵的父加载器计算。
提交重新验证行为
以前,Remix 会在提交任何操作后始终重新验证所有活动加载器,无论操作结果如何。您可以通过 shouldRevalidate
根据每个路由选择退出重新验证。
使用单一提取时,如果操作
返回或抛出带有 4xx/5xx
状态代码的响应
,则默认情况下 Remix 将不会重新验证加载器。如果操作
返回或抛出任何非 4xx/5xx 响应,则重新验证行为不会改变。这里的原因是,在大多数情况下,如果您返回 4xx
/5xx
响应,则您实际上并没有改变任何数据,因此无需重新加载数据。
如果您希望在 4xx/5xx 操作响应后继续重新验证一个或多个加载器,您可以通过从 shouldRevalidate
函数返回 true
来选择在每个路由上进行重新验证。此外,还有一个新的 actionStatus
参数传递给该函数,如果您需要根据操作状态代码做出决定,您可以使用它。
重新验证通过单个 fetch HTTP 调用上的 ?_routes
查询字符串参数进行处理,该参数限制了被调用的加载器。这意味着,当您进行细粒度重新验证时,您将根据所请求的路由进行缓存枚举 - 但所有信息都在 URL 中,因此您不需要任何特殊的 CDN 配置(与通过自定义标头完成此操作相反,自定义标头要求您的 CDN 遵守 Vary
标头)。