vite生产环境报错,TypeError: Failed to fetch dynamically imported module: xxx
1.问题描述
vue + vite项目:有时跳转页面会卡住,但刷新一下页面又恢复正常,控制台信息如下:
TypeError: Failed to fetch dynamically imported module: xxx
2.信息收集
用户端复现了问题,从现场获取到控制台日志:
3.问题分析
从报错来看,动态加载模块失败
单独访问module链接,服务端响应404
查看服务器文件,发现服务端文件,与请求文件的hash不一样, 也就是说服务端文件发生了变更,只有一种情况下,服务端文件会发生变更:有版本发布,并且文件代码有更新,才会导致文件hash值变化。
问题推测:用户在发布版本之前已经打开效能管理平台,在版本发布之后,用户跳转有代码更新的页面便会失败
问题证明:询问用户是在什么时候打开的页面,并通过对比鲸云版本发布时间点,对比结果符合上面的猜想,并通过自定义版本号,在测试环境成功复现问题。
到这里,问题基本确定。
4.问题解决
接下来是如何解决这个问题
问题的本质是:版本发布后,某些资源的链接可能发生变化,如果有用户正打开版本更新前的页面,那么此时跳转页面便有可能失败
问题解决思路:在版本发生变更时,自动重载页面,如此便会获得最新的资源链接
实现思路:
首先需要标记版本号:版本号已经存在于
Jenkinsfile
,不过该文件与js
文件分离,需要从其中读取出来本质上
js
文件都会下发到客户端,在客户端执行,但浏览器又存在js
缓存,因此不能从js
变量读取version
,读取到的都是发布前的版本号可以每次进行代码构建时创建一个
version.txt
,将版本号写入该文件,并且将该文件在服务端暴露,并服务端设置txt
文件不缓存,前端可通过http
请求,访问version.txt
, 则可读取到最新的版本号读取服务端版本号时机选择:有两种方式,一是定时轮询(确定是比较耗费资源),二是用户点击新链接时查询版本号(可在路由守卫中实现,缺点是会影响页面跳转速度)
-
由于上面问题是一个较小概率事件,不应该对解决该问题花费过多的代价
基于方案二,能否在不影响跳转速度的同时,获取最新版本号?可以,也有两种方法:1.使用异步方式获取版本号,2.使用
web worker
获取版本号异步方式缺陷:出现异常可能会影响到主线程,异步代码会在同步代码执行完之后进行,版本检测速度会有影响
使用
web worker
可以解决异步的缺陷,web worker
工作在单独的线程,如果worker出现异常不会影响主线程,在多核CPU
下worker
与主线程并行执行,速度会更快,现代浏览器对
web worker
的支持也比较不错:
5.代码实现
基于以上思路,通过代码实现
读取Jenkinsfile
版本号,定义全局js
变量__APP_VERSION__
(客户端存储的版本号), 创建静态资源version.txt
(服务端当前版本号)
./vite.config.js
import fs from 'fs';
...
// 从Jenkinsfile读取版本号,并生成version.txt
const jenkinsContent = fs.readFileSync(resolve(__dirname, 'Jenkinsfile')).toString();
const versionMatch = jenkinsContent.match(/VERSION\s*=\s*'(\S*)'/);
const versionCode = versionMatch[1];
console.log(`Current application version: ${versionCode}`);
// public文件夹下的内容会原封不动地打包到dist
fs.writeFileSync(resolve(__dirname, 'public/version.txt'), versionCode);
export default ({ mode }) => {
... ...
// 定义全局常量
define: {
__APP_VERSION__: JSON.stringify(versionCode),
},
});
};
发送请求访问version.txt
资源,读取服务端版本号,如果有更新就通知主线程
./workers/versionCheckWorker.js
// 检测版本worker
import axios from 'axios';
const versionFileUrl = `${import.meta.env.VITE_APP_BASE_URL}version.txt`;
const instance = axios.create({
timeout: 3000,
headers: { 'Cache-Control': 'no-cache' },
});
self.onmessage = e => {
const data = e.data;
const currentVersion = data.currentVersion;
const toPath = data.toPath;
// 获取远端版本
instance
.get(versionFileUrl)
.then(res => {
if (res.status === 200) {
self.postMessage({ hasChange: currentVersion != res.data, remoteVersion: res.data, toPath });
} else {
console.error(res);
}
})
.catch(e => console.error(e));
};
self.onmessageerror = e => console.error(e);
在路由守卫中监听用户跳转,并将当前版本号发送给worker
线程,如果检测到版本更新,就刷新页面
./router/index.js
import versionCheckWorker from '../workers/versionCheckWorker?worker&inline';
... ...
router.beforeEach(async to => {
... ...
versionCheck(to);
... ...
});
/**
* 通过web worker检测版本发布,以修复版本发布时,用户长时间停留页面导致无法跳转的问题
*/
const ENABLE = '1';
let checkWorker = null;
/* eslint-disable no-undef */
let appVersion = __APP_VERSION__;
if (import.meta.env.VITE_CHECK_VERSION_ENABLE === ENABLE && window.Worker) {
checkWorker = new versionCheckWorker();
checkWorker.onmessage = e => {
const data = e.data;
if (data && data.hasChange) {
console.log(`远端版本版本发生变化,强制刷新页面,远端版本号:${data.remoteVersion}, 本地版本号: ${appVersion}`);
appVersion = data.remoteVersion;
// 刷新页面
window.location.assign(`${import.meta.env.VITE_APP_BASE_URL}${data.toPath}`);
}
};
console.log(`已启用版本检测,当前版本:${appVersion}`);
}
/**
* 版本检查
*/
const versionCheck = to => {
if (checkWorker) {
checkWorker.postMessage({ currentVersion: appVersion, toPath: to.fullPath });
}
};