单次获取
On this page

单次获取

¥Single Fetch

Single Fetch 是一种新的数据加载策略和流格式。当你启用“单次获取”时,Remix 将在客户端转换时向你的服务器发出单次 HTTP 调用,而不是并行发出多个 HTTP 调用(每个加载器一个)。此外,Single Fetch 还允许你从 loaderaction 中发送裸对象,例如 DateErrorPromiseRegExp 等等。

¥Single Fetch is a new data loading strategy and streaming format. When you enable Single Fetch, Remix will make a single HTTP call to your server on client-side transitions, instead of multiple HTTP calls in parallel (one per loader). Additionally, Single Fetch also allows you to send down naked objects from your loader and action, such as Date, Error, Promise, RegExp, and more.

概述

¥Overview

Remix 在 v2.9.0 中引入了对 "单次获取" (RFC) 的支持,并在 future.unstable_singleFetch 标志后方提供支持(后来在 v2.13.0 中稳定为 future.v3_singleFetch),允许你选择启用此行为。Single Fetch 将是 React Router v7 中的默认设置。

¥Remix introduced support for "Single Fetch" (RFC) behind the future.unstable_singleFetch flag in v2.9.0 (later stabilized as future.v3_singleFetch in v2.13.0) which allows you to opt-into this behavior. Single Fetch will be the default in React Router v7.

启用单次获取旨在降低前期工作量,然后允许你随着时间的推移迭代地采用所有重大更改。你可以先对 启用单次抓取 应用所需的最小更改,然后使用 迁移指南 对应用进行增量更改,以确保平稳、无中断地升级到 React Router v7

¥Enabling Single Fetch is intended to be low effort up-front and then allow you to adopt all breaking changes iteratively over time. You can start by applying the minimal required changes to enable Single Fetch, then use the migration guide to make incremental changes in your application to ensure a smooth, non-breaking upgrade to React Router v7.

另请查看 重大变更,以便了解一些底层行为的变化,特别是围绕序列化和状态/标头行为的变化。

¥Please also review the Breaking Changes so you can be aware of some of the underlying behavior changes, specifically around serialization and status/header behavior.

启用单次抓取

¥Enabling Single Fetch

1.

export default defineConfig({
  plugins: [
    remix({
      future: {
        // ...
        v3_singleFetch: true,
      },
    }),
    // ...
  ],
});

2.

Single Fetch 需要使用 undici 作为 fetch polyfill,或者在 Node 20+ 上使用内置的 fetch,因为它依赖于 @remix-run/web-fetch polyfill 中没有的 API。有关更多详细信息,请参阅下面 2.9.0 发行说明中的 Undici 部分。

¥Single Fetch requires using undici as your fetch polyfill, or using the built-in fetch on Node 20+, because it relies on APIs available there that are not in the @remix-run/web-fetch polyfill. Please refer to the Undici section in the 2.9.0 release notes below for more details.

  • 如果你使用的是 Node 20+,请删除所有对 installGlobals() 的调用,并使用 Node 内置的 fetch(与 undici 相同)。

    ¥If you are using Node 20+, remove any calls to installGlobals() and use Node's built-in fetch (this is the same thing as undici).

  • 如果你管理自己的服务器并调用 installGlobals(),则需要调用 installGlobals({ nativeFetch: true }) 才能使用 undici

    ¥If you are managing your own server and calling installGlobals(), you will need to call installGlobals({ nativeFetch: true }) to use undici.

    - installGlobals();
    + installGlobals({ nativeFetch: true });
    
  • 如果你使用 remix-serve,如果启用了单次抓取,它将自动使用 undici

    ¥If you are using remix-serve, it will use undici automatically if Single Fetch is enabled.

  • 如果你在 remix 项目中使用 Miniflare/Cloudflare Worker,请确保你的 兼容性标志 也设置为 2023-03-01 或更高版本。

    ¥If you are using Miniflare/Cloudflare worker with your remix project, ensure your compatibility flag is set to 2023-03-01 or later as well.

3.

启用“单次获取”后,即使需要运行多个加载器,客户端导航也只会发出一个请求。要处理调用的处理程序的合并标头,headers 导出现在也适用于 loader/action 数据请求。在许多情况下,你现有的文档请求逻辑应该足以满足新的单次获取数据请求的需求。

¥With Single Fetch enabled, there will now only be one request made on client-side navigations even when multiple loaders need to run. To handle merging headers for the handlers called, the headers export will now also apply to loader/action data requests. In many cases, the logic you already have in there for document requests should be close to sufficient for your new Single Fetch data requests.

-import { json } from "@remix-run/node";
+import { data } from "@remix-run/node";

// This example assumes you already have a headers function to handle header
// merging for your document requests
export function headers() {
  // ...
}

