兩個(gè)簡單的自定義插件,探究Vite的插件機(jī)制
?? Vite 插件機(jī)制
Vite 的插件機(jī)制是基于 Rollup 的插件機(jī)制實(shí)現(xiàn)的,但是又進(jìn)行了一些擴(kuò)展。Vite 的插件機(jī)制是通過鉤子函數(shù)實(shí)現(xiàn)的,當(dāng) Vite 運(yùn)行時(shí),會通過鉤子函數(shù)調(diào)用插件中的方法,插件可以在這些方法中干預(yù) Vite 的構(gòu)建過程。
我們主要討論插件的機(jī)制,Api詳情請看官網(wǎng)介紹
通用的鉤子:https://cn.vitejs.dev/guide/api-plugin.html#universal-hooks
Vite 獨(dú)有的鉤子:https://cn.vitejs.dev/guide/api-plugin.html#vite-specific-hooks
下面我們看看插件的原理。
?? Rollup 插件機(jī)制
Rollup 的插件機(jī)制實(shí)現(xiàn)主要基于兩點(diǎn):
- Rollup 維護(hù)了各個(gè)插件接口的 Hook 列表,插件可以向這些列表中添加回調(diào)函數(shù)。
- 在執(zhí)行對應(yīng)過程時(shí),Rollup 會依次觸發(fā)這些 Hook 列表中的回調(diào)函數(shù)。
const hookLists = {
load: [] // load hook 列表
}
function addHook(hookName, hook) {
hookLists[hookName].push(hook) // 向 hook 列表中添加回調(diào)函數(shù)
}
function load(id) {
for (const hook of hookLists.load) { // 觸發(fā)所有 load 鉤子函數(shù)
const result = hook(id) // 調(diào)用鉤子函數(shù)
if (result) return result // 使用第一個(gè)結(jié)果并返回
}
}
插件可以通過 Rollup 提供的 addHook 方法相對應(yīng)的 Hook 列表中添加回調(diào)函數(shù):
export function myPlugin() {
addHook('load', id => { // 向 load 列表添加回調(diào)函數(shù)
// ...
})
}
?? Vite 的巧妙之處
Vite 主要將用戶插件排序,然后和內(nèi)置的插件配置合并,傳遞給了 Rollup 打包。
關(guān)鍵的部分源碼如下:
// 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]
// ...
}
// 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 使用插件時(shí),需要將插件放入 plugins 的數(shù)組中如下:
插件配置
?? 實(shí)踐得真知
接下來我們自定義幾個(gè)插件,感受下 Vite 的插件機(jī)制。
寫這幾個(gè)插件是為了理解插件機(jī)制,官方已經(jīng)提供了相關(guān)的配置或者現(xiàn)成的插件
?? 自動切換端口,默認(rèn)8080
Vite 默認(rèn)的端口不是 8080了,有點(diǎn)不太習(xí)慣,所以自己寫個(gè)插件自動切換端口。
import net from 'net'
function getNextPort(port: number) {
return new Promise((resolve) => {
const server = net.createServer()
server.unref()
server.on('error', () => {
resolve(getNextPort(port + 1))
})
server.listen(port, () => {
server.close(() => {
resolve(port)
})
})
})
}
function autoSwitchPortPlugin() {
let port = 8080
return {
name: 'auto-switch-port',
async configResolved(config: any) {
port = await getNextPort(port) as number
config.server.port = port
},
}
}
export default autoSwitchPortPlugin
修改端口
?? 為文件加上版本號
由于這個(gè)操作是轉(zhuǎn)換 index.html文件,所以需要使用專用鉤子transformIndexHtml
import { createHash } from "crypto"
export default function autoVersionPlugin() {
return {
name: 'auto-version',
async transformIndexHtml(html: string) {
const hash = createHash('md5').update(html).digest('hex')
return html.replace(/(src|href)="(.*?)"/g, `$1="$2?v=${hash}"`)
},
}
}
添加版本號
?? 總結(jié)
Vite 插件機(jī)制主要在整個(gè)構(gòu)建過程的不同時(shí)機(jī)暴露出鉤子函數(shù)供開發(fā)者靈活自定義構(gòu)建過程。所以理解構(gòu)建流程,才能更好的開發(fā)一個(gè)優(yōu)秀的插件。