vite生产环境报错,TypeError: Failed to fetch dynamically imported module: xxx

vite生产环境报错,TypeError: Failed to fetch dynamically imported module: xxx

1.问题描述

vue + vite项目:有时跳转页面会卡住,但刷新一下页面又恢复正常,控制台信息如下:

TypeError: Failed to fetch dynamically imported module: xxx

2.信息收集

用户端复现了问题,从现场获取到控制台日志:

img_16739282021590.png

3.问题分析

从报错来看,动态加载模块失败

单独访问module链接,服务端响应404

查看服务器文件,发现服务端文件,与请求文件的hash不一样, 也就是说服务端文件发生了变更,只有一种情况下,服务端文件会发生变更:有版本发布,并且文件代码有更新,才会导致文件hash值变化。

问题推测:用户在发布版本之前已经打开效能管理平台,在版本发布之后,用户跳转有代码更新的页面便会失败

问题证明:询问用户是在什么时候打开的页面,并通过对比鲸云版本发布时间点,对比结果符合上面的猜想,并通过自定义版本号,在测试环境成功复现问题。

到这里,问题基本确定。

4.问题解决

接下来是如何解决这个问题

问题的本质是:版本发布后,某些资源的链接可能发生变化,如果有用户正打开版本更新前的页面,那么此时跳转页面便有可能失败

问题解决思路:在版本发生变更时,自动重载页面,如此便会获得最新的资源链接

实现思路:

  1. 首先需要标记版本号:版本号已经存在于Jenkinsfile,不过该文件与js文件分离,需要从其中读取出来

  2. 本质上js文件都会下发到客户端,在客户端执行,但浏览器又存在js缓存,因此不能从js变量读取version,读取到的都是发布前的版本号

  3. 可以每次进行代码构建时创建一个version.txt,将版本号写入该文件,并且将该文件在服务端暴露,并服务端设置txt文件不缓存,前端可通过http请求,访问version.txt, 则可读取到最新的版本号

  4. 读取服务端版本号时机选择:有两种方式,一是定时轮询(确定是比较耗费资源),二是用户点击新链接时查询版本号(可在路由守卫中实现,缺点是会影响页面跳转速度)

  5. 由于上面问题是一个较小概率事件,不应该对解决该问题花费过多的代价

    基于方案二,能否在不影响跳转速度的同时,获取最新版本号?可以,也有两种方法:1.使用异步方式获取版本号,2.使用web worker获取版本号

    • 异步方式缺陷:出现异常可能会影响到主线程,异步代码会在同步代码执行完之后进行,版本检测速度会有影响

    • 使用web worker可以解决异步的缺陷,web worker工作在单独的线程,如果worker出现异常不会影响主线程,在多核CPUworker与主线程并行执行,速度会更快,

    • 现代浏览器对web worker的支持也比较不错:

image-20230111103751340.png

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 });
  }
};
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容