前端重新部署如何通知用户更新
需求背景
解决方案
常用的webSocket解决方案
webSocket; 大致逻辑思考应该是前端在部署好后向服务器发送一个状态变更通知;服务器接收后主动向前端push;前端通过心跳检测,接收到相关更新时弹出提示,让用户确认更新;
缺点:
- 需要服务器配合开发;
- 部署好后主动向服务器发送变更通知,但是CI/CD部署是在远端,对于前端来说完全是黑盒子;当然可以尝试去研究docker相关文档应该是有解决方案; 当然也可以选择手段变更状态,在系统哪加个入口点击后向服务器发送通知;
- 整个看下来,成本不小;
纯前端方案
要思考其实我们要的服务器能不能换成项目最终部署的服务地址呢?在build代码时,主动向生产目录塞入一个js/json文件; 里面写入相关变量;再将本地的变量与轮询查询远端该文件中变量值进行比较,如果发生变化,表示有最新版本,即弹更新提示;
- build时,用node生成一个文件如
_version.js
;里面写入window.version=${Date.now()}
; - 将
_version.js
写入到最终打包生成的index.html中;
<script src="/_version.js"></script>
3. 本地可以在入口文件
main.js
中通过window.version
读取本地的版本时间戳;然后定时器轮询获取远程的js文件中的version;将两者进行比较,如果不同,表示有更新
具体实现
为了理解以及我编写过程的方便,下文会结合已有的项目进行讲解; 并顺带提出遇到的一些问题;
1. 生成_version.js
;
这里为何生成js而不是json; 主要是考虑我是将变量挂在到window上,页面加载就会主动加载执行该js文件,window上就会有该变量,这样读取和全局使用就非常方便了;
- 在
package.json
的build命令上加一行执行esno ./build/script/postBuild.ts
"scripts": {
"build": "cross-env NODE_ENV=production node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build && esno ./build/script/postBuild.ts"
}
- 在
script/postBuild.ts
执行createConfig
; 具体createConfig中加入生成代码;
// 常量定义 /build/constant.ts中
export const OUTPUT_DIR = 'dir'
export const OUTPUT_GLOBAL_VERSION = '_TC_ADMIN_VERSION'
export const OUTPUT_VERSION_FILE_NAME = '_version.js';
function getRootPath(...dir: string[]) {
return path.resolve(process.cwd(), ...dir);
}
function createConfig() {
// 其他代码省略 OUTPUT_DIR = 'dist'
fs.mkdirp(getRootPath(OUTPUT_DIR));
// add version
const windowVersion = `window.${OUTPUT_GLOBAL_VERSION}`;
const windowVersionConf = `${windowVersion}=${JSON.stringify({
version: Date.now(),
})};
Object.freeze(${windowVersion});
Object.defineProperty(window, "${OUTPUT_GLOBAL_VERSION}", {
configurable: false,
writable: false,
});
`.replace(/\s/g, '');
writeFileSync(getRootPath(`${OUTPUT_DIR}/${OUTPUT_VERSION_FILE_NAME}`), windowVersionConf);
}
这里考虑到扩展性,所以把最终window挂载的变量值用json装载;为避免被误篡改,所以用Object.freeze浅冻结以及设置描述性属性将version变成不可重写的;
2. 将_version.js
写入到index.html
脚本引用中
因为项目中本身已经引用了vite-plugin-html
插件,所以只需要看官方文档直接使用现存的即可
vite-plugin-html文档地址
// build/vite/plugin/html.ts
export function configHtmlPlugin(env: ViteEnv, isBuild: boolean) {
// 其他代码省略
const getVersionConfigSrc = () => {
return `${path || '/'}${OUTPUT_VERSION_FILE_NAME}?v=${pkg.version}-${new Date().getTime()}`;
};
const htmlPlugin: Plugin[] = html({
inject: {
tags: isBuild
? [
// 其他配置省略
// Embed the generated _version.js file
{
tag: 'script',
attrs: {
src: getVersionConfigSrc(),
},
},
]
: [],
},
});
return htmlPlugin;
}
这时候在终端运行npm run build
就已经能得到这样的目录和代码了;
3. main.js中进行轮询比较远端version值
// build/script/versionUpdateDetection.ts
import { OUTPUT_VERSION_FILE_NAME, OUTPUT_GLOBAL_VERSION } from '../constant';
const REFRESH_MS = 300000;
async function getLatestVersion() {
const jsText = await fetch(
`${window.location.origin}/${OUTPUT_VERSION_FILE_NAME}?v=${Date.now()}`
).then((res) => res.text());
let versionInfo: any = {};
if (jsText) {
const OUTPUT_GLOBAL_VERSION_COPY = `${OUTPUT_GLOBAL_VERSION}_COPY`;
const jsTextCopy = jsText.replace(OUTPUT_GLOBAL_VERSION, OUTPUT_GLOBAL_VERSION_COPY);
// 这里用取巧法将js字符串解构成json;这样方便读取值
const evalRes = eval(jsTextCopy);
versionInfo = evalRes[OUTPUT_GLOBAL_VERSION_COPY];
}
return versionInfo;
}
export function versionUpdateDetection() {
const localVersion = window[OUTPUT_GLOBAL_VERSION]?.version || 0;
const interval = setInterval(async () => {
const { version: latestVersion = 0, author_name, message } = await getLatestVersion();
console.log('当前版本:' + localVersion, '远端最新版本:', latestVersion);
if (latestVersion !== localVersion) {
alert('有新版本了~ 请刷新浏览器更新')
clearInterval(interval);
}
}, REFRESH_MS);
}
// main.js中引入
import { versionUpdateDetection } from 'build/script/versionUpdateDetection';
async function bootstrap() {
const app = createApp(App);
// 省略
app.mount('#app', true);
// 版本更新提示
versionUpdateDetection();
}
void bootstrap();
这里有个坑,在运行build时会报一个错
这是因为
rollup
天然并不具备路径解析的能力;需要借助官方集成的插件
在我们vite
项目中已经做了集成,因此只需要配置即可
export default ({ command, mode }: ConfigEnv): UserConfig => {
// 省略
return {
base: VITE_PUBLIC_PATH,
root,
resolve: {
alias: [
// /@/xxxx => src/xxxx
{
find: /\/@\//,
replacement: pathResolve('src') + '/',
},
// /#/xxxx => types/xxxx
{
find: /\/#\//,
replacement: pathResolve('types') + '/',
},
// /%/xxxx => xxxx 这里是本页面要新加的引入根目录文件解析
{
find: /\/@@\//,
replacement: pathResolve('') + '/',
},
],
},
// 省略
},
};
};
接着上述import { versionUpdateDetection } from 'build/script/versionUpdateDetection';
变更成import { versionUpdateDetection } from '/@@/build/script/versionUpdateDetection';
就可以正常build了;
将代码推送部署,测试就发现已经大功告成;
升级;
转载自:https://juejin.cn/post/7207743145998893093