不用刷新!用户无感升级,解决前端部署最后的问题

22,273 阅读4分钟

本文是azuo和萌妹俩技术创作之旅的第12篇原创文章,内容创作@azuo😄,精神支持@大头萌妹😂

前言:前端部署需要用户刷新才能继续使用,一直是一个老大难的用户体验问题。本文将围绕这个问题进行讲解,揭晓问题发生的原因及解决思路。

一、背景

网站发版过程中,用户可在浏览web页面时,可能会导致页面无法加载对应的资源,导致出现点击无反应的情况,严重影响用户体验。

二、问题分析

2.1 问题现象

网络控制台显示加载页面的资源显示404。

image.png

2.2 满足条件

发生这个现象,需要满足三个条件:

  1. 站点是SPA页面,并开启懒加载;
  2. 资源地址启用内容hash。(加载更快启用了强缓存,为了应对资源变更能及时更新内容,会对资源地址的文件名加上内容hash)。
  3. 覆盖式部署,新版本发布后旧的版本会被删除。

特别在容器部署的情况的SPA页面,很容易满足上诉三个条件。笔者在做公司的内部系统就踩过坑。

2.3 原因分析

浏览器打开页面后,会记录路由和资源路径的映射,服务器发版后,没有及时通知浏览器更新路由映射表。导致用户在发布前端打开的页面,在版本更新后,进入下一个路由加载上一个版本的资源失败,导致需要用户刷新才能正常使用。

image.png

三、解决方案

3.1 方案一:失败重试

3.1.1 思路整理:

既然加载失败了,就重试加载发版后的资源版本就行。增加一个manifest.json文件能够获取新版本对应的资源路径。

image.png

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 思路整理

生产环境发布改成增量发布,不再是覆盖式的发布,发版后旧版本依旧保留。

image.png

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
// https://vitejs.dev/config/
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层配合使用,多版本共存,线上启用通过配置指定启用哪个版本,再也不用赶着时间点去发版。

BFF层和增量部署会碰撞什么样的火花,请看下篇:不用刷新!用户无感升级,解决前端部署最后的问题(实践篇)