export async function loader({}: LoaderFunctionArgs) {
  let tasks = await fetchTasks();
-  return json(tasks, {
+  return data(tasks, {
    headers: {
      "Cache-Control": "public, max-age=604800"
    }
  });
}

⚠️ 这对于缓存行为的审查尤为重要。在单次获取 (Single Fetch) 之前,给定的 loader 可以选择自己的缓存时长,并且该缓存时长将应用于来自该 loader 的单个 HTTP 响应。但是文档请求会调用多个加载器,并且需要应用实现 headers 方法,以便智能地合并从多个路由的加载器返回的标头。使用单次获取时,文档和数据请求现在的行为相同,因此你的 headers 函数需要为所有路由返回正确的标头/缓存行为。

¥⚠️ This is especially important to review for caching behaviors. Prior to Single Fetch, a given loader could choose its own cache duration, and it would apply to the singular HTTP response from that loader. But document requests would call multiple loaders and required an application to implement the headers method to intelligently merge headers returned from loaders across multiple routes. With Single fetch, document and data requests now behave the same so your headers function needs to return the proper headers/caching behavior for all routes.

4.

如果你的 脚本的内容安全策略nonce-sources 都存在,则需要将 nonce 添加到两个位置以实现流式单次获取:

¥If you have a content security policy for scripts with nonce-sources, you will need to add that nonce to two places for the streaming Single Fetch implementation:

  • <RemixServer nonce={yourNonceValue}> - 这会将 nonce 添加到此组件渲染的内联脚本中,这些脚本用于处理客户端的流数据。

    ¥<RemixServer nonce={yourNonceValue}> - this will add the nonce to the inline scripts rendered by this component that handle the streaming data on the client side

  • 在你的 entry.server.tsx 文件中,将 options.nonce 参数设置为 renderToPipeableStream/renderToReadableStream。另请参阅 Remix Streaming 文档

    ¥In your entry.server.tsx in the options.nonce parameter to renderToPipeableStream/renderToReadableStream. See also the Remix Streaming docs

5.

对于大多数 Remix 应用来说,你不太可能使用 renderToString,但如果你选择在 entry.server.tsx 中使用它,请继续阅读,否则你可以跳过此步骤。

¥For most Remix apps it's unlikely you're using renderToString, but if you have opted into using it in your entry.server.tsx, then continue reading, otherwise you can skip this step.

为了保持文档和数据请求之间的一致性,turbo-stream 也用作初始文档请求中向下发送数据的格式。这意味着一旦启用单次抓取,你的应用将无法再使用 renderToString,并且必须在 entry.server.tsx 中使用 React 流式渲染器 API(例如 renderToPipeableStreamrenderToReadableStream)。

¥To maintain consistency between document and data requests, turbo-stream is also used as the format for sending down data in initial document requests. This means that once opted-into Single Fetch, your application can no longer use renderToString and must use a React streaming renderer API such as renderToPipeableStream or renderToReadableStream) in entry.server.tsx.

这并不意味着你必须流式传输 HTTP 响应,你仍然可以利用 renderToPipeableStream 中的 onAllReady 选项或 renderToReadableStream 中的 allReady promise一次性发送完整文档。

¥This does not mean you have to stream down your HTTP response, you can still send the full document at once by leveraging the onAllReady option in renderToPipeableStream, or the allReady promise in renderToReadableStream.

在客户端,这也意味着你需要将客户端的 hydrateRoot 调用封装在 startTransition 调用中,因为流数据将封装在 Suspense 边界中。

¥On the client side, this also means that your need to wrap your client-side hydrateRoot call in a startTransition call because the streamed data will be coming down wrapped in a Suspense boundary.

重大变更

¥Breaking Changes

Single Fetch 引入了一些重大变更 —— 其中一些需要在启用该标志时预先处理,而另一些则可以在启用该标志后逐步处理。在更新到下一个主要版本之前,你需要确保所有这些警告都已处理完毕。

¥There are a handful of breaking changes introduced with Single Fetch — some of which you need to handle up-front when you enable the flag, and some you can handle incrementally after enabling the flag. You will need to ensure all of these have been handled before updating to the next major version.

需要预先处理的更改:

¥Changes that need to be addressed up front:

  • 弃用的 fetch polyfill:旧的 installGlobals() polyfill 不适用于单次获取,你必须使用原生 Node 20 fetch API 或在自定义服务器中调用 installGlobals({ nativeFetch: true }) 来获取 基于 undici 的 polyfill

    ¥Deprecated fetch polyfill: The old installGlobals() polyfill doesn't work for Single Fetch, you must either use the native Node 20 fetch API or call installGlobals({ nativeFetch: true }) in your custom server to get the undici-based polyfill

  • headers 导出应用于数据请求:headers 函数现在将同时适用于文档和数据请求。

    ¥headers export applied to data requests: The headers function will now apply to both document and data requests

你可能需要处理以下可能需要后续处理的变更:

¥Changes to be aware of that you may need to handle over-time:

  • 新的流数据格式:单次抓取通过 turbo-stream 在底层使用了一种新的流式传输格式,这意味着我们可以传输比 JSON 更复杂的数据。

    ¥New streaming Data format: Single fetch uses a new streaming format under the hood via turbo-stream, which means that we can stream down more complex data than just JSON

  • 不再自动序列化:从 loaderaction 函数返回的裸对象不再自动转换为 JSON Response,而是在传输过程中按原样序列化。

    ¥No more auto-serialization: Naked objects returned from loader and action functions are no longer automatically converted into a JSON Response and are serialized as-is over the wire

  • 类型推断更新:为了获得最准确的类型推断,你应该将 augment Remix 的 Future 接口与 v3_singleFetch: true 结合使用。

    ¥Updates to type inference: To get the most accurate type inference, you should augment Remix's Future interface with v3_singleFetch: true

  • 默认重新验证行为更改为选择退出 GET 导航:常规导航的默认重新验证行为将从“选择加入”更改为“选择退出”,并且你的服务器加载器将默认重新运行。

    ¥Default revalidation behavior changes to opt-out on GET navigations: Default revalidation behavior on normal navigations changes from opt-in to opt-out, and your server loaders will re-run by default

  • 选择加入 action 重新验证action4xx/5xx Response 之后的重新验证现在是可选的,而不是可选的

    ¥Opt-in action revalidation: Revalidation after an action 4xx/5xx Response is now opt-in, versus opt-out

使用单次抓取添加新路由

¥Adding a New Route with Single Fetch

启用“单次获取”后,你可以继续编写利用更强大的流格式的路由。

¥With Single Fetch enabled, you can go ahead and author routes that take advantage of the more powerful streaming format.

要获得正确的类型推断,你需要将 augment Remix 的 Future 接口与 v3_singleFetch: true 结合使用。你可以在 类型推断部分 中阅读更多相关信息。

使用单次获取,你可以从加载器返回以下数据类型:BigIntDateErrorMapPromiseRegExpSetSymbolURL

¥With Single Fetch you can return the following data types from your loader: BigInt, Date, Error, Map, Promise, RegExp, Set, Symbol, and URL.

// routes/blog.$slug.tsx
import type { LoaderFunctionArgs } from "@remix-run/node";

export async function loader({
  params,
}: LoaderFunctionArgs) {
  const { slug } = params;

  const comments = fetchComments(slug);
  const blogData = await fetchBlogData(slug);

  return {
    content: blogData.content, // <- string
    published: blogData.date, // <- Date
    comments, // <- Promise
  };
}

export default function BlogPost() {
  const blogData = useLoaderData<typeof loader>();
  //    ^? { content: string, published: Date, comments: Promise }

  return (
    <>
      <Header published={blogData.date} />
      <BlogContent content={blogData.content} />
      <Suspense fallback={<CommentsSkeleton />}>
        <Await resolve={blogData.comments}>
          {(comments) => (
            <BlogComments comments={comments} />
          )}
        </Await>
      </Suspense>
    </>
  );
}

使用单次抓取迁移路由

¥Migrating a Route with Single Fetch

如果你当前从加载器(即 json/defer)返回 Response 实例,则无需对应用代码进行大量更改即可利用单次获取功能。

¥If you are currently returning Response instances from your loaders (i.e., json/defer) then you shouldn't need to make many changes to your app code to take advantage of Single Fetch.

不过,为了更好地准备将来升级到 React Router v7,我们建议你逐个路由地进行以下更改,因为这是验证更新标头和数据类型不会破坏任何内容的最简单方法。

¥However, to better prepare your upgrade to React Router v7 in the future, we recommend that you start making the following changes on a route-by-route basis, as that is the easiest way to validate that updating the headers and data types doesn't break anything.

类型推断

¥Type Inference

如果没有 Single Fetch,任何从 loaderaction 返回的纯 JavaScript 对象都会自动序列化为 JSON 响应(就像你通过 json 返回它一样)。类型推断会假设这种情况,并推断裸对象返回的结果就像 JSON 序列化一样。

¥Without Single Fetch, any plain JavaScript object returned from a loader or action is automatically serialized into a JSON response (as if you returned it via json). The type inference assumes this is the case and infers naked object returns as if they were JSON serialized.

使用单次获取时,裸对象将直接进行流式传输,因此一旦你选择单次获取,内置的类型推断将不再准确。例如,他们会假设 Date 会在客户端被序列化为字符串 😕。

¥With Single Fetch, naked objects will be streamed directly, so the built-in type inference is no longer accurate once you have opted-into Single Fetch. For example, they would assume that a Date would be serialized to a string on the client 😕.

启用单次抓取类型

¥Enable Single Fetch types

要切换到单次提取类型,你应该将 Remix 的 Future 接口与 v3_singleFetch: true 结合使用。你可以在 tsconfig.json > include 覆盖的任何文件中执行此操作。我们建议你在 vite.config.ts 中执行此操作,以便将其与 Remix 插件中的 future.v3_singleFetch 未来标志放在一起:

¥To switch over to Single Fetch types, you should augment Remix's Future interface with v3_singleFetch: true. You can do this in any file covered by your tsconfig.json > include. We recommend you do this in your vite.config.ts to keep it colocated with the future.v3_singleFetch future flag in the Remix plugin:

declare module "@remix-run/server-runtime" {
  // or cloudflare, deno, etc.
  interface Future {
    v3_singleFetch: true;
  }
}

现在 useLoaderDatauseActionData 以及任何其他使用 typeof loader 泛型的实用程序都应该使用单次获取类型:

¥Now useLoaderData, useActionData, and any other utilities that use a typeof loader generic should be using Single Fetch types:

import { useLoaderData } from "@remix-run/react";

export function loader() {
  return {
    planet: "world",
    date: new Date(),
  };
}

export default function Component() {
  const data = useLoaderData<typeof loader>();
  //    ^? { planet: string, date: Date }
}

函数和类实例

¥Functions and class instances

通常,函数无法通过网络可靠地发送,因此它们会被序列化为 undefined

¥In general, functions cannot be reliably sent over the network, so they get serialized as undefined:

import { useLoaderData } from "@remix-run/react";

export function loader() {
  return {
    planet: "world",
    date: new Date(),
    notSoRandom: () => 7,
  };
}

export default function Component() {
  const data = useLoaderData<typeof loader>();
  //    ^? { planet: string, date: Date, notSoRandom: undefined }
}

方法同样不可序列化,因此类实例会被精简为仅包含可序列化属性:

¥Methods are also not serializable, so class instances get slimmed down to just their serializable properties:

import { useLoaderData } from "@remix-run/react";

class Dog {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  bark() {
    console.log("woof");
  }
}

export function loader() {
  return {
    planet: "world",
    date: new Date(),
    spot: new Dog("Spot", 3),
  };
}

export default function Component() {
  const data = useLoaderData<typeof loader>();
  //    ^? { planet: string, date: Date, spot: { name: string, age: number, bark: undefined } }
}

clientLoaderclientAction

¥clientLoader and clientAction

确保包含 clientLoader 参数和 clientAction 参数的类型,因为这是我们的类型检测客户端数据函数的方式。

来自客户端加载器和操作的数据永远不会序列化,因此它们的类型会被保留:

¥Data from client-side loaders and actions are never serialized, so types for those are preserved:

import {
  useLoaderData,
  type ClientLoaderFunctionArgs,
} from "@remix-run/react";

class Dog {
  /* ... */
}

// Make sure to annotate the types for the args! 👇
export function clientLoader(_: ClientLoaderFunctionArgs) {
  return {
    planet: "world",
    date: new Date(),
    notSoRandom: () => 7,
    spot: new Dog("Spot", 3),
  };
}

export default function Component() {
  const data = useLoaderData<typeof clientLoader>();
  //    ^? { planet: string, date: Date, notSoRandom: () => number, spot: Dog }
}

标头

¥Headers

启用“单次获取”后,headers 函数现在可用于文档和数据请求。你应该使用该函数合并并行执行的加载器返回的任何标头,或者返回任何给定的 actionHeaders 组件。

¥The headers function is now used on both document and data requests when Single Fetch is enabled. You should use that function to merge any headers returned from loaders executed in parallel, or to return any given actionHeaders.

返回的响应

¥Returned Responses

使用单次获取时,你不再需要返回 Response 实例,而只需通过裸对象返回直接返回数据即可。因此,在使用单次获取时,json/defer 实用程序应被视为已弃用。它们将在 v2 版本中保留,因此你无需立即删除它们。它们很可能会在下一个主要版本中被移除,因此我们建议从现在开始逐步移除它们。

¥With Single Fetch, you no longer need to return Response instances and can just return your data directly via naked object returns. Therefore, the json/defer utilities should be considered deprecated when using Single Fetch. These will remain for the duration of v2, so you don't need to remove them immediately. They will likely be removed in the next major version, so we recommend removing them incrementally between now and then.

对于 v2,你仍然可以继续返回正常的 Response 实例,并且它们的 status/headers 将以与文档请求相同的方式生效(通过 headers() 函数合并标头)。

¥For v2, you may still continue returning normal Response instances and their status/headers will take effect the same way they do on document requests (merging headers via the headers() function).

随着时间的推移,你应该开始消除加载器和操作返回的响应。

¥Over time, you should start eliminating returned Responses from your loaders and actions.

  • 如果你的 loader/action 返回 json/defer 而未设置任何 status/headers,那么你可以删除对 json/defer 的调用并直接返回数据。

    ¥If your loader/action was returning json/defer without setting any status/headers, then you can remove the call to json/defer and return the data directly

  • 如果你的 loader/action 通过 json/defer 返回自定义 status/headers,你应该将它们切换为使用新的 data() 实用程序。

    ¥If your loader/action was returning custom status/headers via json/defer, you should switch those to use the new data() utility.

客户端加载器

¥Client Loaders

如果你的应用路由使用了 clientLoader 函数,请务必注意,单次获取 (Single Fetch) 的行为会略有变化。由于 clientLoader 旨在为你提供一种选择不调用服务器 loader 函数的方式。 - 单次获取调用执行该服务器加载程序是不正确的。但是我们并行运行所有加载器,我们不想等到知道哪些 clientLoader 真正请求服务器数据后才进行调用。

¥If your app has routes using clientLoader functions, it's important to note that the behavior of Single Fetch will change slightly. Because clientLoader is intended to give you a way to opt-out of calling the server loader function - it would be incorrect for the Single Fetch call to execute that server loader. But we run all loaders in parallel, and we don't want to wait to make the call until we know which clientLoader's are actually asking for server data.

例如,考虑以下 /a/b/c 路由:

¥For example, consider the following /a/b/c routes:

// routes/a.tsx
export function loader() {
  return { data: "A" };
}

// routes/a.b.tsx
export function loader() {
  return { data: "B" };
}

// routes/a.b.c.tsx
export function loader() {
  return { data: "C" };
}

export function clientLoader({ serverLoader }) {
  await doSomeStuff();
  const data = await serverLoader();
  return { data };
}

如果用户从 / -> /a/b/c 导航,那么我们需要运行 ab 的服务器加载器,以及 cclientLoader 加载器。 - 最终可能会(也可能不会)调用其自己的服务器 loader。当我们想要获取 a/b loader 时,我们不能决定将 c 服务器 loader 包含在单个 fetch 调用中,也不能延迟到 c 实际进行 serverLoader 调用(或返回),否则会引入瀑布。

¥If a user navigates from / -> /a/b/c, then we need to run the server loaders for a and b, and the clientLoader for c - which may eventually (or may not) call its own server loader. We can't decide to include the c server loader in a single fetch call when we want to fetch the a/b loader's, nor can we delay until c actually makes the serverLoader call (or returns) without introducing a waterfall.

因此,当你导出一个路由选择退出单次获取的 clientLoader 时,当你调用 serverLoader 时,它将进行单次获取以仅获取其路由服务器 loader。所有未导出 clientLoader 的路由都将在单个 HTTP 请求中获取。

¥Therefore, when you export a clientLoader that route opts-out of Single Fetch and when you call serverLoader it will make a single fetch to get only it's route server loader. All routes that do not export a clientLoader will be fetched in a singular HTTP request.

所以,在上面的路由设置中,从 / -> /a/b/c 导航将导致路由 ab 预先进行一次单一的获取调用:

¥So, on the above route set up a navigation from / -> /a/b/c will result in a singular single-fetch call up front for routes a and b:

GET /a/b/c.data?_routes=routes/a,routes/b

然后,当 c 调用 serverLoader 时,它将仅对 c 服务器 loader 进行自己的调用:

¥And then when c calls serverLoader, it'll make its own call for just the c server loader:

GET /a/b/c.data?_routes=routes/c

资源路由

¥Resource Routes

由于 Single Fetch 使用了新的 流格式,从 loaderaction 函数返回的原始 JavaScript 对象不再通过 json() 实用程序自动转换为 Response 实例。在导航数据加载中,它们会与其他加载器数据合并,并在 turbo-stream 响应中向下传输。

¥Because of the new streaming format used by Single Fetch, raw JavaScript objects returned from loader and action functions are no longer automatically converted to Response instances via the json() utility. Instead, in navigational data loads they're combined with the other loader data and streamed down in a turbo-stream response.

这给 资源路由 带来了一个有趣的难题,因为它们是独一无二的,旨在单独调用 - 而不是总是通过 Remix API。它们也可以通过任何其他 HTTP 客户端(fetchcURL 等)访问。

¥This poses an interesting conundrum for resource routes which are unique because they're intended to be hit individually — and not always via Remix APIs. They can also be accessed via any other HTTP client (fetch, cURL, etc.).

如果资源路由旨在供内部 Remix API 使用,我们希望能够利用 turbo-stream 编码来解锁向下传输更复杂结构(例如 DatePromise 实例)的能力。不过,当从外部访问时,我们可能更倾向于返回更易于使用的 JSON 结构。因此,如果在 v2 中返回原始对象,则行为会略显模糊。 - 应该通过 turbo-stream 还是 json() 序列化?

¥If a resource route is intended for consumption by internal Remix APIs, we want to be able to leverage the turbo-stream encoding to unlock the ability to stream down more complex structures such as Date and Promise instances. However, when accessed externally, we'd probably prefer to return the more easily consumable JSON structure. Thus, the behavior is slightly ambiguous if you return a raw object in v2 - should it be serialized via turbo-stream or json()?

为了简化向后兼容性并简化未来 Single Fetch 标志的采用,Remix v2 将根据访问方式(无论是通过 Remix API 还是外部访问)来处理此问题。如果你不希望原始对象被流式传输以供外部使用,Remix 将来会要求你返回自己的 JSON 响应

¥To ease backwards-compatibility and ease the adoption of the Single Fetch future flag, Remix v2 will handle this based on whether it's accessed from a Remix API or externally. In the future Remix will require you to return your own JSON response if you do not want raw objects to be streamed down for external consumption.

启用“单一获取”后,Remix v2 的行为如下:

¥The Remix v2 behavior with Single Fetch enabled is as follows:

  • 当通过 Remix API(例如 useFetcher)访问时,原始 JavaScript 对象将作为 turbo-stream 响应返回,就像普通的加载器和操作一样(这是因为 useFetcher 会在请求后附加 .data 后缀)。

    ¥When accessing from a Remix API such as useFetcher, raw JavaScript objects will be returned as turbo-stream responses, just like normal loaders and actions (this is because useFetcher will append the .data suffix to the request)

  • 当通过外部工具(例如 fetchcURL)访问时,为了在 v2 中实现向后兼容,我们将继续自动转换为 json()

    ¥When accessing from an external tool such as fetch or cURL, we will continue this automatic conversion to json() for backwards-compatibility in v2:

    • 遇到这种情况时,Remix 将记录弃用警告。

      ¥Remix will log a deprecation warning when this situation is encountered

    • 你可以根据需要更新受影响的资源路由处理程序以返回 Response 对象。

      ¥At your convenience, you can update impacted resource route handlers to return a Response object

    • 解决这些弃用警告将使你更好地为最终的 Remix v3 升级做好准备。

      ¥Addressing these deprecation warnings will better prepare you for the eventual Remix v3 upgrade

    export function loader() {
      return {
        message: "My externally-accessed resource route",
      };
    }
    
    export function loader() {
      return Response.json({
        message: "My externally-accessed resource route",
      });
    }
    

其他详细信息

¥Additional Details

流式传输数据格式

¥Streaming Data Format

以前,Remix 使用 JSON.stringify 通过网络序列化加载器/操作数据,并且需要实现自定义流格式来支持 defer 响应。

¥Previously, Remix used JSON.stringify to serialize your loader/action data over the wire, and needed to implement a custom streaming format to support defer responses.

使用单次获取,Remix 现在在底层使用 turbo-stream,它为流式传输提供一流的支持,并允许你自动序列化/反序列化比 JSON 更复杂的数据。以下数据类型可以直接通过 turbo-stream 进行流式传输:BigIntDateErrorMapPromiseRegExpSetSymbolURLError 的子类型也受支持,只要它们在客户端具有全局可用的构造函数(SyntaxErrorTypeError 等)。

¥With Single Fetch, Remix now uses turbo-stream under the hood which provides first-class support for streaming and allows you to automatically serialize/deserialize more complex data than JSON. The following data types can be streamed down directly via turbo-stream: BigInt, Date, Error, Map, Promise, RegExp, Set, Symbol, and URL. Subtypes of Error are also supported as long as they have a globally available constructor on the client (SyntaxError, TypeError, etc.).

启用单次抓取后,可能需要或不需要立即修改代码:

¥This may or may not require any immediate changes to your code once enabling Single Fetch:

  • ✅ 从 loader/action 函数返回的 json 响应仍将通过 JSON.stringify 进行序列化,因此如果返回 Date,则会从 useLoaderData/useActionData 收到 string

    ¥✅ json responses returned from loader/action functions will still be serialized via JSON.stringify so if you return a Date, you'll receive a string from useLoaderData/useActionData

  • ⚠️ 如果你返回的是 defer 实例或裸对象,它现在将通过 turbo-stream 进行序列化,因此,如果你返回 Date,你将从 useLoaderData/useActionData 收到 Date

    ¥⚠️ If you're returning a defer instance or a naked object, it will now be serialized via turbo-stream, so if you return a Date, you'll receive a Date from useLoaderData/useActionData

    • 如果你希望保留当前行为(不包括流式 defer 响应),你可以将任何现有的裸对象返回值封装在 json 中。

      ¥If you wish to maintain current behavior (excluding streaming defer responses), you may wrap any existing naked object returns in json

这也意味着你不再需要使用 defer 实用程序通过网络发送 Promise 实例!你可以在裸对象的任何位置包含 Promise,并在 useLoaderData().whatever 上获取它。如果需要,你还可以嵌套 Promise - 但要注意潜在的用户体验影响。

¥This also means that you no longer need to use the defer utility to send Promise instances over the wire! You can include a Promise anywhere in a naked object and pick it up on useLoaderData().whatever. You can also nest Promise's if needed - but beware of potential UX implications.

一旦采用单次获取 (Single Fetch),建议你在整个应用中逐步移除 json/defer 的使用,转而返回原始对象。

¥Once adopting Single Fetch, it is recommended that you incrementally remove the usage of json/defer throughout your application in favor of returning raw objects.

流式传输超时

¥Streaming Timeout

以前,Remix 在默认 entry.server.tsx 文件中内置了 ABORT_TIMEOUT 的概念,它会终止 React 渲染器,但它不会执行任何特定操作来清理任何待处理的延迟promise。

¥Previously, Remix has a concept of an ABORT_TIMEOUT built-into the default entry.server.tsx files which would terminate the React renderer, but it didn't do anything in particular to clean up any pending deferred promises.

现在 Remix 正在内部流式传输,我们可以取消 turbo-stream 处理并自动拒绝任何待处理的 Promise,并将这些错误流式传输到客户端。默认情况下,这会在 4950 毫秒后发生 - 这个值略低于大多数 entry.server.tsx 文件中当前 ABORT_DELAY 的 5000 毫秒。 - 因为我们需要取消 Promise,并让拒绝信息在 React 渲染器中流式传输,然后再中止 React 端的操作。

¥Now that Remix is streaming internally, we can cancel the turbo-stream processing and automatically reject any pending promises and stream up those errors to the client. By default, this happens after 4950 ms — a value that was chosen to be just under the current 5000ms ABORT_DELAY in most entry.server.tsx files - since we need to cancel the promises and let the rejections stream up through the React renderer before aborting the React side of things.

你可以通过从 entry.server.tsx 导出 streamTimeout 数值来控制这一点,Remix 将使用该值作为毫秒数,在此之后拒绝 loader/action 中任何未完成的 Promise。建议将此值与中止 React 渲染器的超时时间分离。 - 并且你应该始终将 React 超时设置为更高的值,以便它有时间从 streamTimeout 中流式传输底层拒绝。

¥You can control this by exporting a streamTimeout numeric value from your entry.server.tsx and Remix will use that as the number of milliseconds after which to reject any outstanding Promises from loader/action's. It's recommended to decouple this value from the timeout in which you abort the React renderer - and you should always set the React timeout to a higher value so it has time to stream down the underlying rejections from your streamTimeout.

// Reject all pending promises from handler functions after 5 seconds
export const streamTimeout = 5000;

// ...

function handleBrowserRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  return new Promise((resolve, reject) => {
    const { pipe, abort } = renderToPipeableStream(
      <RemixServer
        context={remixContext}
        url={request.url}
        abortDelay={ABORT_DELAY}
      />,
      {
        onShellReady() {
          /* ... */
        },
        onShellError(error: unknown) {
          /* ... */
        },
        onError(error: unknown) {
          /* ... */
        },
      }
    );

    // Automatically timeout the react renderer after 10 seconds
    setTimeout(abort, 10000);
  });
}

重新验证

¥Revalidations

正常导航行为

¥Normal Navigation Behavior

除了更简单的思维模型以及文档和数据请求的一致性之外,单次获取的另一个好处是更简单(并且希望更好)的缓存行为。通常,与之前的多次提取行为相比,单次提取会发出更少的 HTTP 请求,并且有望更频繁地缓存这些结果。

¥In addition to the simpler mental model and the alignment of document and data requests, another benefit of Single Fetch is simpler (and hopefully better) caching behavior. Generally, Single Fetch will make fewer HTTP requests and hopefully cache those results more frequently compared to the previous multiple-fetch behavior.

为了减少缓存碎片,单次获取会更改 GET 导航的默认重新验证行为。之前,除非你通过 shouldRevalidate 选择加入,否则 Remix 不会重新运行已重用祖级路由的加载器。现在,对于像 GET /a/b/c.data 这样的简单单次获取请求,Remix 会默认重新运行这些消息。如果你没有任何 shouldRevalidateclientLoader 函数,你的应用将采用以下行为。

¥To reduce cache fragmentation, Single Fetch changes the default revalidation behavior on GET navigations. Previously, Remix would not re-run loaders for reused ancestor routes unless you opted in via shouldRevalidate. Now, Remix will re-run those by default in the simple case for a Single Fetch request like GET /a/b/c.data. If you do not have any shouldRevalidate or clientLoader functions, this will be the behavior for your app.

向任何活动路由添加 shouldRevalidateclientLoader 都会触发精细的单次获取调用,其中包含一个 _routes 参数,用于指定要运行的路由子集。

¥Adding either a shouldRevalidate or a clientLoader to any of the active routes will trigger granular Single Fetch calls that include a _routes parameter specifying the subset of routes to run.

如果 clientLoader 在内部调用 serverLoader(),则会触发针对该特定路由的单独 HTTP 调用,类似于旧行为。

¥If a clientLoader calls serverLoader() internally, that will trigger a separate HTTP call for that specific route, akin to the old behavior.

例如,如果你在 /a/b 上导航到 /a/b/c

¥For example, if you are on /a/b and you navigate to /a/b/c:

  • 当不存在 shouldRevalidateclientLoader 函数时:GET /a/b/c.data

    ¥When no shouldRevalidate or clientLoader functions exist: GET /a/b/c.data

  • 如果所有路由都有加载器,但 routes/a 通过 shouldRevalidate 选择退出:

    ¥If all routes have loaders but routes/a opts out via shouldRevalidate:

    • GET /a/b/c.data?_routes=root,routes/b,routes/c
  • 如果所有路由都有加载器,但 routes/bclientLoader

    ¥If all routes have loaders but routes/b has a clientLoader:

    • GET /a/b/c.data?_routes=root,routes/a,routes/c

    • 然后,如果 B 的 clientLoader 调用 serverLoader()

      ¥And then if B's clientLoader calls serverLoader():

      • GET /a/b/c.data?_routes=routes/b

