流式传输
本页内容

流式传输

流式传输允许您通过在内容可用时立即提供内容来增强用户体验,而不是等待页面全部内容准备就绪。

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

步骤

流式传输数据有三个步骤

  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 进行流式传输时,您可以通过在 entry.server.tsx 文件中的 <RemixServer abortDelay> 属性(默认值为 5 秒)来告诉 Remix 等待延迟数据解析的时间,然后再超时。 如果您目前没有 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);

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

流式传输通过将脚本标签插入 DOM 作为延迟的 promise 解析来工作。 如果您的页面包含一个 脚本的内容安全策略,您将需要要么通过在您的 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 值包含在任何延迟的脚本标签中。

Docs and examples licensed under MIT