问题描述
生产环境前端上线后页面出现404,样式不是最新,退出登陆无效,必须刷新才能获取到正确的页面
使用框架:umi+React+qiankun
最终解决办法-简单粗暴版
登录页首次进入时强制刷新,可删除子应用缓存。
前置知识
乾坤通过加载子应用的html文件后,自定义函数解析html文件内容,将解析后的dom数挂载到子应用节点上,切换子应用即卸载该Dom节点。
问题排查:
- 初步猜想:可能是父应用请求子应用index.html文件时因缓存问题,请求了旧版本html文件,导致加载了旧版本umi.js文件,因此无法获取最新版本信息。期望通过Nginx对html不使用任何缓存。
- 猜想验证:经详细询问问题后,发现重新登陆后依旧无法使用最新修改功能。根据对浏览器开发者工具的
网络中接口访问记录进行分析后发现,重新登陆后未请求子应用html文件,且子应用html文件使用协商缓存,因此,并非子应用html文件缓存问题。 - 进一步排查:根据查询乾坤内核逻辑,得出乾坤加载子应用逻辑:
- qiankun通过劫持路由变化,当一级路由与子系统路由一致时,加载相应子系统。
- 加载子系统流程:
- 根据路径通过import-entry-html模块加载子应用html文件,并解析js和css,将其处理为dom节点,挂载到子应用div的dom节点中
- import-entry-html模块中使用fetch方法获取html文件,并存储到xx对象中,下次再次访问该节点时,不需要重新加载子应用
- 判断xx对象存储子应用信息为元凶,
只要该对象不销毁,父应用不会主动更新子应用信息。 - 重现问题:该问题仅当用户短时间内使用过子系统(对象保存了该子系统信息)时,服务器前端文件更新,导致无法访问新版本前端文件。若用户刷新(销毁xx对象)或打开新tab页,则不会出现该问题。
- 总结问题:用户访问页面时,前端文件更新,用户不主动刷新页面,会访问旧版本前端文件,导致部分功能无法使用。
当前解决方案:
- 待解决问题:发现用户访问前端文件与最新文件版本不一致,并提醒。
- 查询资料:umi框架github的issue中存在这一问题,一位老哥已提出解决方案:在打包的文件中添加版本信息(打包时间),上线后合适时机获取新旧版本信息进行对比。
- 资料:# 发版后,页面不刷新会报错,希望能监听这种异常,方便提示用户刷新页面
- 实现方案:
- 打包单独的version添加版本信息,并在html文件中添加版本信息,在父应用路由切换时,通过document获取元素读取当前版本信息,与实时获取服务器version文件中的版本信息进行对比,若不一致,提醒用户,并刷新页面,nginx添加配置令version文件禁止缓存。
- 遇到加载失败后分析错误原因,若出现js文件不存在(404)页面,自动 reload 页面重试
最终方案
- 第一中方案需要在两个位置添加打包信息,且修改Nginx配置。第二种方法仅针对新增路由的情况,若修改其他代码,则无法通过校验,不会刷新。
- 整合两种方案:打包的html文件中添加版本信息,在父应用路由切换时,通过document获取元素读取当前版本信息,与实时获取服务器html文件中的版本信息进行对比,若不一致,提醒用户,并刷新页面
- 原因:因本系统中html文件为协商缓存,每次都会获取html文件,并对比内容是否一致,无需禁止缓存。
- 由于不知道何时刷新页面,可能会在正常使用中多次刷新页面,因此协商缓存可有效减少请求时间。经验证,第一次请求约300ms,后续请求仅需个位数——4ms,至于文本内容本不多的html文件为何会请求这么长时间需后续探索。
- 优化:路由跳转时进行版本对比,版本对比函数为异步函数,不设置await,请求服务器文件时同时进行页面跳转,避免增加等待时间。版本对比完成后,添加延时函数,待路由跳转后执行页面刷新操作
window.location.reload()。
对比版本时机
- 点击路由页面时对比
- 定时任务对比版本
其他解决方案
不管版本是否一致,特定时机强制刷新页面(base和子系统全部刷新),适用于固定时间上线的情况。
方案
- 路由切换时主动获取服务器上版本信息,与本地进行对比(一开始的思路,需要对所有子系统进行改造)
- 检测到token失效,跳转登录页后强制刷新(子系统跳转登录也无法在base中控制)
- 进入登录页强制刷新(locationStorage中存储上次更新时间,每日仅更新一次,由于更新本身不频繁,可解决大部分问题)
经验:
- 重新登陆无法刷新页面,仅重新获取用户权限信息(仅与后端Redis数据相关,与前端更新无效);
- 分析问题需熟悉完整流程,才能发现问题点。
改进时
- 复现问题 将打包后的html文件中添加打包时间。本项目使用umi框架,由于umi不提供源html文件,因此需要通过插件方式,修改打包后的html文件。 难点:
- umi插件使用不够清晰
- addHTMLScripts方法不知道只添加内容,查不到具体可输入参数
- umi路径下
plugin.ts文件默认注册为插件,无需在.umirc.ts中注册,注册两次会报错
参考文章:umi项目中的一些趣事
import dayjs from 'dayjs';
import type {IApi} from 'umi';
export default function(api:IApi){
console.log(dayjs(), dayjs().format('YYYY-MM-DD HH:mm:ss'))
api.addHTMLScripts(()=>[{content: `// 打包时间:${new Date().toISOString()},
// ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`}])
}
打包后结果
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
</head>
<body>
<div id="root"></div>
<script src="/umi.js"></script>
<script>// 打包时间:2025-02-25T13:39:02.375Z,
// 2025-02-25 21:39:02</script>
</body>
</html>
基础部分知识学习
systemjs
-
将子应用打包成一个模块(system规范的js文件),通过引入该文件,加载js文件,实现对子应用的加载
-
将用户的脚本转成对象,便于远程使用script脚本加载(跨域->JSONP)
-
难点:处理依赖项,需要先加载依赖项,再加载子应用逻辑
-
加载流程:
- 首先通过JSONP方式加载依赖列表(相关依赖,子应用js文件),将加载的代码放到某个节点上
- 加载完依赖后,调用回调函数
sysyem.register(依赖列表, 回调函数),System.import('@jw/root-config') - 回调函数:
资源优化:可以将依赖项提出,通过cdn/主应用加载,加载后调用回调函数的setters方法,将结果赋予webpack
流程:获取依赖项,加载依赖项,执行函数
single-spa
微前端,就是可以加载不同的应用,single-spa是基于路由的微前端。
实现方式
与SystemJS相比,添加了路由管理、状态维护。
路由管理:监听页面路由,加载符合路由的子应用js文件;劫持路由变化,当子应用加载完成后,更新路由,避免子系统加载前触发相关事件
路由加载仍使用SysremJs获取js文件,执行相应的代码 在路由变化时,维护启动应用、挂载应用、卸载应用数组 管理子应用状态(未启动、启动、挂载、卸载),根据路径匹配加载相应的js文件 路径匹配后动态加载相应的js文件(注册);
所谓的注册应用就是路径匹配时加载对应应用
-接入已写好的应用 需要根据接入协议改写子应用,bootstrap,mount, unmount 通过System.js加载js文件,single-spa提供了路由,根据路径加载不同js文件(子应用)
- 父应用加载流程:
- 端口访问后加载父应用index.ejs(html)文件,
- html加载父应用js文件
- 匹配路径动态加载应用(子应用js)
- 子应用加载流程:
缺点:
- 需要父应用index.ejs文件的script标签中引入子应用js文件位置
- 需要父应用的js文件中注册应用(registerApplication)
补:动态加载发送请求时需要限制请求路径,容易子应用请求了父应用路径
qiankun
基于single-spa开发,实现了样式隔离、js沙箱、预加载等
- 主应用如何接入乾坤
- 实现reigterAppliction方法注册子应用
- 调用start方法
- 子应用如何接入qiankun
- 在render的index.js页面暴露bootstrap、mount、unmount方法
- 如何实现样式隔离,沙箱
- 在加载子应用时,通过fetch方法获取子应用html文件,通过解析html文件,获取到css,js和dom树,将css中className添加前缀
问题:
- 主应用与子应用独立运行:
乾坤通过window.__POWERED_BY_QIANKUN__变量,判断当前是否在父应用中;
- 可以根据此变量,执行render函数,保证子应用独立运行。
- 根据此变量,修改主路径,避免静态资源获取失败(主子应用服务器地址不同)
- 子应用配置跨域访问,防止主应用无法获取(主应用通过fetch获取)
乾坤通过监听路由变化,加载子应用的html文件,像iframe一样加载子应用,解析后挂载到指定的dom节点展示。
- importHTML具体实现原理:
- 通过fetch方法读取页面内容,返回为页面的html的字符串
- 使用processTpl方法解析html的内容并删除注释,获取style样式及script代码
- 沙箱机制:
- 通过代理windows的方式操作沙箱
- 读取和写入时拦截方法,添加
- shadowDom,为每个微应用的根元素创建一个Shadow Root,将样式插入Shadow Root中,不是主DOM的
head元素 - 乾坤通过动态重写子应用的CSS规则,为每个选择器添加唯一前缀(如
div→[qiankun-subapp] div),将样式限制在子应用容器内。