服務端渲染在久遠的 JSP、PHP 時期就已經在使用了,但是在單頁面應用大行其道的情況下,卻依然有著各種各樣的方案來支援,因為服務端渲染確實有著很多好多好處,尤其是 Node 和三大框架相結合 的前後端同構,前後端共用一套程式碼,更是將單頁應用的便利和服務端渲染的好處相結合,這裡來看一下 React Server Render 的原理和過程。
React 同構
React 同構的關鍵要素
DOM 的一致性
在前後端渲染相同的 Component,將輸出一致的 Dom 結構。
完善的 Component 屬性及生命週期與客戶端的 render 時機是 React 同構的關鍵。
React 的虛擬 DOM 以物件樹的形式儲存在記憶體中,並且是可以在任何支援 JavaScript 的環境中生成的,所以可以在瀏覽器和 Node 中生成,這位前後端同構提供了先決條件。
如上圖:
- React 的虛擬 DOM 的生成是可以在任何支援 Javascript 的環境生成的,所以可以在瀏覽器 Node 環境生成.
- 虛擬 DOM 可以直接轉成 String.
- 然後插入到 HTML 檔案中輸出給瀏覽器便可.
虛擬 Dom 在前後端都是以物件樹的形式存在的,但在展露原型的方式確是不一樣的。
- 在瀏覽器裡,React 通過 ReactDom 的 render 方法將虛擬 Dom 渲染到真實的 Dom 樹上,生成網頁
- 但是在 Node 環境下是沒有渲染引擎的,所以 React 提供了另外兩個方法::
ReactDOMServer.renderToString
和ReactDOMServer.renderToStaticMarkup
可將其渲染為 HTML 字串
不同的生命週期
在服務端上 Component 生命週期只會到 componentWillMount
,客戶端則是完整的。
客戶端 render 時機
同構時,服務端結合資料將 Component 渲染成完整的 HTML 字串並將資料狀態返回給客戶端,客戶端會判斷是否可以直接使用或需要重新掛載。
以上便是 React 在同構/服務端渲染的提供的基礎條件。在實際專案應用中,還需要考慮其他邊角問題,例如伺服器端沒有 window
物件,需要做不同處理等。
renderToString 和 renderToStaticMarkup
ReactDOMServer
提供 renderToString
和 renderToStaticMarkup
的方法,大多數情況使用 renderToString
,這樣會為元件增加 checksum
React 在客戶端通過 checksum 判斷是否需要重新render相同則不重新render,省略建立 DOM 和掛 載DOM 的過程,接著觸發 componentDidMount
等事件來處理服務端上的未盡事宜(事件繫結等),從而加快了互動時間;不同時,元件將客戶端上被重新掛載 render。
renderToStaticMarkup
則不會生成與 react 相關的 data-*
,也不存在 checksum,輸出的 html 如下
在客戶端時元件會被重新掛載,客戶端重新掛載不生成 checknum( 也沒這個必要 ),所以該方法只當服務端上所渲染的元件在客戶端不需要時才使用。
checknum 實際上是 HTML 片段的 adler32 演算法值,實際上呼叫 React.render(<
時候做了下面一些事情:
MyComponent />
, container);
- 看看
container
是否為空,不為空則認為有可能是直出了結果。 - 接下來第一個元素是否有
data-react-checksum
屬性,如果有則通過瀏覽器的 adler32 演算法得到的值和 data-react-checksum 對比,如果一致則表示,無需渲染,否則重新渲染,下面是 adler32 演算法實現:
var MOD = 65521;
// This is a clean-room implementation of adler32 designed for detecting// if markup is not what we expect it to be. It does not need to be// cryptographically strong, only reasonably good at detecting if markup// generated on the server is different than that on the client.function adler32(data) {
var a = 1;
var b = 0;
for (var i = 0;
i <
data.length;
i++) {
a = (a + data.charCodeAt(i)) % MOD;
b = (b + a) % MOD;
} return a | (b <
<
16);
}複製程式碼
注意事項
- 服務端上的資料狀態與同步給客戶端
服務端上的產生的資料需要隨著頁面一同返回,客戶端使用該資料去 render,從而保持狀態一致。服務端上使用 renderToString 而在客戶端上依然重新掛載元件的情況大多是因為在返回 HTML 的時候沒有將服務端上的資料一同返回,或者是返回的資料格式不對導致,開發時可以留意 chrome 上的提示如
-
服務端需提前拉取資料,客戶端則在
componentDidMount
呼叫平臺上的差異,服務端渲染只會執行到compnentWillMount
上,所以為了達到同構的目的,可以把拉取資料的邏輯寫到 React Class 的靜態方法上,一方面服務端上可以通過直接操作靜態方法來提前拉取資料再根據資料生成 HTML,另一方面客戶端可以在componentDidMount
時去呼叫該靜態方法拉取資料 -
保持資料的確定性這裡指影響元件
render
結果的資料,舉個例子,下面的元件由於在服務端與客戶端渲染上會因為元件上產生不同隨機數的原因而導致客戶端將重新渲染。
Class Wrapper extends Component {
render() {
return (<
h1>
{Math.random()
}<
/h1>
);
}
};
複製程式碼
可以將 Math.random()
封裝至 Component 的 props
中,在服務端上生成隨機數並傳入到這個 component 中,從而保證隨機數在客戶端和服務端一致。如
Class Wrapper extends Component {
render() {
return (<
h1>
{this.props.randomNum
}<
/h1>
);
}
};
複製程式碼
服務端上傳入randomNum
let randomNum = Math.random()var html = ReacDOMServer.renderToString(<
Wrapper randomNum={randomNum
} />
);
複製程式碼
- 平臺區分
當前後端共用一套程式碼的時候,像前端特有的 window
物件,Ajax 請求 在後端是無法使用上的,後端需要去掉這些前端特有的物件邏輯或使用對應的後端方案,如後端可以使用 http.request 替代 Ajax 請求,所以需要進行平臺區分,主要有以下幾種方式
1.程式碼使用前後端通用的模組,如 isomorphic-fetch
2.前後端通過 webpack 配置 resolve.alias
對應不同的檔案,如客戶端使用 /browser/request.js
來做 ajax 請求
resolve: {
alias: {
'request': path.join(pathConfig.src, '/browser/request'),
}
}複製程式碼
服務端 webpack 上使用 /server/request.js 以 http.request 替代 ajax 請求
resolve: {
alias: {
'request': path.join(pathConfig.src, '/server/request'),
}
}複製程式碼
3.使用 webpack.DefinePlugin
在構建時新增一個平臺區分的值,這種方式的在 webpack UglifyJsPlugin 編譯後,非當前平臺( 不可達程式碼 )的程式碼將會被去掉,不會增加檔案大小。如在服務端的 webpack 加上下面配置
new webpack.DefinePlugin({
"__ISOMORPHIC__": true
}),複製程式碼
在JS邏輯上做判斷
if(__ISOMORPHIC__){
// do server thing
} else {
// do browser thing
}複製程式碼
4.window 是瀏覽器上特有的物件,所以也可以用來做平臺區分
var isNode = typeof window === 'undefined';
if (isNode) {
// do server thing
} else {
// do browser thing
}複製程式碼
componentWillReceiveProps
中,依賴資料變化的方法,需考慮在componentDidMount
做相容
舉個例子,identity 預設為 UNKOWN,從後臺拉取到資料後,更新其值,從而觸發 setButton 方法
componentWillReceiveProps(nextProps) {
if (nextProps.role.get('identity') !== UNKOWN &
&
nextProps.role.get('identity') !== this.props.role.get('identity'))) {
this.setButton();
}
}複製程式碼
同構時,由於服務端上已做了第一次資料拉取,所以上面程式碼在客戶端上將由於 identity 已存在而導致永不執行 setButton 方法,解決方式可在 componentDidMount 做相容處理
componentDidMount() {
// .. 判斷是否為同構 if (identity !== UNKOWN) {
this.setButton(identity);
}
} 複製程式碼