基于Egg+React+Webpack构建流程

1. 本地Egg项目启动

构建流程 - 图1

  • 首先执行node index.js 或者 npm run dev 启动 Egg应用

  • 在 Egg Agent 里面启动koa服务, 同时在koa服务里面启动Webpack编译服务

  • 挂载Webpack内存文件读取方法覆盖本地文件读取的逻辑

  1. app.react.render = (name, locals, options) => {
  2. const filePath = path.isAbsolute(name) ? name : path.join(app.config.view.root[0], name);
  3. const promise = app.webpack.fileSystem.readWebpackMemoryFile(filePath, name);
  4. return co(function* () {
  5. const code = yield promise;
  6. if (!code) {
  7. throw new Error(`read webpack memory file[${filePath}] content is empty, please check if the file exists`);
  8. }
  9. // dynamic execute javascript
  10. const wrapper = NativeModule.wrap(code);
  11. vm.runInThisContext(wrapper)(exports, require, module, __filename, __dirname);
  12. const reactClass = module.exports;
  13. if (options && options.markup) {
  14. return Promise.resolve(app.react.renderToStaticMarkup(reactClass, locals));
  15. }
  16. return Promise.resolve(app.react.renderToString(reactClass, locals));
  17. });
  18. };
  • Worker 监听Webpack编译状态, 检测Webpack 编译是否完成, 如果未完成, 显示Webpack 编译Loading, 如果编译完成, 自动打开浏览器

  • Webpack编译完成, Agent 发送消息给Worker, Worker检测到编译完成, 自动打开浏览器, Egg服务正式可用

2. 本地构建流程

image.png

React 服务端渲染构建是需要构建两份 JSBundle 文件。SSR 模式开发时,SSR 运行需要 Webapck 单独构建 target: node 和 target: web 的JSBundle,主要的差异在于 Webpack需要处理 require 机制以及磨平 Node 和浏览器运行环境的差异。服务端的JSBundle用来生产HTML,客户端的JSBundle需要 script 到文档,用来进行事件绑定等操作,也就是 React 的 hydrate 机制。

  • 本地开发启动 Webpack 构建, 默认配置文件为项目根目录 webpack.config.js 文件。 SSR 需要配置两份 Webpack 配置,所以构建会同时启动两个 Webpack 构建服务。web 表示构建 JSBundle 给前端用,构建后文件目录 public, 默认端口 9000; node 表示构建 JSBundle 给前端用,构建后文件目录 app/view, 默认端口 9001.
  • 本地构建是 Webpack 内存构建,文件不落地磁盘,所以 app/viewpublic 在本地开发时,是看不到文件的。 只有发布模式(npm run build)才能在这两个目录中看到构建后的内容。

3. 本地服务端渲染页面访问

  • npm run dev

构建流程 - 图3

  • 浏览器输入URL请求地址, 然后 Egg 接收到请求, 然后进入 Controller

  • Node层获取数据后(Node通过http/rpc方式调用Java后端API数据接口), 进入模板render流程

  • 进入render流程后, 通过worker进程通过调用 app.messenger.sendToAgent 发送文件名给Agent进程, 同时通过 app.messenger.on 启动监听监听agent发送过来的消

  • Agent进程获取到文件名后, 从Webpack编译内存里面获取文件内容, 然后Agent 通过 agent.messenger.sendToApp 把文件内容发送给Worker进程

  • Worker进程获取到内容以后, 进行React编译HTML, 编译成HTML后, 进入jss/css资源依赖流程

  • 如果启动代理模式(见easywebpack的setProxy), HTML直接注入相对路径的JS/CSS, 如下:

页面可以直接使用 /public/client/js/vendor.js 相对路径, /public/client/js/vendor.js 由后端框架代理转发到webpack编译服务, 然后返回内容给后端框架, 这里涉及两个应用通信. 如下:

  1. <link rel="stylesheet" href="/public/client/css/home/android/home.css">
  1. <script type="text/javascript" src="/public/client/js/vendor.js"></script>
  2. <script type="text/javascript" src="/public/client/js/home.js"></script>
  • 如果非代理模式(见easywebpack的setProxy), HTML直接注入必须是绝对路径的JS/CSS, 如下:

页面必须使用 http://127.0.0.1:9000/public/client/js/vendor.js 绝对路径

  1. <link rel="stylesheet" href="http://127.0.0.1:9000/public/client/css/home/android/home.css">
  1. <script type="text/javascript" src="http://127.0.0.1:9000/public/client/js/vendor.js"></script>
  2. <script type="text/javascript" src="http://127.0.0.1:9000/public/client/js/home.js"></script>

其中 http://127.0.0.1:9000 是 Agent里面启动的Webpack编译服务地址, 与Egg应用地址是两回事

  • 最后, 模板渲染完成, 服务器输出HTML内容给浏览器.

4. 正式环境发布模式构建流程和运行模式

  • Webpack通过本地构建或者ci直接构建好服务端文件和客户端资源文件到磁盘

  • Egg render直接读取本地文件, 然后渲染成 HTML

  • 根据 manfifest.json 文件注入 jss/css资源依赖注入

  • 模板渲染完成, 服务器输出HTML内容给浏览器.

5. 关于 Egg 框架中的 Agent 和 Worker

  • 我们利用本地开发修改 Node 层代码修复重启时, 只会重启 Worker 进程, 不会重启 Agent 进程, 我们可以在Agent里面启动 Webpack 编译服务解决 Webpack compiler 实例问题.

  • 因为 Egg App 进程 和 Agent 进程是两个进程, 当url访问时, 我们通过 worke r发送消息给 agent 进程, 获取服务端渲染的文件内容, 然后Agent再发送消息给Worker解决文件读取问题.

  • 本地开发 Webpack 热更新内存存储读取和线上应用本机文件读取逻辑分离功能, 我们通过本地开发模式时, 通过读取 Webpack 内存内容覆盖本地文件读取的逻辑, 这样在开发模式和发布模式可以无缝对接.

  • worker 和 agent 通信机制: https://eggjs.org/zh-cn/core/cluster-and-ipc.html

  • 实现 egg 项目服务器代码修改项目自动重启的功能可以使用 egg-development 插件.