如果这种新行为对你的应用来说不是最优的,你可以通过在父路由中添加一个在所需情况下返回 falseshouldRevalidate 来选择恢复不重新验证的旧行为。

¥If this new behavior is suboptimal for your application, you should be able to opt-back into the old behavior of not-revalidating by adding a shouldRevalidate that returns false in the desired scenarios to your parent routes.

另一种选择是利用服务器端缓存来进行昂贵的父加载器计算。

¥Another option is to leverage a server-side cache for expensive parent loader calculations.

提交重新验证行为

¥Submission Revalidation Behavior

以前,Remix 在任何操作提交后都会重新验证所有活动的加载器,无论操作的结果如何。你可以通过 shouldRevalidate 为每个路由选择退出重新验证。

¥Previously, Remix would always revalidate all active loaders after any action submission, regardless of the result of the action. You could opt out of revalidation on a per-route basis via shouldRevalidate.

使用单次获取时,如果 action 返回或抛出带有 4xx/5xx 状态码的 Response 错误,Remix 将默认不会重新验证加载器。如果 action 返回或抛出任何非 4xx/5xx 响应,则重新验证行为保持不变。这里的原因是,在大多数情况下,如果返回 4xx/5xx Response,则表示你没有修改任何数据,因此无需重新加载数据。

