1.背景
- 每次项目上线后,异常监控总是零零散散报一些资源加载或者解析失败的告警
- 仔细对比chunk的hash值会发现已经是上一版本的js文件
- 为什么会出现这个问题呢?也不难想到,项目是单页应用,页面使用懒加载分多个chunk打包,首次只加载首页需要的js文件。如果在项目发布前,用户已经在程序中,并且还有未访问的页面,此时我们重新发布上线了,老的文件已经被覆盖,用户再访问未访问过的页面时就会找不到资源,导致白屏。
- 如果用户在发布前已经进入过其他页面,被缓存在本地,这样虽然不会导致白屏了,但是也无法看到最新的效果。
- 在测试环境模拟一下,点击一个未访问过的页面时,确实白屏了,报错信息也吻合
2.解决思路
- 导致这些问题的根本原因就是,用户和程序都无法感知项目已上线,无法做到立即刷新,重新加载,那我们就想办法在项目上线后通知页面进行刷新。
2.1运行中的项目,如何感知到项目更新了
- 最容易想到的就是有一个存储版本号的地方,我们去轮询是否和上一次请求到的版本一致,如果不一致去做一些刷新的操作
- 既然是前端的项目,最好还是我们自己去维护这个数据,所以我选择在每次打包的时候生成一个存储当前时间戳的json文件,存在dist目录下一起放到我们前端服务器上。
- 写一个最最简单的webpack插件帮我们实现生成json文件的功能
const pluginName = 'GenerateTimeJsonWebpackPlugin';
const fs = require('fs');
const path = require('path');
class GenerateTimeJsonWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tap(pluginName, (compilation) => {
const filePath = path.resolve('build', 'timeStamp.json');
fs.writeFile(filePath, `${JSON.stringify({time: new Date().getTime()})}`, (err) => {
console.log(err);
});
});
}
}
module.exports = GenerateTimeJsonWebpackPlugin;
- 这样每次构建时就会多一个文件
2.2什么时机判断是否更新?
- 轮询肯定是最耗性能的,间隔太小浪费,间隔太大又有白屏的风险
- 由于我们出现请求页面的时机总是在页面加载的时候,所以选择加一个路由守卫,在页面跳转前判断是否更新,如果更新了就reload页面。
- 项目是用react进行开发的,所以写个高阶组件来实现守卫的功能
import { Route } from 'react-router-dom';
import { useRef, useState } from 'react';
import axios from "axios";
const basePath = '/mp/138519745866498048/368331042878263296/credit-shop/';
// 路由守卫
const BeforeRouteEnter = (props) => {
const {path, exact, component} = props;
// 用来控制在时间戳未请求到时,不加载路由对应的页面,避免请求到老的资源
const [loading, setLoading] = useState(process.env.NODE_ENV === 'production');
// 用来存储当前项目的时间戳
const timesTamp = useRef('');
// 生产环境需要判断 当前访问的chunk是否已经更新
if (process.env.NODE_ENV === 'production') {
// 由于项目中统一封装过的axios做了很多错误的处理 为了不影响正常页面的功能 选择单独使用axios请求
axios
.get(basePath + `timeStamp.json?t=${new Date().getTime()}`)
.then(res => {
// 判断是否已经更新了代码 更新了就重新加载页面 请求新的资源
if (timesTamp.current && (timesTamp.current !== res.data?.time)) {
window.location.reload();
}
timesTamp.current = res.data?.time;
setLoading(false);
}).catch(err => {
setLoading(false);
});
}
return !loading ? <Route path={path} exact={exact} component={component}></Route> : ''
}
export default BeforeRouteEnter;
然后将Route组件改成我们自己封装的BeforeRouteEnter,这样在每次跳转前都请求一次当前打包的时间戳,判断是否和正在运行中的一致,不一致就重载,这样一来就解决了我们的问题~
有问题欢迎大家及时指出,避免误导其他同学~