Vite+Vue报错TypeError: Failed to fetch dynamically imported module,阁下该如何应对?

前言

在现代前端开发中,Vite 和 Vue 的结合已经成为构建高性能、模块化应用的主流选择。Vite 的热模块替换(HMR)和快速冷启动能力,加上 Vue 的响应式系统和组件化设计,为开发者提供了无与伦比的开发体验。然而,随着项目复杂度的增加和动态模块加载的广泛应用,开发者可能会遇到一些令人头疼的问题,比如 TypeError: Failed to fetch dynamically imported module

在本文中,我们将深入探讨这个报错的成因,无论你是初学者还是资深开发者,理解动态导入的机制和常见问题的解决方法,都是提升开发效率和项目健壮性的关键一步。接下来,让我们一起揭开这个报错的神秘面纱,探索 Vite 和 Vue 的动态模块加载世界。

错误产生的原因

这个错误是在import()语法动态导入一个ECMAScript模块,在浏览器加载这个模块失败时抛出的,有可能这个模块本身就不存在,也有可能是网络原因导致模块加载失败。

import()动态导入

  • 基本语法
typescript 复制代码
import(moduleName)

在引用的模块不存在时,浏览器会抛出 TypeError异常。参考MDN动态导入import()

项目中使用到这个语法的地方

  • 路由懒加载
    可以根据用户的导航动态加载对应的路由组件,而不是在应用启动时加载所有组件
typescript 复制代码
{
  name: 'PageA',
  path: 'page-a',
  component: () => import('src/views/page-a.vue')
}
  • 功能模块按需加载
    只有当用户触发某个功能时,才加载相关的模块代码
typescript 复制代码
// 动态导入
const loadSomeModule = async () => {
  const someModule = await import('./SomeModule.js');
  return someModule.default
};

使用import()语法目的主要是为了不在程序启动时加载所有模块。这种方式可以显著减少初始加载时间,提高应用的性能。

Vite打包后是如何处理import语法的?

通过npx create-vue@latest命令,快速生成一个Vue + Vite的项目,然后运行npm install && npm run build来看一下最终打包的结果到底是什么样子的。

可以看到路由router.js打包后的产物是下面这样的

javascript 复制代码
// _vite_Deps
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/HomeView-w0ePAOjH.js","assets/HomeView-BFujd8Wq.css","assets/AboutView-9y6kMFrI.js","assets/AboutView-DNtNIo95.css"])))=>i.map(i=>d[i]);

const router = createRouter({
  history: createWebHistory("/"),
  routes: [
    {
      path: "/",
      name: "home",
      component: () => __vitePreload(() => import("./HomeView-w0ePAOjH.js"), true ? __vite__mapDeps([0,1]) : void 0)
    },
    {
      path: "/about",
      name: "about",
      component: () => __vitePreload(() => import("./AboutView-9y6kMFrI.js"), true ? __vite__mapDeps([2,3]) : void 0)
    }
  ]
});

可以看到使用的预加载语法import()被保留了下来,而且还在他外面套了一层函数__vitePreload,这个函数就是Vite预加载资源的核心逻辑,接下来一起看下这个函数的代码。 其中__vite__mapDeps代表这个路由模块所依赖的其他的一些其他模块资源,包括cssjs

javascript 复制代码
const scriptRel = "modulepreload";
const assetsURL = function(dep) {
  return "/" + dep;
};
const seen = {};
/**
 * 预加载
 * @param {*} baseModule import语法动态导入的模块
 * @param {*} deps  模块依赖的其他js和css资源
 * @return Promise
 */