¥With Single Fetch, if an action returns or throws a Response with a 4xx/5xx status code, Remix will not revalidate loaders by default. If an action returns or throws anything that is not a 4xx/5xx Response, then the revalidation behavior is unchanged. The reasoning here is that in most cases, if you return a 4xx/5xx Response, you didn't mutate any data so there is no need to reload data.

如果你想在 4xx/5xx 操作响应后继续重新验证一个或多个加载器,你可以通过从 shouldRevalidate 函数返回 true 来选择在每个路由上进行重新验证。此外,函数中还新增了 actionStatus 参数,如果需要根据操作状态码进行判断,可以使用该参数。

¥If you want to continue revalidating one or more loaders after a 4xx/5xx action response, you can opt-into revalidation on a per-route basis by returning true from your shouldRevalidate function. There is also a new actionStatus parameter passed to the function that you can use if you need to decide based on the action status code.

重新验证通过单个 fetch HTTP 调用中的 ?_routes 查询字符串参数进行处理,这限制了被调用的加载器。这意味着当你进行细粒度的重新验证时,你将根据所请求的路由进行缓存枚举 - 但所有信息都在 URL 中,因此你不需要任何特殊的 CDN 配置(与通过自定义标头完成此操作相比,这需要你的 CDN 遵守 Vary 标头)。

¥Revalidation is handled via a ?_routes query string parameter on the single fetch HTTP call which limits the loaders being called. This means that when you are doing fine-grained revalidation, you will have cache enumerations based on the routes being requested — but all the information is in the URL so you should not need any special CDN configurations (as opposed to if this was done via a custom header that required your CDN to respect the Vary header).

Remix v2.17 中文网 - 粤ICP备13048890号