@bornkiller
2017-08-19T17:08:47.000000Z
字数 4072
阅读 2668
React
笔者负责开发维护的项目,主要属于内部管理控制台项目,对服务端直出并无深入了解。近日开始调研,带来个人视角的 React SSR
,仅讨论具体形态,不赘述优劣。
万事开头难,对于 SSR
渲染,依旧使用 HelloWorld
套路。案例不包含异步渲染,数据拉取,路由等等
因素。
/**
* @description - React SSR HelloWorld
* @author - huang.jian <hjj491229492@hotmail.com>
*/
import React, { Component } from 'react';
export default class HelloWorld extends Component {
constructor(props) {
super(props);
this.state = {
title: 'Hello world!!!',
description: 'React ssr practice!!!'
};
}
render() {
return (
<article className="ssr-operation">
<h3>{this.state.title}</h3>
<p>{this.state.description}</p>
</article>
);
}
}
/**
* @description - SSR HelloWorld case
* @author - huang.jian <hjj491229492@hotmail.com>
*/
// External
import React from 'react';
import { renderToString } from 'react-dom/server';
// Internal
import HelloWorld from '../src/component/HelloWorld/';
// Scope
const RootElement = React.createElement(HelloWorld);
const ssr = renderToString(RootElement);
console.log(ssr);
Node
环境下上述代码无法直接运行,需要借助 Webpack
进行转译。此处并不接入 HTTP
服务器,只进行 Node
环境渲染,react-dom
提供 renderToString
,renderToStaticMarkup
渲染模式,差异在于是否提供辅助标记。下文所指服务端渲染,皆为 renderToString
渲染方式。
输出结果如下:
<!-- renderToStaticMarkup -->
<article class="ssr-operation">
<h3>Hello world!!!</h3>
<p>React ssr practice!!!</p>
</article>
<!-- renderToStaticMarkup -->
<!-- renderToString -->
<article
class="ssr-operation"
data-reactroot=""
data-reactid="1"
data-react-checksum="1848063430"
>
<h3 data-reactid="2">Hello world!!!</h3>
<p data-reactid="3">React ssr practice!!!</p>
</article>
<!-- renderToString -->
开发 web server
暂不过多讨论,将渲染结果装入 html
模板作为相应内容即可,示例使用极简模式。
function interpolate(body, title) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>${title || 'React SSR'}</title>
</head>
<body>
<section class="bootstrap">
${body}
</section>
</body>
</html>
`;
}
webpack
配置单独成块,其设计以浏览器端编译为主,编译 SSR
环境,需要做部分适配。
Node
环境下,第三方模块无需单独加载,所以全部外置,提升效率。
// External
const NodeExternals = require('webpack-node-externals');
module.exports = {
// ...
externals: NodeExternals()
// ...
};
环境无需提供冗余 polyfill
,禁用模块,变量等 polyfill
。部分参数在 target: 'node'
时自动生效,自行决定配置。
module.exports = {
node: {
global: false,
process: false,
crypto: false,
Buffer: false,
fs: false,
net: false,
tls: false,
__dirname: false,
__filename: false
}
};
应用中 import
图片,HTML
,CSS
等文件,会使用 file-loader
,style-loader
等 进行预处理,此处有以下问题:
style-loader
生成的代码 Node
环境无法运行file-loader
支持 emitFile
参数,不会重复生成文件。css, scss
等文件分两种情况讨论,如果不使用 css modules
,基本上不会影响到渲染结果,直接忽略。如果使用 css modules
,配置 css-loader
仅生成 mappings
,不重复传递代码即可。
module.exports = {
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: [
{
loader: 'css-loader/locals',
options: {
root: path.resolve(process.cwd(), 'src'),
modules: true,
camelCase: 'only',
localIdentName: '[name]__[local]___[hash:base64:5]'
}
},
{ loader: 'postcss-loader' }
]
},
// Ignore scss files, which never reflect render markup
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
{ loader: 'ignore-loader' }
]
},
{
test: /\.(png|jpe?g|gif|mp3|woff|woff2|ttf|eot|svg)(\?.*)?$/,
use: [
{
loader: 'file-loader',
options: {
name: 'asset/[name].[ext]',
// Don't emit file again
emitFile: false
}
}
]
}
]
}
};
笔者并未参与后端开发,此块了解不多,仅做简单探讨。前端代码跟后端代码同一仓库维护,统一交由 webpack
编译后执行是否合适?印象之中,后端开发包含日志,监控,数据库等各种模块,与前端代码强行糅合是否合适?下文暂定方案为前者。
此处讨论开发阶段进程管理。web server
开发与前端代码严重耦合,改动前端代码,需要打包 static web
内容,改动 web server
代码需要重启服务。目前笔者基于 webpack
二次包装,定制公司内部使用的脚手架工具 coco
,利用 webpack watch
机制与 nodemon
简单结合,实现代码变更到重启服务的流程。
脚本配置如下:
{
"scripts": {
"dev:client": "coco server --react --bootstrap ./src/main.jsx",
"dev:ssr": "coco ssr --watch --server --bootstrap ./server/main.jsx"
}
}
SSR
需要考虑与 client
渲染的状态同步,如果后端渲染的初始状态与浏览器端渲染初始状态不一致,代码抛出报错。修改上述示例如下:
this.state = {
title: 'Hello world!!!',
description: 'React ssr practice!!!',
// Impure function
timestamp: Date.now()
};
由于 Date.now
为非纯函数,服务端渲染与客户端渲染初始状态不一致,校验无法通过。需要其他方式,保证状态同步,调整如下:
this.state = {
title: 'Hello world!!!',
description: 'React ssr practice!!!',
// eslint-disable-next-line
timestamp: typeof window === 'undefined' ?
global.timestamp : (window.__HELLO_STATE__.timestamp)
};
const timestamp = Date.now();
const HelloState = `
<script>
window.__HELLO_STATE__ = ${JSON.stringify({ timestamp }).replace(/</g, '\\u003c')};
</script>
`;
global.timestamp = timestamp;
不同渲染环境下,皆采用全局变量绑定的方式传递状态,初步同构应用出炉,暂时无视细节。为了达成 SSR
目标,需要将非纯数据源全部外置,React
深度绑定 redux
等状态管理工具,可能是为了 SSR
付出的必要成本。
Email: hjj491229492@hotmail.com