Vite项目大量使用import,如何通过Plugin提升用户体验

1,052 阅读3分钟

前言

公司的单页应用项目中有几十个页面,每个路由都是通过比较简单粗暴的方式配置component: () => import('xxx')。第一次体验这个项目时发现进入另一个页面时会白屏一段时间(主要取决于网络速度)用户体验较差,于是想起 Webpack 中支持使用 Magic Comments 为动态导入的资源添加 prefetchpreload

我翻阅了 Vite 官方文档许久,并未发现其提供了相关功能,最终在官方仓库 Issues 中找到这个需求的 Feature Request ,最后我决定尝试编写一个 Vite Plugin 完成这个需求。

防扣“造轮子”帽子说明:在我实现这个插件之前,此 Issue 中提到的 Plugin 并不能满足我的需求,因为这些插件非常粗暴的将所有通过 import 导入的资源都为其添加了 link 标签,然而我需要的是由使用者自行控制这种行为。

效果

Snipaste_2025-01-07_22-57-33.png

思路

解析模块

使用 AST 语法分析当前模块,得到需要标记为 prefetch or preload 的模块相对路径(moduleId),并解析为绝对路径

transform 中可以通过参数得到当前模块的源代码字符串和模块ID,通过 AST 语法分析后获取到 import(/* vitePrefetch: true */ 'xxx') 中的 xxx,以及 rel (prefetch or preload)

最后通过 this.resolve(xxx, 模块ID) 得到在工程目录下的绝对路径

async transform(code, id) {
  if (!filter(id)) {
    return null
  }

  const magicString = new MagicString(code)

  const modules = extractPreloadModules(code) // AST 语法分析得到模块相对路径数组
  const resolvedModules = await Promise.allSettled(modules.map(({ moduleId }) => this.resolve(moduleId, id)))
  resolvedModules.forEach((result, index) => {
    const { moduleId, rel } = modules[index]

    if (result.status === 'fulfilled') {
      dynamicImportModuleMap.set(result.value?.id, rel)
    } else {
      this.error(`Failed to resolve module: ${moduleId}. The reason is: ${result.reason}`)
    }
  })

  return {
    code: magicString.toString(),
    map: magicString.generateMap({ source: id, includeContent: true }),
  }
}

获取产物资源路径

在生成 bundle 时得到最终产物的资源路径

generateBundle(_, bundle) {
  const bundleEntries = Object.entries(bundle)
  for (const [fileName, file] of bundleEntries) {
    if (file.type !== 'chunk') {
      continue
    }

    const rel = dynamicImportModuleMap.get(file.facadeModuleId)
    if (rel) {
      preloadBundles.push({ moduleId: fileName, rel })
    }
  }
}

插入到 <head>

 transformIndexHtml(html) {
  return {
    html,
    tags: preloadBundles.map(({ rel, moduleId }) => ({
      tag: 'link',
      injectTo: 'head',
      attrs: {
        rel,
        ...(crossorigin ? { crossorigin: true } : {}),
        href: `${config.base}${moduleId}`,
      },
    })),
  }
}

安装

# yarn
yarn add vite-plugin-magic-preloader -D
# npm
npm install vite-plugin-magic-preloader -D
# pnpm
pnpm add vite-plugin-magic-preloader -D

使用

  • vite.config.ts 中配置插件
import { defineConfig } from 'vite'
import magicPreloader from 'vite-plugin-magic-preloader';

export default defineConfig({
  plugins: [magicPreloader()],
});

选项

参数类型默认值说明
includestring | RegExp | (string | RegExp)[]/\.(js|ts|jsx|tsx)$/需要处理的文件
excludestring | RegExp | (string | RegExp)[]/node_modules/排除的文件
crossoriginbooleantrue是否启用 crossorigin

include

需要处理的依赖项,支持字符串、正则表达式、数组类型。默认情况下只处理不在 node_modules 中的 js, ts, jsx, tsx 文件

被命中的文件将会被当作 JavaScript 代码解析,请确保文件内容能被正确解析为 AST

exclude

排除的依赖项,支持字符串、正则表达式、数组类型

示例

const router = [
  {
    path: '/',
    component: () => import(/* vitePrefetch: true */ './views/Home.vue'),
  },
  {
    path: '/about',
    component: () => import(/* vitePreload: true */ './views/About.vue'),
  },
];

若需要在 Vue 单文件组件中也使插件生效,请确保 vite-plugin-magic-preloader@vitejs/plugin-vue 插件之后加载

import vue from '@vitejs/plugin-vue';
import magicPreloader from 'vite-plugin-magic-preloader';

export default defineConfig({
  plugins: [vue(), magicPreloader()],
});

最后

如果此插件也帮助到你的话,欢迎 star。目前还有不足之处,例如不支持 import.meta.glob,欢迎有兴趣完善的朋友 PR。

仓库地址:github.com/cszhjh/vite…

感谢 chouchouji(🐔哥) 对此 repo 做出的贡献

此插件已被 awesome-vite 官方收录 ➡️ here