一.前言
當我們選擇使用Node+React的技術棧開發Web時,React提供了一種優雅的方式實現伺服器渲染。使用React實現伺服器渲染有以下好處:
1.利於SEO:React伺服器渲染的方案使你的頁面在一開始就有一個HTML DOM結構,方便Google等搜尋引擎的爬蟲能爬到網頁的內容。
2.提高首屏渲染的速度:伺服器直接返回一個填滿資料的HTML,而不是在請求了HTML後還需要非同步請求首屏資料。
3.前後端都可以使用js
二.神奇的renderToString和renderToStaticMarkup
有兩個神奇的React API都可以實現React伺服器渲染:renderToString和renderToStaticMarkup。renderToString和renderToStaticMarkup的主要作用都是將React Component轉化為HTML的字串。這兩個函式都屬於react-dom(react-dom/server)包,都接受一個React Component引數,返回一個String。
也許你會奇怪為什麼會有兩個用於伺服器渲染的函式,其實這兩個函式是有區別的:
1.renderToString:將React Component轉化為HTML字串,生成的HTML的DOM會帶有額外屬性:各個DOM會有data-react-id屬性,第一個DOM會有data-checksum屬性。
2.renderToStaticMarkup:同樣是將React Component轉化為HTML字串,但是生成HTML的DOM不會有額外屬性,從而節省HTML字串的大小。
下面是一個在伺服器端使用renderToStaticMarkup渲染靜態頁面的例子:
npm包安裝:
1 |
npm -S install express react react-dom |
server.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
var express = require('express'); var app = express(); var React = require('react'), ReactDOMServer = require('react-dom/server'); var App = React.createFactory(require('./App')); app.get('/', function(req, res) { var html = ReactDOMServer.renderToStaticMarkup( React.DOM.body( null, React.DOM.div({id: 'root', dangerouslySetInnerHTML: { __html: ReactDOMServer.renderToStaticMarkup(App()) } }) ) ); res.end(html); }); app.listen(3000, function() { console.log('running on port ' + 3000); }); |
App.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var React = require('react'), DOM = React.DOM, div = DOM.div, button = DOM.button, ul = DOM.ul, li = DOM.li module.exports = React.createClass({ getInitialState: function() { return { isSayBye: false } }, handleClick: function() { this.setState({ isSayBye: !this.state.isSayBye }) }, render: function() { var content = this.state.isSayBye ? 'Bye' : 'Hello World'; return div(null, div(null, content), button({onClick: this.handleClick}, 'switch') ); } }) |
執行:
1 |
node server.js |
結果:
三.動態的React元件
上例的頁面中,點選“switch”按鈕是沒有反應的,這是因為這個頁面只是一個靜態的HTML頁面,沒有在客戶端渲染React元件並初始化React例項。只有在初始化React例項後,才能更新元件的state和props,初始化React的事件系統,執行虛擬DOM的重新渲染機制,讓React元件真正“動”起來。
或許你會奇怪,伺服器端已經渲染了一次React元件,如果在客戶端中再渲染一次React元件,會不會渲染兩次React元件。答案是不會的。祕訣在於data-react-checksum屬性:
上文有說過,如果使用renderToString渲染元件,會在元件的第一個DOM帶有data-react-checksum屬性,這個屬性是通過adler32演算法算出來:如果兩個元件有相同的props和DOM結構時,adler32演算法算出的checksum值會一樣,有點類似於雜湊演算法。
當客戶端渲染React元件時,首先計算出元件的checksum值,然後檢索HTML DOM看看是否存在數值相同的data-react-checksum屬性,如果存在,則元件只會渲染一次,如果不存在,則會丟擲一個warning異常。也就是說,當伺服器端和客戶端渲染具有相同的props和相同DOM結構的元件時,該React元件只會渲染一次。
在伺服器端使用renderToStaticMarkup渲染的元件不會帶有data-react-checksum屬性,此時客戶端會重新渲染元件,覆蓋掉伺服器端的元件。因此,當頁面不是渲染一個靜態的頁面時,最好還是使用renderToString方法。
上述的客戶端渲染React元件的流程圖如下:
四.一個完整的例子
下面使用React伺服器渲染實現一個簡單的計數器。為了簡單,本例中不使用redux、react-router框架,儘量排除各種沒必要的東西。
專案目錄如下:
npm包安裝:
1 |
npm install -S express react react-dom jsx-loader |
webpack.config.js:webpack配置檔案,作用是在客戶端中可以使用程式碼模組化和jsx形式的元件編寫方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
var path = require('path'); var assetsPath = path.join(__dirname, "public", "assets"); var serverPath = path.join(__dirname, "server"); module.exports = [ { name: "browser", entry: './app/entry.js', output: { path: assetsPath, filename: 'entry.generator.js' }, module: { loaders: [ { test: /\.js/, loader: "jsx-loader" } ] } }, { name: "server-side rending", entry: './server/page.js', output: { path: serverPath, filename: "page.generator.js", // 使用page.generator.js的是nodejs,所以需要將 // webpack模組轉化為CMD模組 library: 'page', libraryTarget: 'commonjs' }, module: { loaders: [ { test: /\.js$/, loader: 'jsx-loader' } ] } } ] |
app/App.js:根元件 (一個簡單的計數器元件),在客戶端和伺服器端都需要引入使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var React = require('react'); var App = React.createClass({ getInitialState: function() { return { count: this.props.initialCount }; }, _increment: function() { this.setState({ count: this.state.count + 1 }); }, render: function() { return ( <div> <span>the count is: </span> <span onClick={this._increment}>{this.state.count}</span> </div> ) } }) module.exports = App; |
server/index.js:伺服器入口檔案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var express = require('express'); var path = require('path'); var page = require("./page.generator.js").page; var app = express(); var port = 8082; app.use(express.static(path.join(__dirname, '..', 'public'))); app.get('/', function(req, res) { var props = { initialCount: 9 }; var html = page(props); res.end(html); }); app.listen(port, function() { console.log('Listening on port %d', port); }); |
server/page.js:暴露一個根元件轉化為字串的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
var React = require('react'); var ReactDOMServer = require("react-dom/server"); var App = require('../app/App'); var ReactDOM = require('react-dom'); module.exports = function(props) { var content = ReactDOMServer.renderToString( <App initialCount={props.initialCount}></App> ); var propsScript = 'var APP_PROPS = ' + JSON.stringify(props); var html = ReactDOMServer.renderToStaticMarkup( <html> <head> </head> <body> <div id="root" dangerouslySetInnerHTML={ {__html: content} } /> <script dangerouslySetInnerHTML={ {__html: propsScript} }></script> <script src={"assets/entry.generator.js"}></script> </body> </html> ); return html; } |
為了讓伺服器端和客戶端的props一致,將一個伺服器生成的首屏props賦給客戶端的全域性變數APP_PROPS,在客戶端初始化根元件時使用這個APP_PROPS根元件的props。
app/entry.js:客戶端入口檔案,用於在客戶端渲染根元件,別忘了使用在伺服器端寫入的APP_PROPS初始化根元件的props
1 2 3 4 5 6 7 8 9 10 |
var React = require('react'), ReactDOM = require('react-dom'), App = require('./App'); var APP_PROPS = window.APP_PROPS || {}; ReactDOM.render( <App initialCount={APP_PROPS.initialCount}/>, document.getElementById('root') ); |
原始碼放在github上,懶得複製貼上搭建專案的同學可以猛戳這裡
github上還有其他的伺服器渲染的例子,有興趣的同學可以參考參考:
參考文章:
1.Rendering React Components on the Server
2.一看就懂的 React Server Rendering(Isomorphic JavaScript)入門教學
3.Clientside react-script overrides serverside rendered props