2 React前端工程架构 - 开发环境实时更新
Wed, Sep 19, 2018
dev server & hot load相关commit
服务端实时渲染相关commit
0x0 问题 & 解决方案概述
问题一:每次client代码改动,都需要手动重新编译打包,导致开发效率低下
工具:webpack dev server
- 通过webpack配置,启动一个资源server
- 服务于:css js html等静态资源文件
- 作用:可以通过该server访问到这些资源
- 原理:
- 文件在编译的过程中,会存在server的内存中
- 每当文件有变化,会自动执行编译过程
- 刷新浏览器就能看到效果
问题二:每次代码变动,都需要刷新浏览器才能看到效果,开发效率低下
工具:hot module replacement
- 代码变动,可以在页面上无刷新的立刻看到效果
- 保持当前状态,不用重新获取数据
0x1 动态编译
1 功能启动
安装webpack-dev-server
npm i webpack-dev-server -D |
修改文件
webpack.config.client.js
... const isDev = process.env.NODE_ENV === "development"; ... const config = { ... mode: 'production' }; if (isDev) { config.devServer = { host: '0.0.0.0', port: '8787', contentBase: path.join(__dirname, '../dist'), hot: false, overlay: { error: true }, publicPath: '/public', historyApiFallback: { index: '/public/index.html' }, } } module.exports = config; |
说明:
- 通过启动webpack的时,传入的参数,判断当前环境
- 若为dev,则配置devserver
- contentBase应该和楼上的output的path一致
- hot:是否启动hot module replacement
- 因为还没做相关配置,暂时关掉
- overlay:发生哪些情况,需要在浏览器弹窗提示
- publicPath:把静态资源自动挂载到publicPath路径下
- 应该和config中的publicPath对应
- historyApiFallback:history中可以配置很多对应关系
- 配置之后,我们所有无法返回的404的内容,都会返回给浏览器这个index.html文件
- 【注意】跑dev服务器之前,需要干掉之前本地编译生成的/dist目录
- dev server会检测硬盘上有木有dist这个目录,有的话直接访问这个目录下的js文件,没办法动态编译了
配置服务器启动方法
{ ... "scripts": { ... "dev:client": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.client.js", ... }, |
说明:
- cross-env:一个抹平操作系统差异的包,需要安装它 `npm i cross-env -D`
效果:
- 修改静态资源相关代码后,浏览器会自动刷新,展现新内容出来 - 再也不用`npm run build:client`了
0x2 热更新
配置好dev server之后,可以实现「自动编译」功能
热更新希望达成的效果,是「不用刷新页面」展示修改后的代码
1 配置
.babelrc
{ ... "plugins": [ "react-hot-loader/babel" ] } |
安装
@next表示是测试版,还没发布到主包
npm i react-hot-loader@next -D |
重新组织app.js
... import { AppContainer } from 'react-hot-loader' const root = document.getElementById("root") const render = (Component) => { ReactDOM.render( <AppContainer> <Component /> </AppContainer> , root ) }; render(App); if (module.hot) { module.hot.accept('./App.jsx', () => { const NextApp = require('./App.jsx').default; render(NextApp) }) } |
说明:
- 自定义了一个render方法,用来实现,把组件挂载到id为root的标签中
- AppContainer:实现hot load必须要用这个标签包裹整个App才行
- if语句部分
- 逻辑:当需要热更新的时候,需要重新渲染整个App
基本功能完成,但是有很多warning,而且刷新很慢,下面为优化方法 - 设置mode
webpack.config.client.js
... if (isDev) { config.entry = { app: [ 'react-hot-loader/patch', path.join(__dirname, '../client/app.js') ] }; config.devServer = { ... hot: true, ... }; config.mode = 'development'; config.plugins.push(new webpack.HotModuleReplacementPlugin()) } ... |
说明:
- webpack的entry可以是一个数组,代表这个entry里面包含了很多引用文件,需要打包到同一个文件中去
- 这里是要把hot-loader的patch打到app.js中
- 以实现hot load功能
- 需要打开hot选项
- 需要把模式设置为development,否则webpack会默认当前为生产环境,导致hot load的时候异常缓慢
- 需要把hotmodule插件,配置到plugins里
0x3 实时服务端渲染
开发过程中,也有「实时服务端渲染」需求
服务端渲染:将实时编译好 & 存在dev server中的「服务端js-bundle」,和dev server中的index.html结合起来,返回最终内容
server.js:渲染服务器,需要区分是否为开发环境
const express = require('express'); const ReactSSR = require('react-dom/server'); const fs = require('fs'); const path = require('path'); const isDev = process.env.NODE_ENV === "development"; const app = express(); if (!isDev) { const template = fs.readFileSync(path.join(__dirname, '../dist/index.html'), 'utf-8'); const serverEntry = require('../dist/server-entry.js').default; app.use('/public', express.static(path.join(__dirname, '../dist'))); app.get('*', function (req, res) { const appString = ReactSSR.renderToString(serverEntry); res.send(template.replace('<!--app-->', appString)) }); } else { const devStatic = require('./util/dev-static'); devStatic(app); } app.listen(2333, function () { console.log('🚀 服务起来了,正在监听2333端口(✌゚∀゚)☞') }); |
server/util/dev-static.js:用来实现server的dev环境下相关逻辑
part I:获取index.html
const axios = require('axios'); const getTemplate = () => { return new Promise((resolve, reject) => { axios.get('http://localhost:8787/public/index.html') .then((res) => { resolve(res.data) }) .catch(reject) }) }; |
需要返回渲染完成之后的内容。我们知道,服务端渲染需要两个东东:
- 编译好的server端jsbundle
- index.html(注入了client端jsbundle的)
- 问题:开发环境下,index.html存在dev server中,不在dist目录下(没有dist目录)
- 思路:通过http请求,向dev server要这个文件
- 工具:axios(nodejs下做http请求的包)
- 安装:`npm i axios -S`
part II:获取编译好的server-jsbundle
... part I ... const webpack = require('webpack'); const path = require('path'); const serverConfig = require('../../build/webpack.config.server'); // #1 const serverCompiler = webpack(serverConfig); // #2 const MemoryFs = require('memory-fs'); const mfs = new MemoryFs; serverCompiler.outputFileSystem = mfs; // #4 const Module = module.constructor; let serverBundle; // #5 serverCompiler.watch({}, (err, stats) => { if (err) throw err; stats = stats.toJson(); stats.errors.forEach(err => console.error(err)); stats.warnings.forEach(warn => console.warn(warn)); const bundlePath = path.join(serverConfig.output.path, serverConfig.output.filename) // #3 const serverBundleString = mfs.readFileSync(bundlePath, 'utf-8'); // #4 const m = new Module(); m._compile(serverBundleString, 'server-entry.js'); serverBundle = m.exports.default; // #5 }); const ReactDomServer = require('react-dom/server'); // #6 module.exports = function (app) { app.get('*', function (req, res) { getTemplate().then((index) => { const appString = ReactDomServer.renderToString(serverBundle); res.send(index.replace('<!--app-->', appString)); }) }) }; |
说明:
- 拿到webpack的server配置文件
- 通过配置,启动一个webpack的编译器
- 可以监听entry下的文件是否有变化,一旦变化会重新打包
- 这一部分,为啥能这样做呢?
- 因为webpack本身就提供了一个在nodejs中作为模块调用的方式
- 它不仅仅是一个命令行打包工具
- 还让我们在服务端渲染的时候,能拿到它打包出来的内容,用于服务端渲染
- 拿到打出来的jsbundle的Path
- 让文件的读写都在内存中执行,而非Disk,以提高速度
- 工具:memory-fs
- 安装:`npm i memory-fs -D`
- 注意:读取出来的是编译好的string,而非可执行的module,需要把string构建成module才行
- 提示:MemoryFs和nodejs中disk FS的api是一致的,只不过实现做了替换
- 输出到内存而不是硬盘
- string构建module
- 拿到当前module的构造器
- 用构造器把string构造成真正的module
- 使用这个api的时候,需要指定它的名字
- 指定文件名,不然无法在缓存中存储这部分内容
- 指定文件名后,下次读这部分内容,才能拿到它
- 获得编译好的bundle
- 注意:需要先exports出来
- 这个模块是通过exports挂载从模块里扔出来的东西的,所以想要获取东西,一定需要先exports
- 使用react-dom/server,把拿到的index.html和server-js-bundle渲染成最终html
配置npm启动脚本
package.json
{ ... "dev:web:start": "cross-env NODE_ENV=development node server/server.js" ... } |
问题:生成的html文件中,引入的js文件不能正确请求 - 请求返回值是index.html,而不是我们需要的js文件真正内容
解决方案:
- 为了让「静态资源文件」可以正确请求到,需要使用中间件
- 开发环境下,客户端的js,都是在webpack dev server存储的,通过一个http服务请求到的
- 我们需要通过代理,把静态文件全部代理到webpack dev server服务,从里面拿静态资源
server.js 修饰
... module.exports = function (app) { const proxy = require('http-proxy-middleware'); app.use('/public', proxy({target: 'http://localhost:8787'})); ... }; |
说明:
- 安装:`npm i http-proxy-middleware -D`
- 指定/public下文件,使用proxy做转发
启动
- 需要先启动dev:client,再启动dev:web:start
warning相关参考文档: