外观
祖传开篇:本文简单介绍vite插件的定义以及如何开发一个简单的vite 插件。作者水平有限,可能比较简陋,也或许有些错误,欢迎指正。
关于vite
Vite 是一个现代化的前端构建工具,它的设计理念是为了提供快速的开发体验,尤其在开发过程中,Vite 使用了非常先进的技术栈,如原生 ES 模块支持和高速的 HMR(热模块替换)。Vite 提供了强大的插件机制,使得开发者可以根据需求定制构建过程、扩展功能、优化性能等。
什么是 vite 插件
Vite 插件是对 Vite 构建过程的扩展,它通过实现 Vite 提供的钩子函数,修改 Vite 的行为或添加自定义功能。
基本用于:
- 修改模块的解析方式。
- 修改源代码或构建输出。
- 引入新的构建工具和功能。
- 改善开发和构建性能。
Vite 插件的 API 非常灵活,并且与 Rollup 插件 API 相似,因为 Vite 本身基于 Rollup 构建工具,所以也能使用 Rollup 的API。
Vite插件开发基础
插件的基本结构
js
export default function myPlugin() {
return {
name: 'my-plugin', // 插件的名称
// 钩子函数...
};
}
- name: 每个插件都需要一个独特的名称。
- 你可以在插件中实现多个钩子函数来定义插件的行为。
插件排序
为了与某些 Rollup 插件兼容,可能需要强制修改插件的执行顺序,或者只在构建时使用。这应该是 Vite 插件的实现细节。可以使用 enforce 修饰符来强制插件的位置:
- pre:在 Vite 核心插件之前调用该插件
- 默认:在 Vite 核心插件之后调用该插件
- post:在 Vite 构建插件之后调用该插件
js
export default function myPlugin() {
return {
name: 'my-plugin', // 插件的名称
enforce: 'pre',
// 钩子函数...
};
}
- Alias (路径别名)相关的插件。
- ⭐ 带有 enforce: 'pre' 的用户插件。
- Vite 核心插件。
- ⭐ 没有 enforce 值的用户插件,也叫 普通插件。
- Vite 生产环境构建用的插件。
- ⭐ 带有 enforce: 'post' 的用户插件。
- Vite 后置构建插件(如压缩插件)
按照不同环境调用
默认情况下插件在开发 (serve) 和生产 (build) 模式中都会调用。如果插件在服务或构建期间按需使用,请使用 apply 属性指明它们仅在 'build' 或 'serve' 模式时调用
js
export default function myPlugin() {
return {
name: 'my-plugin', // 插件的名称
enforce: 'pre',
apply: 'build',
// 钩子函数...
};
}
Vite 与 Rollup 兼容的钩子
- 服务启动阶段:
options
和buildStart
- 每个传入模块请求时:当浏览器发起请求获取页面内容时,Vite 依次调用
resolveid
、load
和transform
- 服务关闭阶段:依次调用
buildEnd
和closeBundle
Vite 独有的
config
用来进一步修改配置。configResolved
用来记录最终的配置信息。configureServer
用来获取 Vite Dev Server 实例,添加中间件。configurePreviewServer
在其他中间件安装前被调用。 如果想在 其他中间件 之后 安装一个插件 可以返回一个钩子,会在其他中间件安装完成后调用transformIndexHtml
用来转换 HTML 的内容。(这个后面的案例会用到)handleHotUpdate
用来进行热更新模块的过滤,或者进行自定义的热更新处理
官方文档写的很详细,这里就不多写了。 官方文档
执行顺序
- 配置阶段
- config
- configResolved
- 开发服务器阶段(仅开发模式)
- configureServer
- 构建阶段
- options
- buildStart
- resolveId
- load
- transform
- buildEnd
- 生成阶段
- generateBundle
- HTML 处理阶段
- transformIndexHtml
configureServer
和 configurePreviewServer
的区别在于前者用于开发模式,后者用于预览模式。
handleHotUpdate
仅在开发模式下生效,因为预览模式不支持热更新。
如果需要更细粒度的控制,可以在 handleHotUpdate
中返回特定的模块列表,或者返回空数组以阻止热更新。
开发两个简单的vite 插件
插件一:内容替换插件
首先来一个简单的内容替换插件 在src下 新建 plugins 目录,新建 my-plugin-console.ts
ts
import type {Plugin} from 'vite';
// 定义插件选项类型
interface myPluginConsoleOptions {
enabled: boolean;
style?: string;
cleanAll?: boolean;
}
export default function myPluginConsole(options: myPluginConsoleOptions): Plugin {
return {
name: 'my-plugin-console',
enforce: 'pre', // 在 Vite 核心插件之前调用该插件
apply: 'build', // 只在构建模式下应用插件
// 在 vite 构建时,在代码转换阶段调用该插件
transform(code, id) {
// 清除所有的console.log
if (options.cleanAll) {
code = code.replace(/console\.log\(([^)]*)\)/g, '')
return {code};
}
// 替换console.log的样式(这里使用拼接语法,所以只有基本类型能够log展示)
if (options.enabled && (id.endsWith('.ts') || id.endsWith('.js') || id.endsWith('.vue'))) {
code = code.replace(/(console\.log\(([^)]*)\))/g, (_, __, argumentsContent) => {
return 'console.log("这是修改后的log日志:%c" + ' + argumentsContent + ' , " ' + options.style + ' ");';
});
return {code};
}
return null;
}
};
}
在 vite.config.ts 中引入插件
ts
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import myPluginConsole from './src/plugins/my-plugin-console.ts';
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(),
myPluginConsole({
enabled: true,
style: 'color:#fff; background: #333;padding:2px;border-radius:3px'
}),
]
})
插件效果:
插件二:图片预加载
图片预加载插件,用于在页面加载时预加载图片,以优化用户体验。 在 src 的 plugins 中新建: vite-plugin-image-prefetch.ts
ts
import type {Plugin} from 'vite';
import fg from 'fast-glob'
interface ImagePrefetchOptions {
/**
* 预加载图片的目录
* @default 'images'
*/
dir: string[]
/**
* 预加载图片的标签
* prefetch 优先级较低、页面空闲时加载、使用时需要再次请求(走缓存)
* preload 优先级较高、加载后存到内存中、无须再次请求
* @default 'prefetch'
*/
rel: 'prefetch' | 'preload'
/**
* 构建后图片存放的目录
* @default 'public'
*/
buildPublicDir: string
/**
* 图片最大同时预加载数量
* @default 6 (浏览器并发最大6个)
*/
maxLoadSize?: number
}
export function imagePrefetch(options: ImagePrefetchOptions): Plugin {
const {dir, rel, buildPublicDir} = options
// 给最大加载数量设置默认值,默认为6 也是浏览器并发的上限
const maxLoadSize = options.maxLoadSize || 6
let imageArray: string[] = []
return {
name: 'vite-plugin-image-prefetch',
// 获取构建后图片路径
generateBundle(_, bundle) {
// 因为可以多路径,这里只需要处理 src路径下的图片,public 中的图片不需要处理
const dirArray = dir.filter(dirItem => dirItem.includes('src'))
if (dirArray.length > 0) {
const values = Object.values(bundle)
let files: string[] = []
dirArray.forEach(dirItem => {
files = [...files, ...fg.sync(dirItem)]
})
values.forEach(value => {
if (files.includes(Reflect.get(value, 'originalFileName'))) {
imageArray.push(value.fileName)
}
})
}
},
transformIndexHtml(html, ctx) {
// 判断是本地调试还是生产构建,
// 本地调试则直接获取路径,生产构建需要在generateBundle (rollup 钩子) 中获取构建后路径
let dirArray = ctx.server ? dir : dir.filter(dirItem => !dirItem.includes('src'))
if (dirArray.length > 0) {
dirArray.forEach(dirItem => {
// 这里获取src路径下的图片时不需要额外拼接 publicDir
const files = dirItem.includes('src/') ?
fg.sync(dirItem)
: fg.sync(dirItem, {
cwd: ctx.server ? ctx.server.config.publicDir : buildPublicDir
})
if (files.length > 0) {
imageArray = [...imageArray, ...files.map(file => (ctx.server ? ctx.server.config.base : '') + file)]
} else {
console.warn(`vite-plugin-image-prefetch dir ${dirItem} is empty`)
}
})
// 设置图片预加载并发
const scriptContent = `
(function() {
const imageArray = ${JSON.stringify(imageArray)};
const maxConcurrent = ${maxLoadSize}; // 最大并发数
let index = 0;
const loadImage = (src) => {
return new Promise((resolve, reject) => {
const link = document.createElement('link')
link.rel = '${rel}'
link.as = 'image'
link.href = src
document.head.appendChild(link)
link.onload = resolve
link.onerror = reject
})
};
const loadImagesWithLimit = () => {
const loadNext = () => {
if (index < imageArray.length) {
const currentSrc = imageArray[index];
index++;
loadImage(currentSrc).finally(() => {
loadNext(); // 递归加载下一个图片
})
}
};
// 控制并发数量
const loadImagePromises = Array.from({ length: maxConcurrent }, loadNext);
};
// 开始加载图片
loadImagesWithLimit();
})();
`
// 将脚本内容添加到 HTML 中
html = html.replace(
'</body>', // 在 </body> 标签前插入脚本
`<script>${scriptContent}</script></body>`
);
} else {
console.warn('vite-plugin-image-prefetch dir is empty')
}
return html
}
}
}
在 vite.config.ts 中引入插件
ts
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import myPluginConsole from './src/plugins/my-plugin-console.ts';
import {imagePrefetch} from './src/plugins/vite-plugin-image-prefetch.ts';
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(),
myPluginConsole({
enabled: true,
style: 'color:#fff; background: #333;padding:2px;border-radius:3px'
}),
imagePrefetch({
dir: ['image/*.{jpg,png,svg,webp}', 'src/assets/image/*.{jpg,png,svg,webp}'],
rel: 'preload',
buildPublicDir: 'public/',
maxLoadSize: 3
})
]
})
效果展示: