写在前面

React 提供的 SSR API 分为两部分,一部分面向服务端(react-dom/server),另一部分仍在客户端执行(react-dom

<img src="http://cdn.ayqy.net/data/home/qxu1001840309/htdocs/cms/wordpress/wp-content/uploads/2020/11/1_VG33xLBOqcpfctgiyh0jtA.png" alt="react ssr" width="625" height="446" class="size-large wp-image-2317" />

一.ReactDOMServer

ReactDOMServer相关 API 能够在服务端将 React 组件渲染成静态的(HTML)标签

The ReactDOMServer object enables you to render components to static markup.

把组件树渲染成对应 HTML 标签的工作在浏览器环境也能完成,因此,面向服务端的 React DOM API 也分为两类:

renderToString

ReactDOMServer.renderToString(element)

最基础的 SSR API,输入 React 组件(准确来说是ReactElement),输出 HTML 字符串。之后由客户端 hydrate API 对服务端返回的视图结构附加上交互行为,完成页面渲染:

If you call ReactDOM.hydrate() on a node that already has this server-rendered markup, React will preserve it and only attach event handlers.

renderToStaticMarkup

ReactDOMServer.renderToStaticMarkup(element)

renderToString类似,区别在于 API 设计上,renderToStaticMarkup只用于纯展示(没有事件交互,不需要 hydrate)的场景

This is useful if you want to use React as a simple static page generator, as stripping away the extra attributes can save some bytes. If you plan to use React on the client to make the markup interactive, do not use this method. Instead, use renderToString on the server and ReactDOM.hydrate() on the client.

因此renderToStaticMarkup只生成干净的 HTML,不带额外的 DOM 属性(如data-reactroot),响应体积上有些微的优势

之所以说体积优势些微,是因为在 React 16 之前,SSR 采用的是基于字符串校验和(string checksum)的 HTML 节点复用方式,字对字地严格校验一致性,一旦发现不匹配就完全丢弃服务端渲染结果,在客户端重新渲染:

If for any reason there’s a mismatch, React raises a warning in development mode and replaces the entire tree of server-generated markup with HTML that has been generated on the client.

生成了大量的额外属性:

// renderToString
<div data-reactroot="" data-reactid="1"
    data-react-checksum="122239856">
  <!-- react-text: 2 -->This is some <!-- /react-text -->
  <span data-reactid="3">server-generated</span>
  <!-- react-text: 4--> <!-- /react-text -->
  <span data-reactid="5">HTML.</span>
</div>

这时候renderToStaticMarkup生成干净清爽的 HTML 还有着不小的体积优势:

// renderToStaticMarkup
<div data-reactroot="">
  This is some <span>server-generated</span> <span>HTML.</span>
</div>

React 16 改用单节点校验来复用(服务端返回的)HTML 节点,不再生成data-reactiddata-react-checksum等体积占用大户,两个 API 渲染结果的体积差异变得微乎其微。例如,对于 React 组件:

class MyComponent extends React.Component {
  state = {
      title: 'Welcome to React SSR!',
    };
  }

  render() {
    return (
      <div>
        <h1 className="here">
          {this.state.title} Hello There!
        </h1>
      </div>
    );
  }
}

二者的渲染结果分别为:

// renderToString
<div data-reactroot=""><h1 class="here">Welcome to React SSR!<!-- --> Hello There!</h1></div>

// renderToStaticMarkup
<div><h1 class="here">Welcome to React SSR! Hello There!</h1></div>

也就是说,目前(2020/11/8,React 17.0.1)renderToStaticMarkuprenderToString的实际差异主要在于:

  • renderToStaticMarkup不生成data-reactroot
  • renderToStaticMarkup不在相邻文本节点之间生成<!-- -->(相当于合并了文本节点,不考虑节点复用,算是针对静态渲染的额外优化措施)

renderToNodeStream

ReactDOMServer.renderToNodeStream(element)

对应于renderToString的 Stream API,将renderToString生成的 HTML 字符串以Node.js Readable stream形式返回

P.S.默认返回utf-8 编码的字节流,其它编码格式需自行转换

P.S.该 API 的实现依赖Node.js 的 Stream 特性,所以不能在浏览器环境使用

renderToStaticNodeStream

ReactDOMServer.renderToStaticNodeStream(element)

对应于renderToStaticMarkup的 Stream API,将renderToStaticMarkup生成的干净 HTML 字符串以Node.js Readable stream形式返回

P.S.同样按 utf-8 编码,并且不能在浏览器环境使用

二.ReactDOM

hydrate()

ReactDOM.hydrate(element, container[, callback])

与常用的render()函数签名完全一致:

ReactDOM.render(element, container[, callback])

hydrate()配合 SSR 使用,与render()的区别在于渲染过程中能够复用服务端返回的现有 HTML 节点,只为其附加交互行为(事件监听等),并不重新创建 DOM 节点:

React will attempt to attach event listeners to the existing markup.

需要注意的是,服务端返回的 HTML 与客户端渲染结果不一致时,出于性能考虑,hydrate()并不纠正除文本节点外的 SSR 渲染结果,而是将错就错

There are no guarantees that attribute differences will be patched up in case of mismatches. This is important for performance reasons because in most apps, mismatches are rare, and so validating all markup would be prohibitively expensive.

只在development模式下对这些不一致的问题报 Warning,因此必须重视 SSR HydrationWarning,要当 Error 逐个解决:

This performance optimization means that you will need to make extra sure that you fix any markup mismatch warnings you see in your app in development mode.

特殊的,对于意料之中的不一致问题,例如时间戳,可通过suppressHydrationWarning={true}属性显式忽略该元素的 HydrationWarning(只是忽略警告,并不纠错,所以仍保留服务端渲染结果)。如果非要在服务端和客户端分别渲染不同的内容,建议先保证首次渲染内容一致,再通过更新来完成(当然,性能会稍差一点),例如:

class MyComponent extends React.Component {
  state = {
    isClient: false
  }

  render() {
    return this.state.isClient ? '渲染...客户端内容' : '渲染...服务端内容';
  }

  componentDidMount() {
    this.setState({
      isClient: true
    });
  }
}

三.SSR 相关的 API 限制

大部分生命周期函数在服务端都不执行

SSR 模式下,服务端只执行 3 个生命周期函数

  • constructor
  • getDerivedStateFromProps
  • render

其余任何生命周期在服务端都不执行,包括getDerivedStateFromErrorcomponentDidCatch等错误处理 API

<img src="http://cdn.ayqy.net/data/home/qxu1001840309/htdocs/cms/wordpress/wp-content/uploads/2020/11/96C4B5DD-20B3-43F9-A8C6-9B220EB2C0A5.png" alt="react ssr lifecycle" width="625" height="355" class="size-large wp-image-2319" />

P.S.已经废弃的componentWillMountUNSAFE_componentWillMountgetDerivedStateFromPropsgetSnapshotBeforeUpdate互斥,若存在后一组新 API 中的任意一个,就不会调用前两个旧 API

不支持 Error Boundary 和 Portal

With streaming rendering it's impossible to "call back" markup that has already been sent, and we opted to keep renderToString and renderToNodeStream's output identical.

为了支持流式渲染,同时保持 String API 与 Stream API 输出内容的一致性,牺牲了会引发渲染回溯的两大特性

  • Error Boundary:能够捕获子孙组件的运行时错误,并渲染一个降级 UI
  • Portal:能够将组件渲染到指定的任意 DOM 节点上,同时保留事件按组件层级冒泡

很容易理解,流式边渲染边响应,无法(回溯回去)修改已经发出去的内容,所以其它类似的场景也不支持,比如渲染过程中动态往head里插个stylescript标签

P.S.关于 SSR Error Boundary 的更多讨论,见componentDidCatch doesn't work in React 16's renderToString

参考资料

有所得、有所惑,真好

关注「前端向后」微信公众号,你将收获一系列「用原创」的高质量技术文章,主题包括但不限于前端、Node.js以及服务端技术

本文首发于 ayqy.net ,原文链接:http://www.ayqy.net/blog/reac...


前端向后
167 声望1.9k 粉丝

[链接]