[关闭]
@bornkiller 2017-08-19T17:08:47.000000Z 字数 4072 阅读 2668

React SSR 预研


React


前言

笔者负责开发维护的项目,主要属于内部管理控制台项目,对服务端直出并无深入了解。近日开始调研,带来个人视角的 React SSR,仅讨论具体形态,不赘述优劣。

API 初探

万事开头难,对于 SSR 渲染,依旧使用 HelloWorld 套路。案例不包含异步渲染,数据拉取,路由等等
因素。

  1. /**
  2. * @description - React SSR HelloWorld
  3. * @author - huang.jian <hjj491229492@hotmail.com>
  4. */
  5. import React, { Component } from 'react';
  6. export default class HelloWorld extends Component {
  7. constructor(props) {
  8. super(props);
  9. this.state = {
  10. title: 'Hello world!!!',
  11. description: 'React ssr practice!!!'
  12. };
  13. }
  14. render() {
  15. return (
  16. <article className="ssr-operation">
  17. <h3>{this.state.title}</h3>
  18. <p>{this.state.description}</p>
  19. </article>
  20. );
  21. }
  22. }
  1. /**
  2. * @description - SSR HelloWorld case
  3. * @author - huang.jian <hjj491229492@hotmail.com>
  4. */
  5. // External
  6. import React from 'react';
  7. import { renderToString } from 'react-dom/server';
  8. // Internal
  9. import HelloWorld from '../src/component/HelloWorld/';
  10. // Scope
  11. const RootElement = React.createElement(HelloWorld);
  12. const ssr = renderToString(RootElement);
  13. console.log(ssr);

Node 环境下上述代码无法直接运行,需要借助 Webpack 进行转译。此处并不接入 HTTP 服务器,只进行 Node 环境渲染,react-dom 提供 renderToStringrenderToStaticMarkup 渲染模式,差异在于是否提供辅助标记。下文所指服务端渲染,皆为 renderToString 渲染方式。

输出结果如下:

  1. <!-- renderToStaticMarkup -->
  2. <article class="ssr-operation">
  3. <h3>Hello world!!!</h3>
  4. <p>React ssr practice!!!</p>
  5. </article>
  6. <!-- renderToStaticMarkup -->
  7. <!-- renderToString -->
  8. <article
  9. class="ssr-operation"
  10. data-reactroot=""
  11. data-reactid="1"
  12. data-react-checksum="1848063430"
  13. >
  14. <h3 data-reactid="2">Hello world!!!</h3>
  15. <p data-reactid="3">React ssr practice!!!</p>
  16. </article>
  17. <!-- renderToString -->

开发 web server 暂不过多讨论,将渲染结果装入 html 模板作为相应内容即可,示例使用极简模式。

  1. function interpolate(body, title) {
  2. return `
  3. <!DOCTYPE html>
  4. <html lang="en">
  5. <head>
  6. <meta charset="UTF-8">
  7. <title>${title || 'React SSR'}</title>
  8. </head>
  9. <body>
  10. <section class="bootstrap">
  11. ${body}
  12. </section>
  13. </body>
  14. </html>
  15. `;
  16. }

Webpack 配置

webpack 配置单独成块,其设计以浏览器端编译为主,编译 SSR 环境,需要做部分适配。

Externals

Node 环境下,第三方模块无需单独加载,所以全部外置,提升效率。

  1. // External
  2. const NodeExternals = require('webpack-node-externals');
  3. module.exports = {
  4. // ...
  5. externals: NodeExternals()
  6. // ...
  7. };

Node

环境无需提供冗余 polyfill,禁用模块,变量等 polyfill。部分参数在 target: 'node' 时自动生效,自行决定配置。

  1. module.exports = {
  2. node: {
  3. global: false,
  4. process: false,
  5. crypto: false,
  6. Buffer: false,
  7. fs: false,
  8. net: false,
  9. tls: false,
  10. __dirname: false,
  11. __filename: false
  12. }
  13. };

Loader

