likes
comments
collection
share

实现前端项目发布后,用户无刷新升级。——从零开始搭建一个高颜值后台管理系统全栈框架(十五)

作者站长头像
站长
· 阅读数 18

背景

现在前端改东西发布后,如果用户没刷新的情况下,切换页面,可能会报错。

原因

这是因为我们文件做了路由按需加载,也就是说只有切页面的时候,才去加载对应代码,这样做的好处是第一次加载的资源比较少,白屏时间短。

但是这样做会引发另外一个问题,假如用户一直在使用系统,这时候我们改了某个页面的bug,用户在没打开过那个页面的情况下,(如果用户打开过,因为前端会把加载过的代码缓存在内存中,不去从服务器加载js,这种情况不会报错。)我们发布后,因为这当前文件的hash已经变了,老的那个js文件已经不存在了,所以这时候用户再切到那个页面就会报错。

解决这个问题的方案很多,这里我把我了解的方案,给大家分享一下,最后一种方案很牛,一定要看到最后。

方案

多版本

描述

上面说了因为新发布的代码会把老的文件覆盖,那我们每次打包的时候,把老的代码给保留下来就行了,这个方案我没有实际操作过,但是理论上是可行的。

实现思路

CI中,拉取上一个版本的镜像,然后使用cp命令把镜像里的上个版本的代码复制出来,然后和当前打包出来的文件夹合并一下,然后打出一个新的镜像。

# 从镜像中复制文件到外面
docker cp <容器ID或名称>:<容器内路径> <宿主机路径>

小结

这个方案需要有运维的知识,对docker要有一定的了解。前端一直不刷新,改的bug会一直不生效,需要通知用户手动刷新。同时需要后端接口也支持多版本,不然前后端版本对不上,可能会报错。

后端给前端推送消息,刷新页面

实现思路

后端写一个接口,使用websocket通知所有客户端,前端接收到对应的消息后,调用浏览器刷新页面方法刷新页面。发布成功后在CI中调用这个接口就行了。

小结

这个方案有个问题,万一用户正在输入某个表单,这时候前端发布了,然后浏览器自动刷新,这时候用户心态可能会炸。如果给用户一个选项,可以稍后刷新,用户做完操作后,可能会忘记刷新,然后有可能会出现上面的报错。

监听文件404,刷新页面

描述

这是个简单粗暴的方法,全局监听js资源加载404,如果404了,就刷新页面。

实现

window.addEventListener('error', function(event) {
  const target = event.target;
  if (target.tagName === 'SCRIPT' && target.src) {
    // 刷新页面
    window.location.reload();
  }
}, true);

总结

简单粗暴,我目前在公司里使用的就是这个方案。

轮询检查index.html,查询js有没有变动

描述

前端写一个定时器,定时请求index.html文件内容,判断当前内容和上一次的是不是一样,如果不一样说明前端发布了新版本,然后和上面一样刷新就行了。

总结

具体实现可以参考这篇文章。

终极方案-用户无感升级

说明

实现思路

既然文件加载失败了,我们只要发现文件加载失败了,找到正确的资源文件名重新加载就行了。当文件加载失败时,怎么找到对应的资源文件名呢,这里用到了manifest.json文件,打包资源的文件映射,有了它我们知道了文件名就能找到打包后的文件路径。

实现

修改vite打包设置,开启manifest.json输出

实现前端项目发布后,用户无刷新升级。——从零开始搭建一个高颜值后台管理系统全栈框架(十五)

manifest.json文件内容

实现前端项目发布后,用户无刷新升级。——从零开始搭建一个高颜值后台管理系统全栈框架(十五)

file字段就是打包后的字段名

具体代码:

// src/config/routes.tsx
export const modules = import.meta.glob('../pages/**/index.tsx');

export const componentPaths = Object.keys(modules).map((path: string) => path.replace('../pages', ''));

let manifest: any;

export const components = Object.keys(modules).reduce<Record<string, () => Promise<any>>>((prev, path: string) => {
   const formatPath = path.replace('../pages', '');
   prev[formatPath] = async () => {
      try {
         // 这里其实就是动态加载js,如果报错了说明js资源不存在
         return await modules[path]() as any;
      } catch {
         // 如果manifest已经存在了,就不用再请求了
         if (manifest) {
            try {
               // 有可能manifest是过期的,所以可能还会加载失败
               return await import('/' + manifest[`src/pages${formatPath}`]?.file);
            } catch {
               // 如果失败,重新获取一下manifest.json,拿到最新的路径
               manifest = await (await fetch('/manifest.json')).json() as any;
               return await import('/' + manifest[`src/pages${formatPath}`]?.file);
            }
         } else {
            // 如果manifest.json为空,请求manifest.json,并根据最新的路径加载对应js
            manifest = await (await fetch('/manifest.json')).json() as any;
            return await import('/' + manifest[`src/pages${formatPath}`]?.file);
         }
      }
   }
   return prev;
}, {});

以前为了解决404问题,nginx配置了当请求的资源存在的时候返回index.html,导致现在js虽然不存在了,请求也不会报错,返回了index.html的内容。

修改ngxin配置,如果js和css文件匹配不到则返回404

实现前端项目发布后,用户无刷新升级。——从零开始搭建一个高颜值后台管理系统全栈框架(十五)

测试验证

  • 修改菜单管理页面第一列标题

    实现前端项目发布后,用户无刷新升级。——从零开始搭建一个高颜值后台管理系统全栈框架(十五)

  • 打开网站,随便打开一个页面,这时候不要打开菜单管理页面。

  • 等发布完成后,不要刷新,打开菜单管理页面。根据下图可以发现,新的内容已经生效了

    实现前端项目发布后,用户无刷新升级。——从零开始搭建一个高颜值后台管理系统全栈框架(十五)

  • 开始js是404,后面请求了manifest.json后,加载了新的资源。

    实现前端项目发布后,用户无刷新升级。——从零开始搭建一个高颜值后台管理系统全栈框架(十五)

最后

上面方案有个小问题,如果开始加载过菜单管理页面后,后面改的东西就不会生效了,这时候可以和前面方案结合一下,使用轮询检查manifest.json内容有没有变化,如果有变化通知用户刷新。

到此框架这一块的功能基本已经实现完了,后面会开始开发低代码平台,在公司做了几年低代码,把我的一些心得分享给大家。

我的目标是做一个企业级的低代码平台,也会像前面这些文章一样,带着大家一点一点做,让大家看完后自己也能实现一个低代码平台,敬请期待吧。

项目体验地址:fluxyadmin.cn/user/login

前端仓库地址:github.com/dbfu/fluxy-…

后端仓库地址:github.com/dbfu/fluxy-…