流式传输
On this page

流式传输

¥Streaming

Streaming 允许你通过在内容可用时立即交付内容来增强用户体验,而无需等待页面所有内容准备就绪。

¥Streaming allows you to enhance user experience by delivering content as soon as it's available, rather than waiting for the entire content of a page to be ready.

确保你的托管服务提供商支持流式传输;并非所有 页面都这样做。如果你的响应似乎无法流畅运行,这可能是原因所在。

¥Ensure your hosting provider supports streaming; not all of them do. If your responses don't seem to stream, this might be the cause.

步骤

¥Steps

流式传输数据分为三个步骤:

¥There are three steps to streaming data:

  1. 项目设置:我们需要确保我们的客户端和服务器入口点已设置为支持流数据。

    ¥Project Setup: we need to make sure our client and server entry points are set up to support streaming

  2. 组件设置:我们需要确保我们的组件能够渲染流数据。

    ¥Component Setup: we need to make sure our components can render streamed data

  3. 延迟加载器数据:最后,我们可以在加载器中延迟数据。

    ¥Deferring Loader Data: finally we can defer data in our loaders

1. 项目设置

¥ Project Setup

从开始就绪:使用入门模板创建的 Remix 应用已预先配置为支持流式传输。

¥Ready from Start: Remix apps created using starter templates are pre-configured for streaming.

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

¥Manual Setup Needed?: If your project began from scratch or used an older template, verify entry.server.tsx and entry.client.tsx have streaming support. If you don't see these files, then you are using the defaults and streaming is supported. If you have created your own entries, the following are the template defaults for your reference:

2. 组件设置

¥ Component Setup

不带流的路由模块可能如下所示:

¥A route module without streaming might look like this:

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>。这有点像样板代码,但很简单:

¥To render streamed data, you need to use <Suspense> from React and <Await> from Remix. It's a bit of boilerplate, but straightforward:

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>
    </>
  );
}

即使在我们开始延迟数据之前,此代码仍将继续有效。最好先编写组件代码。如果你遇到问题,可以更轻松地追踪问题所在。

¥This code will continue to work even before we start deferring data. It's a good idea to do the component code first. If you run into issues, it's easier to track down where the problem lies.

3. 在 Loader 中延迟数据加载

¥ Deferring Data in Loaders

现在我们的项目和路由组件已设置流数据,我们可以开始在加载器中延迟数据了。我们将使用 Remix 的 defer 实用程序来实现这一点。

¥Now that our project and route component are set up stream data, we can start deferring data in our loaders. We'll use the defer utility from Remix to do this.

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

¥Note the change in the async promise code.

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通过网络传输到浏览器。

¥Instead of awaiting the reviews promise, we pass it to defer. This tells Remix to stream that promise over the network to the browser.

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

¥That's it! You should now be streaming data to the browser.

避免低效流式传输

¥Avoid Inefficient Streaming

务必在等待任何其他promise之前,先启动延迟数据的promise,否则你将无法充分利用流式传输的优势。请注意与以下效率较低的代码示例的区别:

¥It's important to initiate promises for deferred data before you await any other promises, otherwise you won't get the full benefit of streaming. Note the difference with this less efficient code example:

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,
  });
}

服务器超时处理

¥Handling Server Timeouts

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

¥When using defer for streaming, you can tell Remix how long to wait for deferred data to resolve before timing out via the <RemixServer abortDelay> prop (which defaults to 5 seconds) in your entry.server.tsx file. If you don't currently have an entry.server.tsx file you can expose it via npx remix reveal entry.server. You can also use this value to abort the React renderToPipeableStream method via a setTimeout.

const ABORT_DELAY = 5_000;

// ...

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

// ...

setTimeout(abort, ABORT_DELAY);

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

¥Streaming with a Content Security Policy

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

¥Streaming works by inserting script tags into the DOM as deferred promises resolve. If your page includes a Content Security Policy for scripts, you'll either need to weaken your security policy by including script-src 'self' 'unsafe-inline' in your Content-Security-Policy header, or add nonces to all of your script tags.

如果你使用的是 nonce,则需要在三个地方引入它:

¥If you are using a nonce, it needs to be included in three places:

  • Content-Security-Policy 标头如下所示:Content-Security-Policy: script-src 'nonce-secretnoncevalue'

    ¥The Content-Security-Policy header, like so: Content-Security-Policy: script-src 'nonce-secretnoncevalue'

  • <Scripts /><ScrollRestoration /><LiveReload /> 组件,如下所示:<Scripts nonce="secretnoncevalue" />

    ¥The <Scripts />, <ScrollRestoration /> and <LiveReload /> components, like so: <Scripts nonce="secretnoncevalue" />

  • entry.server.ts 中,你可以像这样调用 renderToPipeableStream

    ¥In entry.server.ts where you call renderToPipeableStream, like so:

const { pipe, abort } = renderToPipeableStream(
  <RemixServer
    context={remixContext}
    url={request.url}
    abortDelay={ABORT_DELAY}
  />,
  {
    nonce: "secretnoncevalue",
    /* ...remaining fields */
  }
);

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

¥This will ensure the nonce value is included on any deferred script tags.

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