React Router v7 已发布。 查看文档
流式传输
本页内容

流式传输

流式传输允许您在内容可用时立即交付内容,而不是等待页面的全部内容准备就绪,从而增强用户体验。

请确保您的托管服务提供商支持流式传输,并非所有提供商都支持。如果您的响应似乎没有流式传输,这可能是原因。

步骤

流式传输数据有三个步骤

  1. 项目设置:我们需要确保我们的客户端和服务器入口点已设置为支持流式传输
  2. 组件设置:我们需要确保我们的组件可以渲染流式数据
  3. 延迟加载器数据:最后,我们可以在加载器中延迟数据

1. 项目设置

开箱即用:使用启动模板创建的 Remix 应用程序已预先配置为支持流式传输。

需要手动设置吗?如果您的项目是从头开始或使用较旧的模板开始的,请验证 entry.server.tsxentry.client.tsx 是否支持流式传输。如果您没有看到这些文件,则您正在使用默认值并且支持流式传输。如果您创建了自己的入口点,以下是供您参考的模板默认值

2. 组件设置

没有流式传输的路由模块可能如下所示

import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";

export async function loader({
  params,
}: LoaderFunctionArgs) {
  const [product, reviews] = await Promise.all([
    db.getProduct(params.productId),
    db.getReviews(params.productId),
  ]);

  return json({ product, reviews });
}

export default function Product() {
  const { product, reviews } =
    useLoaderData<typeof loader>();
  return (
    <>
      <ProductPage data={product} />
      <ProductReviews data={reviews} />
    </>
  );
}

为了渲染流式数据,您需要使用来自 React 的 <Suspense> 和来自 Remix 的 <Await>。这有点样板代码,但很简单

import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { Await, useLoaderData } from "@remix-run/react";
import { Suspense } from "react";

import { ReviewsSkeleton } from "./reviews-skeleton";

export async function loader({
  params,
}: LoaderFunctionArgs) {
  // existing code
}

export default function Product() {
  const { product, reviews } =
    useLoaderData<typeof loader>();
  return (
    <>
      <ProductPage data={product} />
      <Suspense fallback={<ReviewsSkeleton />}>
        <Await resolve={reviews}>
          {(reviews) => <ProductReviews data={reviews} />}
        </Await>
      </Suspense>
    </>
  );
}

即使在我们开始延迟数据之前,此代码也将继续工作。最好先完成组件代码。如果您遇到问题,则更容易追踪问题所在。

3. 在加载器中延迟数据

现在我们的项目和路由组件都设置为流式传输数据,我们可以开始在加载器中延迟数据。我们将使用 Remix 的 defer 工具来执行此操作。

请注意异步 Promise 代码的变化。

import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { defer } from "@remix-run/node"; // or cloudflare/deno
import { Await, useLoaderData } from "@remix-run/react";
import { Suspense } from "react";

import { ReviewsSkeleton } from "./reviews-skeleton";

export async function loader({
  params,
}: LoaderFunctionArgs) {
  // 👇 note this promise is not awaited
  const reviewsPromise = db.getReviews(params.productId);
  // 👇 but this one is
  const product = await db.getProduct(params.productId);

  return defer({
    product,
    reviews: reviewsPromise,
  });
}

export default function Product() {
  const { product, reviews } =
    useLoaderData<typeof loader>();
  // existing code
}

我们不是等待 reviews promise,而是将其传递给 defer。这告诉 Remix 通过网络将该 Promise 流式传输到浏览器。

就是这样!您现在应该正在将数据流式传输到浏览器。

避免低效的流式传输

重要的是在等待任何其他 Promise 之前,启动延迟数据的 Promise,否则您将无法获得流式传输的全部好处。请注意此效率较低的代码示例的区别

export async function loader({
  params,
}: LoaderFunctionArgs) {
  const product = await db.getProduct(params.productId);
  // 👇 this won't initiate loading until `product` is done
  const reviewsPromise = db.getReviews(params.productId);

  return defer({
    product,
    reviews: reviewsPromise,
  });
}

处理服务器超时

当使用 defer 进行流式传输时,您可以告诉 Remix 等待延迟数据解析的时间,在通过 entry.server.tsx 文件中的 <RemixServer abortDelay> 属性(默认为 5 秒)超时之前。如果您当前没有 entry.server.tsx 文件,您可以通过 npx remix reveal entry.server 暴露它。您也可以通过 setTimeout 使用此值来中止 React renderToPipeableStream 方法。

const ABORT_DELAY = 5_000;

// ...

const { pipe, abort } = renderToPipeableStream(
  <RemixServer
    context={remixContext}
    url={request.url}
    abortDelay={ABORT_DELAY}
  />
  // ...
);

// ...

setTimeout(abort, ABORT_DELAY);

使用内容安全策略进行流式传输

流式传输的工作原理是在延迟的 Promise 解析时将脚本标签插入到 DOM 中。如果您的页面包含 针对脚本的内容安全策略,则您需要通过在 Content-Security-Policy 标头中包含 script-src 'self' 'unsafe-inline' 来削弱安全策略,或者向所有脚本标签添加 nonce。

如果您使用 nonce,则需要在三个地方包含它

  • Content-Security-Policy 标头,如下所示:Content-Security-Policy: script-src 'nonce-secretnoncevalue'
  • <Scripts /><ScrollRestoration /><LiveReload /> 组件,如下所示:<Scripts nonce="secretnoncevalue" />
  • 在您调用 renderToPipeableStreamentry.server.ts 中,如下所示
const { pipe, abort } = renderToPipeableStream(
  <RemixServer
    context={remixContext}
    url={request.url}
    abortDelay={ABORT_DELAY}
  />,
  {
    nonce: "secretnoncevalue",
    /* ...remaining fields */
  }
);

这将确保 nonce 值包含在任何延迟的 script 标签中。

文档和示例在以下许可下授权 MIT