一.前言
当我们选择使用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: './s
erver
/
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}</s
pan
>
<
/
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
5.React Server Side Rendering 解决 SPA 应用的 SEO 问题
6.Server-Side Rendering with React + React-Router
原创文章转载请注明:
转载自AlloyTeam:http://www.alloyteam.com/2017/01/react-from-scratch-server-render/