应用中 import 图片,HTMLCSS 等文件,会使用 file-loaderstyle-loader 等 进行预处理,此处有以下问题:

file-loader 支持 emitFile 参数,不会重复生成文件。css, scss 等文件分两种情况讨论,如果不使用 css modules,基本上不会影响到渲染结果,直接忽略。如果使用 css modules,配置 css-loader 仅生成 mappings,不重复传递代码即可。

  1. module.exports = {
  2. module: {
  3. rules: [
  4. {
  5. test: /\.css$/,
  6. exclude: /node_modules/,
  7. use: [
  8. {
  9. loader: 'css-loader/locals',
  10. options: {
  11. root: path.resolve(process.cwd(), 'src'),
  12. modules: true,
  13. camelCase: 'only',
  14. localIdentName: '[name]__[local]___[hash:base64:5]'
  15. }
  16. },
  17. { loader: 'postcss-loader' }
  18. ]
  19. },
  20. // Ignore scss files, which never reflect render markup
  21. {
  22. test: /\.scss$/,
  23. exclude: /node_modules/,
  24. use: [
  25. { loader: 'ignore-loader' }
  26. ]
  27. },
  28. {
  29. test: /\.(png|jpe?g|gif|mp3|woff|woff2|ttf|eot|svg)(\?.*)?$/,
  30. use: [
  31. {
  32. loader: 'file-loader',
  33. options: {
  34. name: 'asset/[name].[ext]',
  35. // Don't emit file again
  36. emitFile: false
  37. }
  38. }
  39. ]
  40. }
  41. ]
  42. }
  43. };

Web Server

笔者并未参与后端开发,此块了解不多,仅做简单探讨。前端代码跟后端代码同一仓库维护,统一交由 webpack 编译后执行是否合适?印象之中,后端开发包含日志,监控,数据库等各种模块,与前端代码强行糅合是否合适?下文暂定方案为前者。

进程管理

此处讨论开发阶段进程管理。web server 开发与前端代码严重耦合,改动前端代码,需要打包 static web 内容,改动 web server 代码需要重启服务。目前笔者基于 webpack 二次包装,定制公司内部使用的脚手架工具 coco,利用 webpack watch 机制与 nodemon 简单结合,实现代码变更到重启服务的流程。

脚本配置如下:

  1. {
  2. "scripts": {
  3. "dev:client": "coco server --react --bootstrap ./src/main.jsx",
  4. "dev:ssr": "coco ssr --watch --server --bootstrap ./server/main.jsx"
  5. }
  6. }

状态同步

SSR 需要考虑与 client 渲染的状态同步,如果后端渲染的初始状态与浏览器端渲染初始状态不一致,代码抛出报错。修改上述示例如下:

  1. this.state = {
  2. title: 'Hello world!!!',
  3. description: 'React ssr practice!!!',
  4. // Impure function
  5. timestamp: Date.now()
  6. };

image.png-46.4kB

由于 Date.now 为非纯函数,服务端渲染与客户端渲染初始状态不一致,校验无法通过。需要其他方式,保证状态同步,调整如下:

  1. this.state = {
  2. title: 'Hello world!!!',
  3. description: 'React ssr practice!!!',
  4. // eslint-disable-next-line
  5. timestamp: typeof window === 'undefined' ?
  6. global.timestamp : (window.__HELLO_STATE__.timestamp)
  7. };
  1. const timestamp = Date.now();
  2. const HelloState = `
  3. <script>
  4. window.__HELLO_STATE__ = ${JSON.stringify({ timestamp }).replace(/</g, '\\u003c')};
  5. </script>
  6. `;
  7. global.timestamp = timestamp;

不同渲染环境下,皆采用全局变量绑定的方式传递状态,初步同构应用出炉,暂时无视细节。为了达成 SSR 目标,需要将非纯数据源全部外置,React 深度绑定 redux 等状态管理工具,可能是为了 SSR 付出的必要成本。

Contact

Email: hjj491229492@hotmail.com

qrcode_for_gh_d8efb59259e2_344.jpg-8.7kB

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注