基于 vue-cli3 SSR 程序实现热更新功能
前沿
通过上一篇文章 通过vue-cli3构建一个SSR应用程序 我们知道了什么是 SSR,以及如何通过 vue-cli3 构建一个 SSR 应用程序。但是最后遗留了一些问题没有处理,就是没有添加开发时的热更新功能,难道要每次更新代码都要重新编译打包吗?显然不是很合理。那接下来我们将为该SSR程序添加热更新的功能。
1、解决思路
我们知道SSR程序每次打包编译完成后,都会生成这两个文件 vue-ssr-client-manifest.json 和 vue-ssr-server-bundle.json
- vue-ssr-client-manifest.json
主要记录了静态资源文件的配置信息
- vue-ssr-server-bundle.json
主要记录了js文件的内容
那现在的问题就是要解决如何在保存代码后,获取到最新的vue-ssr-client-manifest.json 和 vue-ssr-server-bundle.json这两个文件。
通过该图,我们知道,既然要热更新,那 webpack dev server 肯定跑不了。
所以解决的步骤如下:
- 起一个 webpack dev server 服务,暴露 8080 端口
- 起一个 webpack compiler 编译 webpack 配置文件,监听文件修改,实时编译获取最新的 vue-ssr-server-bundle.json
- 通过 webpack dev server 获取最新的 vue-ssr-client-manifest.json
- 结合 vue-ssr-server-bundle.json 和 vue-ssr-client-manifest.json 渲染 html 页面返回给浏览器
2、编码实现
有了思路后,剩下的就是要思考如何通过代码实现了。
2.1、 起一个 webpack dev server 服务
通过 npm run serve 我们能很快的起一个 webpack dev server 服务
npm run serve
2.2、获取 webpack 配置文件,并编译
通过阅读 官方文档 我们知道 webpack 的配置文件在 /node_modules/@vue/cli-service/webpack.config.js 中
// 1、webpack配置文件
const webpackConfig = require('@vue/cli-service/webpack.config')
2.3、编译 webpack 配置文件,并监听文件修改
// 2、编译webpack配置文件
const serverCompiler = webpack(webpackConfig)
const mfs = new MemoryFS()
// 指定输出到的内存流中
serverCompiler.outputFileSystem = mfs
// 3、监听文件修改,实时编译获取最新的 vue-ssr-server-bundle.json
let bundle
serverCompiler.watch({}, (err, stats) =>{
if (err) {
throw err
}
stats = stats.toJson()
stats.errors.forEach(error => console.error(error) )
stats.warnings.forEach( warn => console.warn(warn) )
const bundlePath = path.join(
webpackConfig.output.path,
'vue-ssr-server-bundle.json'
)
bundle = JSON.parse(mfs.readFileSync(bundlePath,'utf-8'))
console.log('new bundle generated')
})
2.4、获取最新的 vue-ssr-client-manifest.json
// 4、获取最新的 vue-ssr-client-manifest.json
// 这边的 8080 是 dev server 的端口号
const clientManifestResp = await axios.get('http://localhost:8080/vue-ssr-client-manifest.json')
const clientManifest = clientManifestResp.data
2.5、结合各个步骤的核心后的最后代码
安装所需要的库
npm install webpack memory-fs concurrently -D
npm install koa-router axios -S
在项目根目录下 新建一个 server/dev.ssr.js,代码如下
// server/dev.ssr.js
const webpack = require('webpack')
const axios = require('axios')
const MemoryFS = require('memory-fs')
const fs = require('fs')
const path = require('path')
const Router = require('koa-router')
// 1、webpack配置文件
const webpackConfig = require('@vue/cli-service/webpack.config')
const { createBundleRenderer } = require("vue-server-renderer");
// 2、编译webpack配置文件
const serverCompiler = webpack(webpackConfig)
const mfs = new MemoryFS()
// 指定输出文件到的内存流中
serverCompiler.outputFileSystem = mfs
// 3、监听文件修改,实时编译获取最新的 vue-ssr-server-bundle.json
let bundle
serverCompiler.watch({}, (err, stats) =>{
if (err) {
throw err
}
stats = stats.toJson()
stats.errors.forEach(error => console.error(error) )
stats.warnings.forEach( warn => console.warn(warn) )
const bundlePath = path.join(
webpackConfig.output.path,
'vue-ssr-server-bundle.json'
)
bundle = JSON.parse(mfs.readFileSync(bundlePath,'utf-8'))
console.log('new bundle generated')
})
// 处理请求
const handleRequest = async ctx => {
console.log('path', ctx.path)
if (!bundle) {
ctx.body = '等待webpack打包完成后在访问在访问'
return
}
// 4、获取最新的 vue-ssr-client-manifest.json
const clientManifestResp = await axios.get('http://localhost:8080/vue-ssr-client-manifest.json')
const clientManifest = clientManifestResp.data
const renderer = createBundleRenderer(bundle, {
runInNewContext: false,
template: fs.readFileSync(path.resolve(__dirname, "../src/index.temp.html"), "utf-8"),
clientManifest: clientManifest
});
const html = await renderToString(ctx,renderer)
ctx.body = html;
}
function renderToString(context,renderer) {
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
err ? reject(err) : resolve(html);
});
});
}
const router = new Router()
router.get("*", handleRequest);
module.exports = router
新建一个 server/ssr.js,代码如下
// server/ssr.js
const Koa = require('koa')
const koaStatic = require("koa-static");
const path = require('path')
const resolve = file => path.resolve(__dirname, file);
const app = new Koa()
const isDev = process.env.NODE_ENV !== 'production'
const router = isDev ? require('./dev.ssr') : require('./server')
app.use(router.routes()).use(router.allowedMethods())
// 开放目录
app.use(koaStatic(resolve("../dist")));
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`server started at localhost:${port}`);
});
module.exports = app
修改 package.json 添加几个执行脚本
// package.json scripts字段
"dev:serve": "cross-env WEBPACK_TARGET=node node ./server/ssr.js",
"dev": "concurrently \"npm run serve\" \"npm run dev:serve\" "
执行 npm run dev 命令
npm run dev
访问 localhost:3000 你会发现,还是有问题。
静态资源的文件引用的是 node.js server 的服务的,即引用到了 3000 端口上去了,但是 3000 端口的服务并没有这些静态资源文件, 这些静态资源文件在 webpack dev server 中。
那如何解决呢?
- 修改 node server 将这些静态资源的请求代理到 webpack dev server 中
- 改变 webpack 的 baseUrl,直接引用到 webpack dev server 中
很显然,第二种方式实现起来比较简单,那我们就修改 webpack 的 baseUrl 配置
修改 vue.config.js
// vue.config.js
// 添加一个字段,如果是开发环境,就指定到webpack dev server中
baseUrl: isDev ? 'http://127.0.0.1:8080' : '',
重新 npm run dev ,然后访问 localhost:3000
已经能正常访问了,那我们试试 热更新的更新能不能实现,修改一段代码,
会发现 node server 会重新编译 webpack 配置文件,然后在看看浏览器有没有更新
你会发现浏览器还是没能热更新内容,打开 F12,你会发现这个错误,
这是我们常见的不允许跨域的错误提示。
解决方式:
- 配置 webpack dev server 允许跨域
// vue.config.js
// 添加一个 devServer的字段
devServer: {
headers: {'Access-Control-Allow-Origin': '*'}
},
重新 npm run dev ,然后访问 localhost:3000
是已经能实现热更新的了。
3、优化
1、favicon 的问题 打开 f12 还是能看到有问题
具体实现可以参考我的 github代码
2、修改 server 端代码自动重启代码
可以使用 nodemon,或者 pm2 实现
4、总结
通过上一篇 通过vue-cli3构建一个SSR应用程序 和这篇文章,我们一步一步搭建起了基于 vue-cli3 的一个 ssr 应用程序,并添加了热更新的功能,在这期间也踩了很多坑。但是最终实现了之后,你会觉得这些付出都是值得的,因为这些都是为自己的成长奠定基础。
如果有更好的实现方法,欢迎交流交流!
如果有不对的地方,欢迎指出!
5、源码
项目源码:vue-cli-ssr-example 欢迎 star
感謝您寫了這麼好的文章,受益良多 😄