服務端渲染在久遠的 JSP、PHP 時期就已經在使用了,可是在單頁面應用大行其道的狀況下,卻依然有着各類各樣的方案來支持,由於服務端渲染確實有着不少好多好處,尤爲是 Node 和三大框架相結合 的先後端同構,先後端共用一套代碼,更是將單頁應用的便利和服務端渲染的好處相結合,這裏來看一下 React Server Render 的原理和過程。 html
在先後端渲染相同的 Component,將輸出一致的 Dom 結構。前端
完善的 Component 屬性及生命週期與客戶端的 render 時機是 React 同構的關鍵。react
React 的虛擬 DOM 以對象樹的形式保存在內存中,而且是能夠在任何支持 JavaScript 的環境中生成的,因此能夠在瀏覽器和 Node 中生成,這位先後端同構提供了先決條件。webpack
如上圖:git
虛擬 Dom 在先後端都是以對象樹的形式存在的,但在展露原型的方式確是不同的。github
ReactDOMServer.renderToString
和 ReactDOMServer.renderToStaticMarkup
可將其渲染爲 HTML 字符串在服務端上 Component 生命週期只會到 componentWillMount
,客戶端則是完整的。web
同構時,服務端結合數據將 Component 渲染成完整的 HTML 字符串並將數據狀態返回給客戶端,客戶端會判斷是否能夠直接使用或須要從新掛載。ajax
以上即是 React 在同構/服務端渲染的提供的基礎條件。在實際項目應用中,還須要考慮其餘邊角問題,例如服務器端沒有 window
對象,須要作不一樣處理等。算法
ReactDOMServer
提供 renderToString
和 renderToStaticMarkup
的方法,大多數狀況使用 renderToString
,這樣會爲組件增長 checksumchrome
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); } } 複製代碼