前端部署通知用户刷新网页
业务场景
前端打包后,部署上线,用户有可能在部署前是停留在前版本页面上,不清楚网页重新部署了,点击其他菜单时,有时候 JavaScript 中 hash 变了导致报错,无法跳转至新的页面。
考虑方案
通过每次发版更新
version
,前端轮询version
判断是否已更新版本根据打包完生成的
script hash
值轮询对比判断是否一样;不一样,则更新了版本
第一种方案
考虑每次打包自动生成 version
文件,版本号根据打包时间撮生成。
自动写入 version.json 文件
新建 webpack
插件 version-plugin.js
js
const fs = require('fs');
const path = require('path');
module.exports = class VersionPlugin {
constructor({ rootDir = 'public', ...options }) {
this.options = options;
this.rootDir = rootDir;
}
writeVersionFile() {
const version = {
version: new Date().getTime(),
...this.options
};
const data = `${JSON.stringify(version)}`;
fs.writeFileSync(
path.resolve(__dirname, `../${this.rootDir}/version.json`),
data
);
}
apply() {
this.writeVersionFile();
}
};
在项目中添加相关插件
js
configureWebpack: config => {
config.plugins.push(
new VersionPlugin({
tag: 0.01
})
);
},
当编译打包时,会在默认 public
文件夹内自动生成 version.json
文件
json
{"version":1673330288607,"tag":0.01}
创建通知更新逻辑
查看代码
js
/**
* 通知更新版本
*/
import { http } from './request';
class NoticeVersion {
oldVersion = '';
newVersion = '';
dispatch = {};
constructor({ timer }) {
this.oldVersion = '';
this.newVersion = '';
this.dispatch = {};
this.polling = null;
this.init();
this.timering(timer);
}
async init() {
const { version } = await this.getVersion();
this.oldVersion = version;
}
async getVersion() {
try {
const uri = process.env.VUE_APP_PUBLIC_PATH + 'version.json';
const { data } = await http.get(uri);
return data;
} catch (error) {
console.log('获取版本信息错误', JSON.stringify(error));
this.clear();
}
}
on(key, func) {
if (!this.dispatch[key]) {
this.dispatch[key] = [];
}
this.dispatch[key].push(func);
return this;
}
compare() {
if (this.oldVersion != this.newVersion) {
this.dispatch['update'].forEach((func) => {
func();
});
} else {
this.dispatch['no-update'].forEach((func) => {
func();
});
}
}
timering(timer = 1000) {
this.polling = setInterval(async () => {
const { version } = await this.getVersion();
this.newVersion = version;
this.compare();
}, timer);
}
clear() {
if (!this.polling) {
clearInterval(this.polling);
}
this.polling = null;
}
}
export default {
install() {
//实例化该类
const up = new NoticeVersion({
timer: 2000,
});
//未更新通知
up.on('no-update', () => {
console.log('未更新');
});
//更新通知
up.on('update', () => {
console.log('更新了');
});
},
};
第二种方案
第二种其实和第一种差不多,不过不需要 webpack
插件,只需要获取 html
页面的 script
js
export class NoticeVersion {
oldScript = []
newScript = []
dispatch = {}
constructor(options) {
this.oldScript = [];
this.newScript = []
this.dispatch = {}
this.init()
this.timering(options?.timer) // 轮询
}
async init() {
const html = await this.getHtml()
this.oldScript = this.parserScript(html)
}
async getHtml() {
const html = await fetch('/').then(res => res.text()); // 读取index html
return html
}
parserScript(html) {
const reg = new RegExp(/<script(?:\s+[^>]*)?>(.*?)<\/script\s*>/ig)
return html.match(reg) // 匹配script标签
}
//发布订阅通知
on(key, func) {
if (!this.dispatch[key]) {
this.dispatch[key] = [];
}
this.dispatch[key].push(func);
return this;
}
compare(oldArr, newArr) {
const base = oldArr.length
const arr = Array.from(new Set(oldArr.concat(newArr)))
// 如果新旧length 一样无更新
if (arr.length === base) {
this.dispatch['no-update'].forEach(fn => {
fn()
})
} else {
// 否则通知更新
this.dispatch['update'].forEach(fn => {
fn()
})
}
}
timering(time = 10000) {
//轮询
setInterval(async () => {
const newHtml = await this.getHtml()
this.newScript = this.parserScript(newHtml)
this.compare(this.oldScript, this.newScript)
}, time)
}
}
第三种方案
或者直接使用 Vue.config.errorHandler
错误捕获了某种错误直接强制刷新页面
js
Vue.config.errorHandler = (err, vm, info) => {
console.log(err)
let isNeedRefreshErr = false;
if (err.message.indexOf('Failed to fetch dynamically imported module') > -1 || err.message.indexOf('\'text/html\' is not a valid JavaScript MI') > -1) {
isNeedRefreshErr = true
}
if (err.message && isNeedRefreshErr) {
// TODO
}
}
去掉 HTML 缓存
在 HTML
中 meta
上设置不缓存
HTML
<meta http-equiv="Pragma" content="no-cache" /> // 旧协议
<meta http-equiv="Expires" content="0" /> // 旧协议
<meta http-equiv="Cache-Control" content="no-cache" /> // 目前主流
nginx
配置禁用 HTML
缓存
conf
location / {
root **;
# 配置页面不缓存html和htm结尾的文件
if ($request_filename ~* .*.(?:htm|html)$)
{
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
}
index index.html index.htm;
}
Private
会影响到CDN
缓存命中率,但本身CDN
缓存也会存在版本问题,量不大的情况下也可以禁掉No-cache
可以使用缓存,但是使用前必须到服务端验证,可以是no-cache
,也可以是max-age=0
No-store
完全禁用缓存Must-revalidate
与no-cache
类似,强制到服务端验证,作用于一些特殊场景,例如发送了校验请求,但发送失败了之类Proxy-revalidate
与上面类似,要求代理缓存服务验证有效性
总结
推荐使用第一种版本号来控制,可以生成其他版本信息,更清晰查看当前版本描述。
也可以使用 plugin-web-update-notification 插件。