寫在前面
React 提供的 SSR API 分為兩部分,一部分面向服務端(react-dom/server),另一部分仍在客戶端執行(react-dom)
[caption id="attachment_2317" align="alignnone" width="625"]
react ssr[/caption]
一。ReactDOMServer
ReactDOMServer 相關 API 能夠在服務端將 React 組件渲染成靜態的(HTML)標籤:
The ReactDOMServer object enables you to render components to static markup.
把組件樹渲染成對應 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 對服務端返回的視圖結構附加上交互行為,完成頁面渲染:
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-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 節點:
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 個生命周期函數:
constructorgetDerivedStateFromPropsrender
其餘任何生命周期在服務端都不執行,包括 getDerivedStateFromError、componentDidCatch 等錯誤處理 API
[caption id="attachment_2319" align="alignnone" width="625"]
react ssr lifecycle[/caption]
P.S. 已經廢棄的 componentWillMount、UNSAFE_componentWillMount 與 getDerivedStateFromProps、getSnapshotBeforeUpdate 互斥,若存在後一組新 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 裡插個 style 或 script 標籤
P.S. 關於 SSR Error Boundary 的更多討論,見 componentDidCatch doesn't work in React 16's renderToString
暫無評論,快來發表你的看法吧