写在前面
React 提供的 SSR API 分为两部分,一部分面向服务端(react-dom/server
),另一部分仍在客户端执行(react-dom
)
一.ReactDOMServer
ReactDOMServer
相关 API 能够在服务端将 React 组件渲染成静态的(HTML)标签:
把组件树渲染成对应 HTML 标签的工作在浏览器环境也能完成,因此,面向服务端的 React DOM API 也分为两类:
- 能跨 Node.js、浏览器环境运行的 String API:renderToString()、renderToStaticMarkup()
- 只能在 Node.js 环境运行的 Stream API:renderToNodeStream()、renderToStaticNodeStream()
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-reactid
、data-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)renderToStaticMarkup
与renderToString
的实际差异主要在于:
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
其余任何生命周期在服务端都不执行,包括getDerivedStateFromError
、componentDidCatch
等错误处理 API
P.S.已经废弃的componentWillMount
、UNSAFE_componentWillMount
与getDerivedStateFromProps
、getSnapshotBeforeUpdate
互斥,若存在后一组新 API 中的任意一个,就不会调用前两个旧 API
不支持 Error Boundary 和 Portal
为了支持流式渲染,同时保持 String API 与 Stream API 输出内容的一致性,牺牲了会引发渲染回溯的两大特性:
- Error Boundary:能够捕获子孙组件的运行时错误,并渲染一个降级 UI
- Portal:能够将组件渲染到指定的任意 DOM 节点上,同时保留事件按组件层级冒泡
很容易理解,流式边渲染边响应,无法(回溯回去)修改已经发出去的内容,所以其它类似的场景也不支持,比如渲染过程中动态往head
里插个style
或script
标签
P.S.关于 SSR Error Boundary 的更多讨论,见componentDidCatch doesn't work in React 16's renderToString
参考资料
- ReactDOMServer
- ReactDOM
- The Component Lifecycle
- What’s New With Server-Side Rendering in React 16
- react lifecycle methods diagram
有所得、有所惑,真好
关注「前端向后」微信公众号,你将收获一系列「用心原创」的高质量技术文章,主题包括但不限于前端、Node.js以及服务端技术
本文首发于 ayqy.net ,原文链接:http://www.ayqy.net/blog/reac...