不用刷新!用户无感升级,解决前端部署最后的问题
前端部署需要用户刷新才能继续使用,一直是一个老大难的用户体验问题。本文将围绕这个问题进行讲解,揭晓问题发生的原因及解决思路。
一、背景
网站发版过程中,用户可在浏览web页面时,可能会导致页面无法加载对应的资源,导致出现点击无反应的情况,严重影响用户体验。
二、问题分析
2.1 问题现象
网络控制台显示加载页面的资源显示404。
2.2 满足条件
发生这个现象,需要满足三个条件:
- 站点是SPA页面,并开启懒加载;
- 资源地址启用内容hash。(加载更快启用了强缓存,为了应对资源变更能及时更新内容,会对资源地址的文件名加上内容hash)。
- 覆盖式部署,新版本发布后旧的版本会被删除。
特别在容器部署的情况的SPA页面,很容易满足上诉三个条件。笔者在做公司的内部系统就踩过坑。
2.3 原因分析
浏览器打开页面后,会记录路由和资源路径的映射,服务器发版后,没有及时通知浏览器更新路由映射表。导致用户在发布前端打开的页面,在版本更新后,进入下一个路由加载上一个版本的资源失败,导致需要用户刷新才能正常使用。
三、解决方案
3.1 方案一:失败重试
3.1.1 思路整理:
既然加载失败了,就重试加载发版后的资源版本就行。增加一个manifest.json文件能够获取新版本对应的资源路径。
3.1.2 举例说明
以vue项目进行举例子说明:
第一步: 修改构建工具配置以生成manifest文件
使用vite构建的项目,可以在vite.config.ts增加配置build.manifest为true,用以生成manifest.json文件
export default defineConfig({
// 更多配置
build: {
//开启manifest
manifest: true,
cssCodeSplit: false //关闭单独生成css文件,方便demo演示
}
})
如果使用webpack构建的项目,可以使用webpack-manifest-plugin插件进行配置。
进行项目生产构建,生成manifest.json,内容如下:
// 简单说明:文件内容是项目原始代码目录结构和构建生成的资源路径一一对应
{
"index.html": { // 页面入口
"dynamicImports": ["src/pages/page1.vue", "src/pages/page2.vue"],
"file": "assets/index-e170761c.js",
"isEntry": true,
"src": "index.html"
},
// page1对应单文件组件
"src/pages/page1.vue": {
"file": "assets/page1-515906ab1.js", // JS文件
"imports": ["index.html"],
"isDynamicEntry": true,
"src": "src/pages/page1.vue"
},
// page2对应单文件组件
"src/pages/page2.vue": {
"file": "assets/page2-9785c68c.js", // JS文件
"imports": ["index.html"],
"isDynamicEntry": true,
"src": "src/pages/page2.vue"
},
"style.css": {
"file": "assets/style-809e5baa.css",
"src": "style.css"
}
}
第二步,修改route文件,加上重试逻辑
在路由文件中,增加加载页面js失败的重试逻辑,通过新版的manifest.json来获取新版的页面js,再次加载。
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory('/'),
routes: [
{
path: '/page1',
// component: () => import(`../pages/page1.vue`), // 变更前
component: () => retryImport('page1'), // 变更后
},
{
path: '/page2',
// component: () => import(`../pages/page1.vue`),
component: () => retryImport('page2'),
},
]
})
async function retryImport(page) {
try {
// 加载页面资源
switch (page) {
case 'page1':
// 这里demo演示,没有使用dynamic-import-vars
return await import(`../pages/page1.vue`)
default:
return await import(`../pages/page2.vue`)
}
} catch (err: any) {
// 判断是否是资源加载错误,错误重试
if (err.toString().indexOf('Failed to fetch dynamically imported module') > -1) {
// 获取manifest资源清单
return fetch('/manifest.json').then(async (res) => {
const json = await res.json()
// 找到对应的最新版本的js
const errPage = `src/pages/${page}.vue`
// 加载新的js
return await import(`/${json[errPage].file}`)
})
}
throw err
}
}
export default router
3.1.3 总结
这个方案改造只涉及前端层,成本最低,但是无法做到多版本共存,只能适配部分发版变更,如果涉及删除页面的版本,最好增加一个容错页面。
3.2 方案二:增量部署
3.2.1 思路整理
生产环境发布改成增量发布,不再是覆盖式的发布,发版后旧版本依旧保留。
3.2.2 示例实践
需要改造构建配置,增加版本的概念,保证新旧版本不路径冲突
vite 构建工具示例:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
const version = require('./package.json').version
// 支持构建命令传入构建版本
const versionName = process.env.VERSION_NAME || version
//
export default defineConfig({
plugins: [vue()],
build: {
manifest: true,
assetsDir: `./${versionName}`, // 版本号
}
})
webpack构建工具示例:
// webpack.config.js
const path = require('path');
// 支持构建命令传入构建版本
const versionName = process.env.VERSION_NAME || version
module.exports = {
//...
output: {
path: path.resolve(__dirname, `dist/${versionName}/assets`),
},
};
3.2.3 总结
需要CI/CD发版改造,由之前的容器部署改成静态部署(即文件上传对象存储的思路一样),这种增量发部署适配全部的场景,而且,支持多版本共存,能做到版本灰度放量。
四、总结
本文通过覆盖式部署在前端版本发布,导致用户不可使用的严重体验问题,分析问题发生根因,并给出两种解决思路。笔者结合公司的云设施,最后使用增量部署
,并BFF层配合使用,多版本共存,线上启用通过配置指定启用哪个版本,再也不用赶着时间点去发版。
来源:juejin.cn/post/7223196531143131194