单页面应用 SPA 在构建后会生成的资源文件,可能导致已经打开的页面用户点击按钮跳转无响应,问题出现取决于构建配置策略。
开发流程中,经常在开发环境构建与后端进行联调,在测试环境修复 Bug 也进行频繁重新构建发布,从而打断正在测试的 QA 同学,故而有些项目组严格执行测试环境只能由 QA 同学进行构建 😅。
对于线上的系统,由于通常在晚上发布,然而问题一直存在,用户都学会刷新页面自救(强者从不埋怨环境)
早期对于 SPA 项目(vue)都是这样处理的
// vue-router
// ...code
router.onError(handlerRouterError);
/**
* 路由加载404,提醒发布新版本需要刷新
*/
async function handlerRouterError(error) {
const patters = [
'Loading chunk',
'Failed to fetch dynamically imported',
'Importing a module script failed'
];
if (patters.some(pattern => error.message.includes(pattern))) {
// 早期直接刷新,被 QA 同学骂了
// window.location.reload(true);
const { action } = await Notice({
type: 'upgrade',
message: '系统发布了新版本,是否立刻刷新?'
});
if (action === 'confirm') {
window.location.reload(true);
}
}
}
这种方案只能覆盖路由之间的跳转,而 QA 同学想第一时间知道是否构建了最新页面
方案调研
- 增量发布,确保旧资源访问成功
- 缺点:不能同步最新的功能,如果接口有改动会炸
- 修改构建输出文件名,保证一样,通过插件添加参数
?v=时间戳- 缺点:可能无法饶过浏览器强缓存
- 插件生成
manifest文件,轮询检测文件差异 - 插件生成
version文件,轮询检测版本号,原理同上 - pwa 方式,最完美的方式,参考
vuepress- 缺点:成本比较高
自撸轮询检测插件
今天来说说自己撸一个轮询检测模式的插件,伸手党可以使用 plugin-web-update-notification
在我们低代码平台可以看到使用的是校验资源产物
每次返回的内容太多,http 请求的成本也是成本,作为我们这种小业务线的的前端,有没有低端的技术低成本的可以用一下。
还是使用生成 version.json 文件方式吧,但会生成多余的文件。
有没有更简单的方式,让我们回到 2019 github.com/EtherDream/…
fetch(url, {
cache: 'no-cache',
})
这就是我们的法宝,详细的自行翻 MDN 文档。
我们只针对单页入口 html 进行轮询访问,既可以使用缓存又可以更新缓存,你真他娘是个小天才。
创建 unplugin-web-update-notification
对于兼容 webpack,vite的插件推荐使用 unplugin
直接 git clone @sxzz 的 github.com/sxzz/unplug…, 已经有编码规范、eslint、typescript配置,tql!!!
我们的目标是给 SPA 的入口文件 main.js 注入轮询代码(留后门,FBI Warning),副作用影响 sourcemap。
那就开始吧
- 把包名改了
// package.json
{
"name": "unplugin-web-update-notification"
}
- 修改
index.ts
import { injectWebUpdateNotification } from './core/injectWebUpdateNotification'
const isProd = (mode: string) => {
// webpack
if (typeof process.env.NODE_ENV !== 'undefined') {
return process.env.NODE_ENV === 'production'
}
// vite import.meta
return mode === 'production'
}
export default createUnplugin<Options | undefined, false>((rawOptions = {}) => {
const options = resolveOption(rawOptions)
const filter = createFilter(options.include, options.exclude)
const name = 'unplugin-web-update-notification'
let mode = ''
return {
name,
enforce: options.enforce,
apply: 'build',
transformInclude(id) {
return filter(id)
},
transform(code /*, id*/) {
if (!isProd(mode)) return code
return [code, injectWebUpdateNotification(options.options)].join('')
},
vite: {
configResolved(config) {
// 解决获取 vite 环境变量
mode = config.mode
},
},
}
})
- 编写
injectWebUpdateNotification
这里是使用模板的形式,喜欢高亮的同学可以使用 fs 读取文件
import { type NotificationOptions } from './options'
export const injectWebUpdateNotification = (
options: Partial<NotificationOptions> = {}
) => {
return `
// 防止入口文件重名
// eslint-disable-next-line
import { ElNotification as __ElNotification } from 'element-ui';
;(function unPluginWebUpdateNotification() {
const storeKey = 'Last-Modified'
let pollingInterval = null
let lastVersion = window.sessionStorage.getItem(storeKey)
function setLocalData(version) {
window.sessionStorage.setItem(storeKey, version)
lastVersion = version
}
async function getVersion() {
const response = await fetch("${options.baseURL || '/'}", {
cache: 'no-cache',
})
return response.headers.get(storeKey)
}
async function checkWebUpdate(showDialog = false) {
const version = await getVersion()
if ((lastVersion && version !== lastVersion) || showDialog) {
showWebUpdateNotification(version)
}
return () => {
clearInterval(pollingInterval)
}
}
function pollingCheck() {
// 默认每5分钟一次
pollingInterval = setInterval(checkWebUpdate, ${options.interval} * 60 * 1000)
}
function showWebUpdateNotification(version) {
__ElNotification.closeAll()
__ElNotification({
collapse: true,
title: '📢 系统升级通知',
duration: 0,
message: '检测当前系统已更新,是否刷新页面?',
onClick: () => {
setLocalData(version)
window.location.reload();
}
});
}
async function setup() {
// 首次访问页面保存版本
const version = await getVersion()
setLocalData(version)
// 轮询的命运开启,原神启...
pollingCheck()
}
setup()
// 导出公共函数,将在路由钩子上使用
window.unpluginWebUpdateNotification = checkWebUpdate
})();
`
}
- 修改路由钩子
// vue-router
// ...code
router.onError(handlerRouterError);
/**
* 路由加载404,提醒发布新版本需要刷新
*/
async function handlerRouterError(error) {
const patters = [
'Loading chunk',
'Failed to fetch dynamically imported',
'Importing a module script failed'
];
if (patters.some(pattern => error.message.includes(pattern))) {
// true 直接显示版本更新通知
window?.unpluginWebUpdateNotification?.(true)
}
}
- 发布 npm 包
# 此处省略一大堆 npm scripts
> pnpm release
- 使用
在 vue-cli 项目
module.exports = {
configureWebpack: {
plugins: [
require('unplugin-web-update-notifaction/webpack').default()
]
}
}
在 vite 项目
import { defineConfig } from 'vite'
import webUpdateNotification from 'unplugin-web-update-notification/vite'
export default defineConfig({
plugins: [
// ...
webUpdateNotification()
]
})
最终效果: