SSR in React

API

hydrateRoot (opens in a new tab)

将已经在服务端生成好的(通过 react-dom/server (opens in a new tab))HTML 代码注水,让 HTML 内容成为 React Component(React 来接管页面内容和交互)

const root = hydrateRoot(domNode, reactNode, options?)

Returns

hydrateRoot returns an object with two methods: render (opens in a new tab) and unmount. (opens in a new tab)

  • root.render to update a React component inside a hydrated React root for a browser DOM element.
  • root.unmount to destroy a rendered tree inside a React root. 通常完整的 React App 不需要

Pitfall

要保证水合后的 HTML 服务端生成的内容一致,不然会让用户看到的内容发生闪动。React 会恢复这些错误,但务必需要自己修复。

可以通过渲染不同的客户端/服务端内容,下面这个组件会正常 match 服务端内容,但是会在第二次 render 后渲染成 Is Client。渲染会变慢,而且会有闪动。

import { useState, useEffect } from "react";
 
export default function App() {
  const [isClient, setIsClient] = useState(false);
 
  useEffect(() => {
    setIsClient(true);
  }, []);
 
  return <h1>{isClient ? "Is Client" : "Is Server"}</h1>;
}

注意 useEffect 在 SSR 的时候不会执行

useEffect 在 SSR 的时候为什么不执行 (opens in a new tab)

useEffect 只会在 mount/update 之后才会执行(页面上渲染

Your understanding is correct. useEffect happens after mount/update, but the server doesn’t mount so it doesn’t happen.

— Kent C. Dodds (@kentcdodds) February 26, 2021 (opens in a new tab)

“it [useEffect] won’t run on the server, but it also won’t warn.”

— Hugo (@hugo) February 26, 2021

renderToPipeableStream (opens in a new tab)

Streaming HTML

将 React 组件树渲染成 pipeable Node.js Stream

Node 环境专属 API,其他的环境使用 Web Stream 比如 Deno 或者其他 runtimes 需要用 renderToReadableStream。

const { pipe, abort } = renderToPipeableStream(reactNode, options?)

Options 参数

  • bootstrapScripts:string 数组,里面是 url,会作为 <script> 标签输出到页面上,在脚本中调用 hydrateRoot,如果不传,就不会在客户端执行 React(纯 Server 渲染的静态页面)
  • identifierPrefix:string,给 useId 用的 ID 前缀,可用来标示 App 的唯一性
  • onAllReady:当所有渲染都完成会触发的回调,包含 shell 和 content
  • onError:当服务发生异常会触发
  • onShellReady:当初始的 shell 部分内容渲染完成后触发的回调
  • onShellError:shell 渲染异常的回调,不会有字节开始 stream,onShellReady 和 onAllReady 也不会触发,需要返回一个 fallback HTML

返回:两个方法

  • pipe:将 HTML 输出成 Writable Node.js Stream (opens in a new tab),当 onShellReady 的时候就可以开始让服务开始 streaming 了,或者在 onAllReady 的时候开始(为了爬虫/静态生成)
  • abort:可以终端服务端渲染,让客户端渲染剩余部分

shell & content & streaming

因为这个 API 主打流式渲染,能够让客户端更早的渲染出画面,依赖数据的部分可以等到数据返回了再流式传输到客户端渲染。

所以我们通过 <Suspense> 组件划分静态骨架部分和依赖数据的部分,前者就称为 shell,后者称为 content,shell 无需数据渲染完成后即可开始传输(onShellReady)

具体的一些用法可以看官网。

SSR 最好还是结合框架去使用,不推荐自己手写框架。

const { pipe } = renderToPipeableStream(<App />, {
  bootstrapScripts: ["/main.js"],
  onShellReady() {
    response.statusCode = getStatusCode();
    response.setHeader("content-type", "text/html");
    pipe(response);
  },
  onShellError(error) {
    response.statusCode = getStatusCode();
    response.setHeader("content-type", "text/html");
    response.send("<h1>Something went wrong</h1>");
  },
  onError(error) {
    didError = true;
    caughtError = error;
    console.error(error);
    logServerCrashReport(error);
  },
});

renderToReadableStream (opens in a new tab)

Streaming HTML

renders a React tree to a Readable Web Stream. (opens in a new tab)

是 Web Stream,所以是用在 Web Workder 中或者 Deno 的