流式传输允许您在内容可用时立即交付内容,而不是等待页面的全部内容准备就绪,从而增强用户体验。
请确保您的托管服务提供商支持流式传输,并非所有提供商都支持。如果您的响应似乎没有流式传输,这可能是原因。
流式传输数据有三个步骤
开箱即用:使用启动模板创建的 Remix 应用程序已预先配置为支持流式传输。
需要手动设置吗?如果您的项目是从头开始或使用较旧的模板开始的,请验证 entry.server.tsx
和 entry.client.tsx
是否支持流式传输。如果您没有看到这些文件,则您正在使用默认值并且支持流式传输。如果您创建了自己的入口点,以下是供您参考的模板默认值
没有流式传输的路由模块可能如下所示
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>
</>
);
}
即使在我们开始延迟数据之前,此代码也将继续工作。最好先完成组件代码。如果您遇到问题,则更容易追踪问题所在。
现在我们的项目和路由组件都设置为流式传输数据,我们可以开始在加载器中延迟数据。我们将使用 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" />
renderToPipeableStream
的 entry.server.ts
中,如下所示const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
nonce: "secretnoncevalue",
/* ...remaining fields */
}
);
这将确保 nonce 值包含在任何延迟的 script 标签中。