服务端渲染有利于首屏加载速度和搜索引擎优化(SEO)。
babel-node
因为 Node 环境原生不支持 JSX,首先我们使用 babel-node
让 Node 环境支持 JSX:
yarn add babel-cli
然后我们在 package.json
里用 babel-node
来替代 Node
,”scripts” 字段里加入下面一行:
"scripts": {
"server": "NODE_ENV=test nodemon --exec babel-node server/server.js",
}
现在我们的 Node 环境已经支持 es6 了,那么怎么支持 JSX 呢?我们在项目根目录下新建个 babel 的配置文件 .babelrc
,然后把我们的 package.json
文件下的 babel 的配置复制到 .babelrc
文件中:
{
"presets": [
"react-app"
],
"plugins": [
"transform-decorators-legacy",
[
"import",
{
"libraryName": "antd-mobile",
"style": "css"
}
]
]
}
renderToString()
然后我们把 src/index.js 下的内容复制到服务端 server/server.js中
let context = {}
const markup = renderToString(
(<Provider store={store}>
<StaticRouter
location={req.url}
context={context}
>
<App></App>
</StaticRouter>
</Provider>)
)
css-modules-require-hook 解决 CSS 报错
Node 里是没有 CSS 的,所以我们用 css-modules-require-hook 这个包来解决 CSS 报错相关问题:
yarn add css-modules-require-hook
在服务端的代码中 Route 相关代码之前引入 csshook:
import csshook from 'css-modules-require-hook/preset' // import hook before routes
然后在项目根目录下建立文件 cmrh.conf.js:
// cmrh.conf.js
module.exports = {
// Same scope name as in webpack build
generateScopedName: '[name]__[local]___[hash:base64:5]',
}
asset-require-hook 解决图片报错
在 servder.js 代码中加入下列代码:
import assethook from 'asset-require-hook'
assethook({
extensions: ['png']
})
拼接 public/index.html 骨架
把服务端生成的 html 放到 html 骨架里:
const pageHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root">${markup}</div>
</body>
</html>`
res.send(pageHtml)
现在我们的 html 页面就是带有骨架的完整页面了。
引入 CSS 和 JS 文件
我们 build 项目生成的 build 文件夹下的 build/asset-manifest.json
文件中有 CSS 和 JS 的路径,因为文件名每次 build 每次都在变,这样做是为了在文件名中加入 hash 值使之不命中缓存。
import staticPath from '../build/asset-manifest.json'
然后我们在 html 骨架中加入下面两行代码来分别引入 CSS 和 JS
<link rel="stylesheet" href="/${staticPath['main.css']}">
<script src="/${staticPath['main.js']}"></script>
重启项目我们发现我们服务端渲染的首页已经正常显示了。
React 16 renderToNodeStream() 新 API 实现服务端渲染
直接渲染成 Node 节点流,通过这个节点流给浏览器流式地返回,我们把我们上面的 renderToString() 改为 React 16 的renderToNodeStream()
const objDes = {
'/msg': 'React Chat',
'/boss': 'boss genius'
}
res.write(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<meta name="keywords" content="React, Redux, SSR, ${objDes[req.url]}">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="stylesheet" href="/${staticPath['main.css']}">
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root">`)
const markupStream = renderToNodeStream(
(<Provider store={store}>
<StaticRouter
location={req.url}
context={context}
>
<App></App>
</StaticRouter>
</Provider>)
)
markupStream.pipe(res, {end: false})
markupStream.on('end', ()=> {
res.write(`</div>
<script src="/${staticPath['main.js']}"></script>
</body>
</html>`)
res.end()
})
然后把 src/index.js
里的 ReactDOM.render()
改为 ReactDOM.hydrate()
然后我们就完成流式渲染了。