[微信]前端版本过低引导弹窗方案分享
前端
原文
https://mp.weixin.qq.com/s/PT0PZ3S1Cvh2nltcIKwa3g
方案
弹窗内容
弹窗的触发条件
本地版本号和云端版本号。本地版本号是用户请求到的前端页面的代码版本号,是用户访问页面时决定;云端版本号可以理解为最新前端版本号,它是每次开发者发布前端代码时决定的。
flowchart LR 1["打开网页"] 2["获取本地和云端版本号"] 3["是否相同"] 4["不做操作"] 5["提示更新"] 1 --> 2 2 --> 3 3 -->|是| 4 3 -->|否| 5
解决方案
版本号的生成
本地版本号是用户访问时决定的,那无疑页面的 html 文件就是这个版本号存在的最佳载体,我们可以在打包时通过 plugin 给 html 文件注入一个版本号。
云端版本号
云端版本号的选择则有很多方式了,数据库、cdn 等等都可以满足需求。不过考虑到实现成本和泳道的情况,想了一下两个思路一个是打包的同时生成一个 version.json 文件,配一个路由去访问;另一个是直接访问对应的 html 代码,解析出注入的版本号,二者各自有适合的场景。
微前端的适配
我们现在的大多数项目都包含了主应用和子应用,那其实不管是子应用的更新还是主应用的更新都应该有相关的提示,而且相互独立,但同时又需要保证弹窗不同时出现。
想要沿用之前的方案其实只需要解决三个问题。
- 主子应用的本地版本号标识需要有区分,因为 html 文件只有一个,需要能在 html 文件中区分出哪个应用的版本是什么,这个我们只需在 plugin 中注入标识即可解决。
- 云端版本号请求时也要请求对应的云端版本号,这个目前采用的方案是主应用去请求唯一的 version.json 文件,因为主应用路由是唯一的,子应用则去请求最新的 html 资源文件,解析出云端版本号。
- 不重复弹窗我们只需要在展示弹窗前,多加一个是否已经有弹窗展示的判断即可了。
实现
监听时机和频控逻辑
代码
vue2 cli4.x
// npm
npm install --save-dev webpack-shell-plugin
// vue.config.js
const WebpackShellPlugin = require('webpack-shell-plugin');
module.exports = {
configureWebpack: {
plugins: [
new WebpackShellPlugin({
onBuildEnd: ['node scripts/createVersion.js']
})
]
}
};
const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio');
const packageJson = require('../package.json');
class VersionPlugin {
constructor(params) {
this._version = new Date().getTime(); // 版本时间
this._name = params?.name || 'dist'; // 打包目录
this._subName = params?.subName || packageJson.name // 前缀
}
apply(){
try {
const filePath = path.resolve(`./${this._name}/version.json`);
// 1. version.json
fs.writeFileSync(filePath, JSON.stringify({
version: this._version,
}, null, 2));
// 2. index.html
const htmlPath = path.resolve(`./${this._name}/view.html`);
const data = fs.readFileSync(htmlPath);
const $ = cheerio.load(data);
$('body').append(`<div id="${this._subName}-versionTag" style="display: none">${this._version}</div>`);
fs.writeFileSync(htmlPath, $.html());
}catch (err) {
console.log(err);
}
}
}
new VersionPlugin().apply()
弹窗
<template>
<div>
</div>
</template>
<script>
import moment from 'moment'
export default {
name: 'checkVersion',
data(){
return {
expireMinutes: 1,
timer: null,
}
},
methods: {
visibleFn(e) {
if(document.visibilityState === 'visible'){
this.fetchVersion();
}
},
compireVersion(latestVersion, currentVersion){
console.log(latestVersion, currentVersion)
try {
if (latestVersion && currentVersion && latestVersion > currentVersion) {
// 提醒过了就设置一个更新提示过期时间,一天内不需要再提示了,弹窗过期时间暂时全局只需要一个!!
localStorage.setItem(`versionUpdateExpireTime`, moment().endOf('day').format('x'));
this.notify()
}
// 设置下次比较时间
localStorage.setItem(`versionInfoExpireTime`, moment().add(this.expireMinutes, 'minutes').format('x'));
}catch (e) {
console.log(e)
}
},
routerFn(e) {
console.log('popstate', e)
},
notify(){
this.$notify({
title: '版本更新提示',
dangerouslyUseHTMLString: true,
position: 'bottom-right',
message: `您已经长时间未使用此页面,在此期间平台有过更新,如您此时在页面中没有填写相关信息等操作,请点击刷新页面使用最新版本! <br/>
<div style="display: flex; justify-content: space-between">
<div></div>
<button type="button" class="el-button el-button--primary el-button--mini" onclick="refresh()">
刷新
</button>
</div>
`,
});
},
fetchVersion(){
/**
*
* @desc 为了防止打扰,版本更新每个应用 10分钟提示
*/
// if (Number(localStorage.getItem(`versionUpdateExpireTime`) || 0) >= new Date().getTime()) {
// return;
// }
/**
* @desc 不需要每次切换页面都去判断资源,每次从服务器获取到的版本信息,给半个小时的缓存时间,需要区分子应用
*/
if (Number(localStorage.getItem(`versionInfoExpireTime`) || 0) > new Date().getTime()) {
return;
}
const dom = document.getElementById('versionTag');
if(dom){
const currentVersion = Number(dom.innerText) || undefined;
fetch(`/version.json?timestamp=${new Date().getTime()}`).then(res=>{
if(res.status==200){
return res.json();
}
}).then(res=>{
if(Reflect.has(res, 'version')){
const latestVersion = res.version;
this.compireVersion(latestVersion, currentVersion);
}
})
}
},
},
destroyed() {
document.removeEventListener('visibilitychange', this.visibleFn);
window.removeEventListener('popstate', this.routerFn);
if(this.timer){
window.clearInterval(this.timer)
}
},
mounted() {
this.fetchVersion();
window.refresh = ()=>{
window.parent.location.reload();
}
this.timer = setInterval(()=>{
this.fetchVersion();
}, 1000*60*this.expireMinutes)
document.addEventListener('visibilitychange', this.visibleFn);
window.addEventListener('popstate', this.routerFn);
},
}
</script>
vue3 vite4.x
import cheerio from 'cheerio';
import fs from 'fs';
import path from 'path';
interface OptionProps {
outDir?: string;
version?: string;
}
export default (option: OptionProps) => {
let {
outDir = 'dist',
version = new Date().getTime(),
} = option ?? {}
return {
name: 'vate-plugin-add-version',
transformIndexHtml(html: string) {
const $ = cheerio.load(html);
$('body').append(`<div id="versionTag" style="display: none">${version}</div>`);
return $.html();
},
writeBundle() {
const versionData = {
version
};
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
fs.writeFileSync(path.join(outDir, 'version.json'), JSON.stringify(versionData, null, 2));
},
}
}
// vite.config.ts
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
createVersion({
outDir: 'dist',
})],
build: {
outDir: 'dist'
},
server: {
port: 3000,
host: "0.0.0.0"
},
})
调试
发布成功后,可以根据如下步骤测试
- 删除 localstorage 中相关的 value
- 修改 html 中的 version,改成一个比较小的数值即可
- 切换路由,或者隐藏/打开页面,出现弹窗