监控前端代码版本迭代实现页面自动刷新
背景:
当前端版本迭代较为频繁的时候,使用webpack
对项目进行打包,虽然我们对js和css文件使用了chunkhash进行了文件缓存控制,但是项目的index.html文件在版本频繁迭代更新时,会存在被浏览器缓存的情况。在发版后,用户不强制刷新页面,浏览器会使用缓存的index.html文件,从而导致向服务器端请求了上个版本chunkhash的js和css文件,最终页面404(上个版本chunkhash
的js和css在版本更新时已替换删除了)。
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
}
解决思路:
- 服务器端发版,上一个版本的代码不删掉;
- 在每次打包生产代码时,在static下生产一个version.json的版本信息文件,在前端页面实时请求服务器端的version.json中的版本号和浏览器本地缓存的version.json进行对比,从而监控版本迭代更新,实现页面自动更新,获取新的index.html文件(前提是服务器端对index.html进行不缓存配置)。
思路1
:缺点是会随着频繁发版,服务器端前端项目文件会越来越多,浪费空间,同时,旧页面的接口涉及到接口后端同学已经废弃了,引起报错;
所以现在主要以思路2
进行处理:
- 配置环境
// config/dev.env.js
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
VERSION: '""'
})
// config/prod.env.js
'use strict'
module.exports = {
NODE_ENV: '"production"', // 区分开发/生产环境
VERSION: '"v' + new Date().getTime() + '"' // 版本格式
}
- 自定义版本信息生成插件: 如何编写一个插件?
'use strict';
var FStream = require('fs');
/**
* 版本信息生成插件
* @author guoqian.xu
* @param options
* @constructor
*/
function VersionPlugin(options) {
this.options = options || {};
!this.options.versionDirectory && (this.options.versionDirectory = 'static');
}
// apply方法是必须要有的,因为当我们使用一个插件时(new somePlugins({})),webpack会去寻找插件的apply方法执行
VersionPlugin.prototype.apply = function (compiler) {
var self = this;
compiler.plugin('compile', function (params) {
// 生成版本信息文件路径
// this.options.context:项目的绝对路径
var dir_path = this.options.context + '/' + self.options.versionDirectory;
var version_file = dir_path + '/version.json';
var content = '{"version":' + self.options.env.VERSION + '}';
FStream.exists(dir_path, function (exist) {
if (exist) {
writeVersion(self, version_file, content);
return;
}
FStream.mkdir(dir_path, function (err) {
if (err) throw err;
console.log('\n创建目录[' + dir_path + ']成功');
writeVersion(self, version_file, content);
});
});
});
// 编译器对'所有任务已经完成'这个事件的监听
compiler.plugin('done', function (stats) {
console.log('应用编译完成!');
});
};
const writeVersion = (self, versionFile, content) => {
console.log('\n当前版本号:' + self.options.env.VERSION);
console.log('开始写入版本信息...');
// 写入文件
FStream.writeFile(versionFile, content, function (err) {
if (err) throw err;
console.log('版本信息写入成功!');
});
};
module.exports = VersionPlugin;
- 加载插件
// webpack.prod.config.js
// 版本信息生成
const VersionPlugin = require('./version-plugin');
...
const webpackConfig = merge(baseWebpackConfig, {
...
plugins: [
// 版本信息生成
new VersionPlugin({
path: config.build.assetsRoot,
env: env,
versionDirectory: 'static'
}),
...
]
...
});
- 选择你需要的位置进行监控(路由钩子、特定页面、接口调用拦截等)
// 版本监控
async versionCheck() {
if (NODE_ENV === 'development') return;
const response = await this.$ajax.get(`../static/version.json`);
if (VERSION !== response.data.version) {
this.$alert('发现新版本,自动更新中...', '温馨提示', {
confirmButtonText: '我知道了',
type: 'warning',
closeOnClickModal: false,
closeOnPressEscape: false,
showClose: false,
callback: action => {
window.location.reload(true);
}
});
}
}
这里说一下在路由钩子的检测实现:
// router/index.js
import Vue from 'vue';
import Router from 'vue-router';
import routeInterceptor from './hooks';
...
router.beforeEach(routeInterceptor);
// hooks/index.js
/* hooks 目录用于配置路由钩子 */
import routerViewChange from './routerViewChange';
...
const allHooks = [
/* 放置全部的路由钩子 */
routerViewChange
];
const routeInterceptor = ({ path: toPath, query: toQuery, matched }, { path: fromPath }, next) => {
// 路由拦截
if (matched.length === 0) {
// 404页面
next({ path: '/error/404' });
} else {
// 找到匹配当前路由的钩子
const hookMatched = allHooks.filter(({ path: hookPath }) => {
if (hookPath instanceof RegExp) {
const routerPathReg = hookPath;
return routerPathReg.test(toPath);
}
return hookPath === toPath;
});
const hookLen = hookMatched.length;
if (hookLen) {
let hookActived = 0;
// 匹配到路由钩子后, 触发该路由下的全部钩子函数
hookMatched.forEach((hook /* , idx */) => {
hook.action({
toPath,
fromPath,
toQuery,
next(params) {
// 使用计数器控制 保证全部路由钩子均执行完毕 (包括异步调用 next 函数)后, 才继续路由导航
if (hookActived < hookLen - 1) {
hookActived += 1;
return;
}
next(params);
}
});
});
} else {
next();
}
}
};
export default routeInterceptor;
// hooks/routerViewChange.js
import { ajax } from '@/utils';
const routerViewChange = {
path: /\/\w+?\//, // 匹配所有路由
action: async({ toPath, next }) => {
if (NODE_ENV === 'development') return;
const response = await ajax.get(`../static/version.json`);
if (VERSION !== response.data.version) {
window.confirm('发现新版本,自动更新中...');
window.location.reload(true);
}
next();
}
};
export default routerViewChange;
- 作者使用的是
nginx
,这里实现nginx
对index.html
的不缓存处理
location ~ .*\.(htm|html)?$ {
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
# 指明root,如果不在外层上指明root,需要在这里指明
root /data/server/ui/vue-project/dist/;
}
最后产出的结果: