@bornkiller
2017-08-19T09:08:47.000000Z
字数 4072
阅读 2970
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>*/// Externalimport React from 'react';import { renderToString } from 'react-dom/server';// Internalimport HelloWorld from '../src/component/HelloWorld/';// Scopeconst 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 --><articleclass="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 环境下,第三方模块无需单独加载,所以全部外置,提升效率。
// Externalconst 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 againemitFile: 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 functiontimestamp: Date.now()};

由于 Date.now 为非纯函数,服务端渲染与客户端渲染初始状态不一致,校验无法通过。需要其他方式,保证状态同步,调整如下:
this.state = {title: 'Hello world!!!',description: 'React ssr practice!!!',// eslint-disable-next-linetimestamp: 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