const __vitePreload = function preload(baseModule, deps, importerUrl) {
  let promise = Promise.resolve();
  if (deps && deps.length > 0) {
    document.getElementsByTagName("link");
    const cspNonceMeta = document.querySelector(
      "meta[property=csp-nonce]"
    );
    const cspNonce = (cspNonceMeta == null ? void 0 : cspNonceMeta.nonce) || (cspNonceMeta == null ? void 0 : cspNonceMeta.getAttribute("nonce"));
     // 使用Promise.allSettled 等待所有的模块都处理完成
    promise = Promise.allSettled(
      deps.map((dep) => {
        dep = assetsURL(dep);
        if (dep in seen) return;
        seen[dep] = true;
        const isCss = dep.endsWith(".css");
        const cssSelector = isCss ? '[rel="stylesheet"]' : "";
        // 如果页面已经存在资源,则跳过
        if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) {
          return;
        }
        // 创建link标签
        const link = document.createElement("link");
        // css直接通过stylesheet加载, js通过modulepreload进行预加载
        link.rel = isCss ? "stylesheet" : scriptRel;
        if (!isCss) {
          link.as = "script";
        }
        link.crossOrigin = "";
        link.href = dep;
        if (cspNonce) {
          link.setAttribute("nonce", cspNonce);
        }
         // 添加到head标签中
        document.head.appendChild(link);
        if (isCss) {
          // CSS 加载完毕后再执行,避免发生 FOUC 现象
          return new Promise((res, rej) => {
            link.addEventListener("load", res);
            // 加载失败 抛出错误
            // 这就是为什么线上有时候也会有 Unable to preload CSS for这类报错了
            link.addEventListener(
              "error",
              () => rej(new Error(`Unable to preload CSS for ${dep}`))
            );
          });
        }
      })
    );
  }
  function handlePreloadError(err) {
    // 创建一个vite:preloadError事件
    const e = new Event("vite:preloadError", {
      cancelable: true
    });
    e.payload = err;
    // 触发事件
    window.dispatchEvent(e);
    if (!e.defaultPrevented) {
      // 如果事件没有被调用event.preventDefault(),则抛出错误
      throw err;
    }
  }
  return promise.then((res) => {
    for (const item of res || []) {
      // 遍历所有资源的加载结果
      if (item.status !== "rejected") continue;
      // 加载失败,调用handlePreloadError处理错误
      handlePreloadError(item.reason);
    }
    // 依赖资源都处理完成,执行基础模块import()导入
    return baseModule().catch(handlePreloadError);
  });
};

如何处理这个错误

一把梭处理

按照Vite官方文档给出的方案,直接刷新页面

javascript 复制代码
window.addEventListener('vite:preloadError', (event) => {
  event.preventDefault()
  window.location.reload()
})

分场景处理

1、功能模块的处理

动态导入功能模块可能有以下两个原因

  • 用户点击了某个按钮触发
  • 拆包, 将一些不是首屏必须的包放到页面加载完成之后再加载,加快首屏渲染速度
javascript 复制代码
import('.someModule').then(module => {
  // 导入成功
}).catch(e => {
  // 导入失败
  // 可以根据业务 可以Toast提示 或者其他的操作
})

1、路由模块的处理

针对路由模块也可以有两种处理方式

  • 监听router.onError事件
    监听到加载报错后,直接使用location.href导航到目标页面
javascript 复制代码
router.onError((error, to) => {
  const errors = ['Failed to fetch dynamically imported module', 'Unable to preload CSS'];
  if (errors.some((e) => error.message.includes(e))) {
    window.location.href = to.fullPath;
  }
});
  • 如果异步路由组件加载失败, 降级显示一个加载失败提示组件
    这个降级组件具体显示的内容可以根据业务场景决定
javascript 复制代码
import { createRouter, createWebHistory } from 'vue-router'
import { h } from 'vue'

// 失败提示组件
const ErrorComponent = {
  render() {
    return h('div', '天哪,网络似乎出了点小问题~,找一个网好的地方试试?')
  }
}

function injectErrorComponent(importFun) {
  return () => importFun().catch(() => {
      return ErrorComponent
    })
  
}

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: injectErrorComponent(() => import('../views/HomeView.vue')),
    },
    {
      path: '/about',
      name: 'about',
      component: injectErrorComponent(() => import('../views/AboutView.vue')),
    },
  ],
})

export default router

结语

没有最好的方案,只有最适合的方案❤️。

相关推荐
林太白10 分钟前
企业级NestJS如何创建项目学起来
前端·vue.js·后端
橙某人10 分钟前
横向图片选择器之自动滚动定位功能-Javascript、Vue
前端·javascript·vue.js
.切切切 切萝卜28 分钟前
【编写Node接口;接口动态获取VUE文件并异步加载, 并渲染impoort插件使用】
vue.js·前端框架·vue
天天鸭39 分钟前
都2025了你不会用‘容器查询’适配屏幕?,只知道媒体查询?
前端·javascript·vue.js
Kagol1 小时前
🎉TinyPro v1.2.0 正式发布,趁着 TinyPro 项目刚创建不久,快来参与贡献(蹭 PR)吧!
前端·vue.js·nestjs
Michael.Scofield1 小时前
vue: router基础用法
前端·javascript·vue.js
lorogy11 小时前
【VSCode配置】运行springboot项目和vue项目
vue.js·spring boot·vscode
秋野酱13 小时前
基于 Spring Boot + Vue 的 [业务场景] 管理系统设计与实现
vue.js·spring boot·后端
勘察加熊人13 小时前
vue模拟扑克效果
javascript·css·vue.js
William Dawson13 小时前
如何在 Vue 3 中实现百度地图位置选择器组件
前端·javascript·vue.js