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相关参考文档: