写在前面

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)标签

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

renderToString

ReactDOMServer.renderToString(element)

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

renderToStaticMarkup

ReactDOMServer.renderToStaticMarkup(element)

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

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

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

生成了大量的额外属性:

// 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 节点:

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

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

特殊的,对于意料之中的不一致问题,例如时间戳,可通过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

为了支持流式渲染,同时保持 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...

03-05 15:26