我点燃了火,却控制不了它。 ---刘慈欣-
简介
Viite 的插件机制是基于 Rollup 的插件机制实现的,但是又进行了一些扩展。Vite 的插件机制是通过钩子函数实现的,当 Vite 运行时,会通过钩子函数调用插件中的方法,插件可以在这些方法中干预 Vite 的构建过程。
如果插件不使用 Vite 特有的钩子,可以作为 兼容 Rollup 的插件 来实现,推荐使用 Rollup 插件名称约定。
- Rollup 插件应该有一个带 rollup-plugin- 前缀、语义清晰的名称。
- 在 package.json 中包含 rollup-plugin 和 vite-plugin 关键字。
这样,插件也可以用于纯 Rollup 或基于 WMR 的项目。
对于 Vite 专属的插件:
- Vite 插件应该有一个带 vite-plugin- 前缀、语义清晰的名称。
- 在 package.json 中包含 vite-plugin 关键字。
- 在插件文档增加一部分关于为什么本插件是一个 Vite 专属插件的详细说明(如,本插件使用了 Vite 特有的插件钩子)。
如果你的插件只适用于特定的框架,它的名字应该遵循以下前缀格式:
- vite-plugin-vue- 前缀作为 Vue 插件
- vite-plugin-react- 前缀作为 React 插件
- vite-plugin-svelte- 前缀作为 Svelte 插件
更多详情参见 虚拟模块的相关内容.
Rollup 插件机制
Rollup 的插件机制实现主要基于两点:
- Rollup 维护了各个插件接口的 Hook 列表,插件可以向这些列表中添加回调函数。
- 在执行对应过程时,Rollup 会依次触发这些 Hook 列表中的回调函数。
js
const hookLists = {
load: [] // load hook 列表
}
function addHook(hookName, hook) {
hookLists[hookName].push(hook) // 向 hook 列表中添加回调函数
}
function load(id) {
for (const hook of hookLists.load) { // 触发所有 load 钩子函数
const result = hook(id) // 调用钩子函数
if (result) return result // 使用第一个结果并返回
}
}
插件可以通过 Rollup 提供的 addHook 方法相对应的 Hook 列表中添加回调函数:
js
export function myPlugin() {
addHook('load', id => { // 向 load 列表添加回调函数
// ...
})
}
Vite
Vite 主要将用户插件排序,然后和内置的插件配置合并,传递给了 Rollup 打包。
关键的部分源码如下:
js
// vite/node/config.ts
export async function resolveConfig() {
// ...
// resolve plugins
const rawUserPlugins = (
(await asyncFlatten(config.plugins || [])) as Plugin[]
).filter(filterPlugin)
const [prePlugins, normalPlugins, postPlugins] =
sortUserPlugins(rawUserPlugins)
// run config hooks
const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins]
// ...
}
js
// vite/node/build.ts
export async function build() {
const config = await resolveConfig(
inlineConfig,
'build',
'production',
'production',
)
//...
const plugins = (
ssr ? config.plugins.map((p) => injectSsrFlagToHooks(p)) : config.plugins
) as Plugin[]
const rollupOptions: RollupOptions = {
context: 'globalThis',
preserveEntrySignatures: ssr
? 'allow-extension'
: libOptions
? 'strict'
: false,
cache: config.build.watch ? undefined : false,
...options.rollupOptions,
input,
plugins,
external,
onwarn(warning, warn) {
onRollupWarning(warning, warn, config)
},
}
// ...
// write or generate files with rollup
const { rollup } = await import('rollup')
bundle = await rollup(rollupOptions)
// ...
}
使用
Vite 使用插件时,需要将插件放入 plugins 的数组中如下:
js
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import autoSwitchPortPlugin from "./src/utils/autoSwitchPortPlugin.js";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), autoSwitchPortPlugin()],
})
实践
接下来我们自定义几个插件,感受下 Vite 的插件机制。
写这几个插件是为了理解插件机制,官方已经提供了相关的配置或者现成的插件
🚗 自动切换端口,默认8080
编写一个 Vite 插件,将项目的默认端口设置为 8080,当8080 被占用的时候并自动+1 寻找下一个可用端口。
js
import net from 'net'
/**
* 获取下一个可用端口
* 在这段代码中,server的作用是查找并确定下一个可用的TCP端口。通过创建一个未绑定到特定端口的TCP服务器实例,并尝试在指定的起始端口上监听连接请求:
* 使用net.createServer()创建TCP服务器。
* 调用.unref()方法确保当没有活动连接时,Node.js进程可以正常退出,不影响其他任务执行或进程结束。
* 在server.listen(port, ...)中,尝试在给定的端口上启动服务器监听。如果端口已被占用,则会触发错误事件,递归调用getNextPort函数以获取下一个端口继续尝试。
* 若成功监听到指定端口,则关闭服务器(server.close(...)),并使用resolve(port)返回当前找到的可用端口。
* 整个流程的核心目的是实现一个自动切换端口的插件功能,该功能能够帮助找到下一个可用的TCP端口供服务器应用使用。
* @param {number} port - 起始端口默认 8080,当被占用,自动递增 port
* @returns {Promise<number>} - Promise对象,resolve时返回下一个可用端口
*/
function getNextPort(port) {
return new Promise((resolve) => {
// net.createServer()是一个函数调用,它创建了一个新的TCP服务器实例。这个服务器等待并处理来自客户端的连接请求。
const server = net.createServer()
/**
* 是对服务器实例调用.unref()方法。此方法的作用是将服务器从事件循环(Event Loop)中的引用计数中移除。
* 当所有活动的引用都被取消引用(unref)后,且没有其他任务在执行时,Node.js进程可以正常退出,即使服务器仍在监听连接。
* 换句话说,如果仅有一个对服务器的引用,并对其调用了unref,那么当服务器上没有活动连接时,
* 主线程在完成其他任务后将能够优雅地关闭,而不会因为服务器还在监听而继续保持运行状态。
*/
server.unref()
// 错误处理
server.on('error', () => {
resolve(getNextPort(port + 1))
})
// 监听指定端口
server.listen(port, () => {
server.close(() => {
resolve(port)
})
})
})
}
/**
* 自动切换端口插件
* @returns {object} - 包含name和configResolved方法的对象
*/
function autoSwitchPortPlugin() {
let port = 8080
return {
name: 'auto-switch-port',
/**
* 在配置resolved时动态切换服务器端口
* @param {object} config - 服务器配置对象
*/
async configResolved(config) {
port = await getNextPort(port)
config.server.port = port
},
}
}
export default autoSwitchPortPlugin
🚗 为文件加上版本号
由于这个操作是转换 index.html文件,所以需要使用专用钩子transformIndexHtml
js
import { createHash } from "crypto"
/**
* 自动版本插件
*
* @returns {Object} - 包含 transformIndexHtml 方法的对象
*/
export default function autoVersionPlugin() {
return {
/**
* 组件名称
*
* @type {string}
*/
name: 'auto-version',
/**
* 转换首页 HTML
*
* @param {string} html - 需要转换的 HTML 字符串
* @returns {string} - 转换后的 HTML 字符串
*/
async transformIndexHtml(html) {
/**
* 创建哈希对象
*
* @type {Object}
*/
// 创建一个md5的哈希对象
const hash = createHash('md5').update(html).digest('hex')
// 将html中的所有(src|href)="*"替换为(src|href)="*?v=hash",其中hash为上面计算得到的哈希值
return html.replace(/(src|href)="(.*?)"/g, `$1="$2?v=${hash}"`)
},
}
}
在编译后的 dist 目录发现所有外部资源的尾部都添加了 hash 值
引用
通用的钩子Vite
Vite 独有的钩子Vite
rollupRollup