前言
公司的单页应用项目中有几十个页面,每个路由都是通过比较简单粗暴的方式配置component: () => import('xxx')
。第一次体验这个项目时发现进入另一个页面时会白屏一段时间(主要取决于网络速度)用户体验较差,于是想起 Webpack 中支持使用 Magic Comments 为动态导入的资源添加 prefetch 或 preload。
我翻阅了 Vite 官方文档许久,并未发现其提供了相关功能,最终在官方仓库 Issues 中找到这个需求的 Feature Request ,最后我决定尝试编写一个 Vite Plugin 完成这个需求。
防扣“造轮子”帽子说明:在我实现这个插件之前,此 Issue 中提到的 Plugin 并不能满足我的需求,因为这些插件非常粗暴的将所有通过
import
导入的资源都为其添加了link
标签,然而我需要的是由使用者自行控制这种行为。
效果
思路
解析模块
使用 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()],
});
选项
参数 | 类型 | 默认值 | 说明 |
---|---|---|---|
include | string | RegExp | (string | RegExp)[] | /\.(js|ts|jsx|tsx)$/ | 需要处理的文件 |
exclude | string | RegExp | (string | RegExp)[] | /node_modules/ | 排除的文件 |
crossorigin | boolean | true | 是否启用 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。
感谢 chouchouji(🐔哥) 对此 repo 做出的贡献
此插件已被 awesome-vite 官方收录 ➡️ here