一键式启动项目与热更新
# 痛点
在经受完前面一堆代码的洗礼之后,相信读者或许感受到启动项目的不方便了,首先先得将业务代码按照两份配置构建一遍,接着再来启动服务,这让我们在调试过程中十分不友好。
不仅如此,我们在修改了前端页面代码之后,我们还得重新构建一遍,然后再启动项目,即便是只添加了一个字,我们都需要重复走一遍这个逻辑,是不是十分的难受。
# 需求
针对上述问题,这里提出几点优化目标:
- 首先我们能否一键自动构建并启动项目,解决繁琐的启动流程。
- 其次,是否能实现热更新能力,页面修改内容之后能无刷新更新页面。
- 在具备了前端代码热更新能力之后,我们修改了
webpack
相关配置文件是否也能自动重新构建并重启服务。
# 优化
针对上述优化目标,这里也将逐一进行解决。
为了实现一键启动项目并实时打包构建,这里需要修改一下服务端代码,集成webpack
打包能力,话不多说,先上码:
# 完整代码
const { createBundleRenderer } = require('vue-server-renderer');
const express = require('express');
const app = express();
const router = express.Router();
const chalk = require('chalk');
const fs = require('fs');
const template = fs.readFileSync('./index.html', 'utf-8')
const path = require('path');
let clientManifest = require(path.resolve(__dirname, 'dist', 'vue-ssr-client-manifest.json'));
let serverBundle = require(path.resolve(__dirname, 'dist', 'vue-ssr-server-bundle.json'));
// ------可以从这开始看
const webpack = require('webpack');
const WebpackDevMiddleware = require('webpack-dev-middleware');
const WebpackHotMiddleware = require('webpack-hot-middleware');
const clientWebpackConfig = require('./build/webpack.client.config');
const serverWebpackConfig = require('./build/webpack.server.config')
const clientCompiler = webpack(clientWebpackConfig);
const serverCompiler = webpack(serverWebpackConfig);
let renderer = {};
let buildCount = 0;
// 客户端构建
const clientMiddleware = WebpackDevMiddleware(clientCompiler)
app.use(clientMiddleware);
clientCompiler.hooks.done.tap('compilerDone', () => {
console.log('客户端构建完成')
clientManifest = JSON.parse(clientMiddleware.context.outputFileSystem.readFileSync(path.join(clientWebpackConfig.output.path, 'vue-ssr-client-manifest.json')).toString())
runBuildRenderer();
})
// 服务端构建
const serverMiddleware = WebpackDevMiddleware(serverCompiler)
app.use(serverMiddleware);
serverCompiler.hooks.done.tap('compilerDone', () => {
console.log('服务端构建完成')
serverBundle = JSON.parse(serverMiddleware.context.outputFileSystem.readFileSync(path.join(clientWebpackConfig.output.path, 'vue-ssr-server-bundle.json')).toString())
runBuildRenderer();
})
// 判断并重新创建新的renderer
function runBuildRenderer(init = false) {
buildCount++;
if (!init && buildCount < 2) return;
if (clientManifest && serverBundle) {
buildCount = 0;
console.log('新的renderer已产生')
renderer = createBundleRenderer(serverBundle, {
template,
clientManifest,
runInNewContext: false
})
}
}
// 热更新中间件
app.use(WebpackHotMiddleware(clientCompiler, { log: false }));
runBuildRenderer(true);
// ------到这
app.use(express.static(path.resolve(__dirname, './dist')))
router.get('*', (req, res) => {
const context = { url: req.url };
renderer.renderToString(context, (err, html) => {
if (err) res.send(err);
res.send(html);
})
})
app.use(router);
app.listen(3000, function() {
console.log(
'App runing at:',
`Local: ${ chalk.blueBright.underline('http://localhost:3000') }`
)
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
大家看到这么老长一段代码不要慌张,且听笔者为你细细道来。
# 逻辑分析
这里关键部分就在上述笔者标记的代码段中间,首先这里会涉及到三个库:
webpack
:用于构建。webpack-dev-middleware
:用于配合webpack
将构建好的文件保存在内存中,而不是写入到文件。webpack-hot-middleware
:用于配合实现热更新。
# webpack构建部分
const webpack = require('webpack');
const WebpackDevMiddleware = require('webpack-dev-middleware');
const clientWebpackConfig = require('./build/webpack.client.config');
const serverWebpackConfig = require('./build/webpack.server.config')
const clientCompiler = webpack(clientWebpackConfig);
const serverCompiler = webpack(serverWebpackConfig);
const clientMiddleware = WebpackDevMiddleware(clientCompiler)
const serverMiddleware = WebpackDevMiddleware(serverCompiler)
2
3
4
5
6
7
8
这里主要是服务端进行webpack
打包,配合webpack-dev-middleware
,实现对构建完毕资源的访问。
从代码上看,首先引入webpack
与webpack-dev-middleware
包,接着导入客户端构建与服务端构建这两份webpack
配置文件用于生成compiler
,然后使用webpack-dev-middleware
,实现将构建代码输出到内存中便于访问。
这里由于构建之后的内容都输出到了内存中,而在生成renderer
的部分需要客户端构建manifest
文件和服务端构建的bundle
文件,所以我们得想办法在它们构建完之后触发重新生成renderer
,以便于实现修改webpack
配置文件也能顺利生成新的renderer
。
所以呢,鉴于以上问题,我们需要去监听一下webpack
构建完成的事件,也就是当它构建完之后就重新执行生成renderer
逻辑。
# 客户端构建部分
// 客户端构建
clientCompiler.hooks.done.tap('compilerDone', () => {
console.log('客户端构建完成')
clientManifest = JSON.parse(clientMiddleware.context.outputFileSystem.readFileSync(path.join(clientWebpackConfig.output.path, 'vue-ssr-client-manifest.json')).toString())
runBuildRenderer();
})
2
3
4
5
6
这里我们定义了一个全局变量
clientManifest
,用于每次构建都能被实时访问到。
这里我们就通过生成的客户端构建实例来注册一个编译完成的事件,注册完之后,webpack
内部就会帮我们调用这个回调函数,同时我们这个时候在回调函数中就能访问到构建好的文件了,具体实现如上。在读取完对应文件之后,我们重新执行创建renderer
函数,就能更新renderer
了。
这个
runBuildRenderer
函数放在后面分析,不要慌。
# 服务端构建部分
同理,跟客户端构建类似。
// 服务端构建
serverCompiler.hooks.done.tap('compilerDone', () => {
console.log('服务端构建完成')
serverBundle = JSON.parse(serverMiddleware.context.outputFileSystem.readFileSync(path.join(clientWebpackConfig.output.path, 'vue-ssr-server-bundle.json')).toString())
runBuildRenderer();
})
2
3
4
5
6
我们通过观察可以发现,这里的客户端构建与服务端构建部分主要做的事情就是等webpack
构建好了之后更新相关全局变量的值,同时执行这个runBuildRenderer
函数,先从名字来看,就是用来重新生成renderer
的,那么接下来让我们看看它内部的实现吧。
# runBuildRenderer
为什么客户端部分和服务端部分都会调用这个函数,这是因为生成renderer
需要客户端与服务端构建的结果,而他们的构建又没法保证顺序,所以这里会都调用一次,然后维护一个buildCount
变量,执行一次则加一,等到被调用了两次也就是客户端构建和服务端构建都完成了再更新renderer
,并重置buildCount
为下一次做准备。
function runBuildRenderer(init = false) {
buildCount++;
if (!init && buildCount < 2) return;
if (clientManifest && serverBundle) {
buildCount = 0;
console.log('新的renderer已产生')
renderer = createBundleRenderer(serverBundle, {
template,
clientManifest,
runInNewContext: false
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
从功能上来看也是比较简单的,主要就是用来生成新的renderer
,同时区分了初始化和热更新的状态,初始化的时候我们默认直接取打包到dist
目录中的结果;如果是热更新阶段,那么就需要等clientManifest
、serverBundle
都构建好了再重新rebuild
。
当然了,初始化的时候笔者会调用一次,并给
init
参数传为true
,生成renderer
,以用于保证初始渲染能力。
# 浏览器热更新
正如我们使用vue
脚手架启动项目一样,我们也希望能够在业务代码改变之后,页面自动热更新,而不需要我手动刷新页面,这个时候就需要我们webpack-hot-middleware
来配合了。
// server.js
const WebpackHotMiddleware = require('webpack-hot-middleware');
app.use(WebpackHotMiddleware(clientCompiler, { log: false }));
2
3
我们在server.js
中也就是服务端添加一个中间件即可。
注意!还没完,我们还需要做一件事才能生效。
# 修改客户端构建webpack配置
const webpack = require('webpack')
const hotModuleScript = 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=10000&reload=true'
{
entry: [hotModuleScript, path.resolve(__dirname, '../entry-client.js')],
plugins: [new webpack.HotModuleReplacementPlugin()]
}
2
3
4
5
6
7
我们得把我们客户端构建的入口配置改一下,以数组形式,在原有的入口文件前面添加这么一段。
同时,添加一个新插件(plugin
),它是我们webpack
内置的,可以直接通过webpack.
来获取。