一.前言
当我们选择使用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包安装:
| npm-Sinstall 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 | varexpress=require('express'); varapp=express(); varReact=require('react'), ReactDOMServer=require('react-dom/server'); varApp=React.createFactory(require('./App')); app.get('/',function(req,res){ varhtml=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 | varReact=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(){ varcontent=this.state.isSayBye?'Bye':'Hello World'; returndiv(null, div(null,content), button({onClick:this.handleClick},'switch') ); } }) |
运行:
结果:
三.动态的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包安装:
| npm install-Sexpress 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 | varpath=require('path'); varassetsPath=path.join(__dirname,"public","assets"); varserverPath=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 | varReact=require('react'); varApp=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 | varexpress=require('express'); varpath=require('path'); varpage=require("./page.generator.js").page; varapp=express(); varport=8082; app.use(express.static(path.join(__dirname,'..','public'))); app.get('/',function(req,res){ varprops={ initialCount:9 }; varhtml=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 | varReact=require('react'); varReactDOMServer=require("react-dom/server"); varApp=require('../app/App'); varReactDOM=require('react-dom'); module.exports=function(props){
varcontent=ReactDOMServer.renderToString( <App initialCount={props.initialCount}></App> ); varpropsScript='var APP_PROPS = '+JSON.stringify(props); varhtml=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> ); returnhtml; } |
为了让服务器端和客户端的props一致,将一个服务器生成的首屏props赋给客户端的全局变量APP_PROPS,在客户端初始化根组件时使用这个APP_PROPS根组件的props。
app/entry.js:客户端入口文件,用于在客户端渲染根组件,别忘了使用在服务器端写入的APP_PROPS初始化根组件的props
| varReact=require('react'), ReactDOM=require('react-dom'), App=require('./App'); varAPP_PROPS=window.APP_PROPS||{}; ReactDOM.render( <App initialCount={APP_PROPS.initialCount}/>, document.getElementById('root') ); |
源代码放在github上,懒得复制粘贴搭建项目的同学可以猛戳这里
github上还有其他的服务器渲染的例子,有兴趣的同学可以参考参考:
1.无webpack无jsx版本
2.使用webpack版本
参考文章:
1.Rendering React Components on the Server
2.一看就懂的 React Server Rendering(Isomorphic JavaScript)入門教學
3.Clientside react-script overrides serverside rendered props
4.React直出实现与原理
5.React Server Side Rendering 解决 SPA 应用的 SEO 问题
6.Server-Side Rendering with React + React-Router
建议继续学习:
- 使用CSS3开启GPU硬件加速提升网站动画渲染性能 (阅读:5409)
- 移动互联网创业公司的服务器选择 (阅读:4971)
- 通过HttpListener实现轻量级Web服务器[原创] (阅读:3416)
- 从用户体验出发的性能指标分析-Start Render (阅读:3375)
- 查看你服务器的安全性 (阅读:3319)
- JavaScript定时机制、以及浏览器渲染机制 浅谈 (阅读:3088)
- Ruby作为服务器端应用已经成熟了 (阅读:2735)
- 图片服务器博客 (阅读:2567)
- border:none;与border:0;的区别 (阅读:2546)
- CSS的渲染效率 (阅读:2497)
QQ技术交流群:445447336,欢迎加入!
扫一扫订阅我的微信号:IT技术博客大学习