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

结语

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

相关推荐
武昌库里写JAVA5 小时前
iview table组件 自定义表头
vue.js·spring boot·毕业设计·layui·课程设计
武昌库里写JAVA5 小时前
iview 分页改变每页条数时请求两次问题
vue.js·spring boot·毕业设计·layui·课程设计
计算机学姐6 小时前
基于SpringBoot的同城宠物照看管理系统
java·vue.js·spring boot·后端·mysql·mybatis·宠物
松树戈13 小时前
idea结合CopilotChat进行样式调整实践
前端·javascript·vue.js·copilot
xiegwei14 小时前
使用Vite创建vue3项目
vue·vite
漫无目的行走的月亮14 小时前
VUE实现todolist
前端·vue.js·elementui
观无15 小时前
Nginx发布Vue(ElementPlus),与.NETCore对接(腾讯云)
vue.js·nginx·.netcore
2501_9153738815 小时前
vite入门教程
vue.js
香蕉可乐荷包蛋17 小时前
Three.js在vue中的使用(二)-动画、材质
javascript·vue.js·材质
green_pine_1 天前
Vue3学习笔记2——路由守卫
前端·vue.js·笔记·学习