淺析Vite插件機制,你學(xué)會了嗎?
前言
對于Vite來說,它是基于esbuild與rollup雙引擎設(shè)計的,在開發(fā)階段使用esbuild進行依賴預(yù)構(gòu)建,然后基于瀏覽器原生支持的ESM完成開發(fā)預(yù)覽,而在生產(chǎn)環(huán)境打包時,直接使用的rollup構(gòu)建。那么在這種背景下,Vite的插件機制應(yīng)該如何設(shè)計?
在源碼中,我們能夠經(jīng)??吹絇luginContainer的身影,Vite正是通過它來模擬rollup的行為
pluginContainer
PluginContainer 的 實現(xiàn) 基于借鑒于 WMR 中的 rollup-plugin-container.js ,主要功能有兩個:
- 管理插件的生命周期
- 實現(xiàn)插件鉤子內(nèi)部的 Context 上下文對象
插件生命周期
在開發(fā)階段,vite會模擬rollup的行為,所以插件的執(zhí)行機制也與rollup相同
圖片
- 調(diào)用 options 鉤子進行配置的轉(zhuǎn)換,得到處理后的配置對象。
- 調(diào)用buildStart鉤子,正式開始構(gòu)建流程。
- 調(diào)用 resolveId 鉤子中解析文件路徑。(從 input 配置指定的入口文件開始)。
- 調(diào)用load鉤子加載模塊內(nèi)容。
- 緊接著 Rollup 執(zhí)行所有的 transform 鉤子來對模塊內(nèi)容進行進行自定義的轉(zhuǎn)換(比如 babel 轉(zhuǎn)譯)
- Rollup 拿到最后的模塊內(nèi)容,進行 AST 分析,得到所有的 import 內(nèi)容,調(diào)用 moduleParsed 鉤子
- 直到所有的 import 都解析完畢,Rollup 執(zhí)行buildEnd鉤子,Build 階段結(jié)束。
?
這里需要注意的是:在 vite 中由于 AST 分析是通過 esbuild 進行的,所有沒有模擬 moduleParsed 鉤子
傳遞上下文對象
上下文對象通過 Context 實現(xiàn) PluginContext 接口定義,PluginContext 實際上是 Rollup 內(nèi)部定義的類型,可以在源碼中看到 vite 實現(xiàn)了 Rollup 上下文對象
class Context implements PluginContext {
//... 具體實現(xiàn)
}
type PluginContext = Omit<
RollupPluginContext, // Rollup 定義插件上下文接口
// not documented
| 'cache'
// deprecated
| 'moduleIds'
>
Context 上下文對象一共有 14 個核心方法,其中有 3 個方法是比較核心的方法
- parse:使用 Rollup 的內(nèi)部基于 SWC 的解析器將代碼解析為 AST
- resolve:將相對路徑解析為絕對路徑,從而正確地處理模塊之間的引用
- load:加載并解析與給定 ID 對應(yīng)的模塊,并在提供的情況下附加附加的元信息到模塊
更多內(nèi)容可以查看rollup文檔
rollup插件
rollup構(gòu)建流程主要分為兩大類:build和output,build 階段主要負(fù)責(zé)創(chuàng)建模塊依賴圖,初始化各個模塊的 AST 以及模塊之間的依賴關(guān)系。output階段才是真正的打包構(gòu)建過程
插件hook類型(構(gòu)建階段)
通過構(gòu)建流程rollup的hook類型可以分為:build hook和output hook兩大類
- Build Hook即在Build階段執(zhí)行的鉤子函數(shù),在這個階段主要進行模塊代碼的轉(zhuǎn)換、AST 解析以及模塊依賴的解析,那么這個階段的 Hook 對于代碼的操作粒度一般為模塊級別,也就是單文件級別。
- Ouput Hook(官方稱為Output Generation Hook),則主要進行代碼的打包,對于代碼而言,操作粒度一般為 chunk級別(一個 chunk 通常指很多文件打包到一起的產(chǎn)物)。
插件hook類型(執(zhí)行方式)
除了上面這種分類,rollup插件還可以根據(jù)各自的執(zhí)行方式來進行分類:
- Build Hook即在Build階段執(zhí)行的鉤子函數(shù),在這個階段主要進行模塊代碼的轉(zhuǎn)換、AST 解析以及模塊依賴的解析,那么這個階段的 Hook 對于代碼的操作粒度一般為模塊級別,也就是單文件級別。
- Ouput Hook(官方稱為Output Generation Hook),則主要進行代碼的打包,對于代碼而言,操作粒度一般為 chunk級別(一個 chunk 通常指很多文件打包到一起的產(chǎn)物)。
除了根據(jù)構(gòu)建階段可以將 Rollup 插件進行分類,根據(jù)不同的 Hook 執(zhí)行方式也會有不同的分類,主要包括Async、Sync、Parallel、Squential、First這五種。在后文中我們將接觸各種各樣的插件 Hook,但無論哪個 Hook 都離不開這五種執(zhí)行方式。
1. Async & Sync
首先是Async和Sync鉤子函數(shù),兩者其實是相對的,分別代表異步和同步的鉤子函數(shù),這個很好理解。
2. Parallel
這里指并行的鉤子函數(shù)。如果有多個插件實現(xiàn)了這個鉤子的邏輯,一旦有鉤子函數(shù)是異步邏輯,則并發(fā)執(zhí)行鉤子函數(shù),不會等待當(dāng)前鉤子完成(底層使用 Promise.all)。
比如對于Build階段的buildStart鉤子,它的執(zhí)行時機其實是在構(gòu)建剛開始的時候,各個插件可以在這個鉤子當(dāng)中做一些狀態(tài)的初始化操作,但其實插件之間的操作并不是相互依賴的,也就是可以并發(fā)執(zhí)行,從而提升構(gòu)建性能。反之,對于需要依賴其他插件處理結(jié)果的情況就不適合用 Parallel 鉤子了,比如 transform。
3. Sequential
Sequential 指串行的鉤子函數(shù)。這種 Hook 往往適用于插件間處理結(jié)果相互依賴的情況,前一個插件 Hook 的返回值作為后續(xù)插件的入?yún)?,這種情況就需要等待前一個插件執(zhí)行完 Hook,獲得其執(zhí)行結(jié)果,然后才能進行下一個插件相應(yīng) Hook 的調(diào)用,如transform。
4. First
如果有多個插件實現(xiàn)了這個 Hook,那么 Hook 將依次運行,直到返回一個非 null 或非 undefined 的值為止。比較典型的 Hook 是 resolveId,一旦有插件的 resolveId 返回了一個路徑,將停止執(zhí)行后續(xù)插件的 resolveId 邏輯。
通用hook
以下鉤子在服務(wù)器啟動時被調(diào)用:
- options
- buildStart
以下鉤子會在每個傳入模塊請求時被調(diào)用:
- resolveId
- load
- transform
它們還有一個擴展的 options 參數(shù),包含其他特定于 Vite 的屬性。
以下鉤子在服務(wù)器關(guān)閉時被調(diào)用:
- buildEnd
- closeBundle
請注意 moduleParsed 鉤子在開發(fā)中是 不會 被調(diào)用的,因為 Vite 為了性能會避免完整的 AST 解析。
output階段的hook(除了 closeBundle) 在開發(fā)中是 不會 被調(diào)用的。你可以認(rèn)為 Vite 的開發(fā)服務(wù)器只調(diào)用了 rollup.rollup() 而沒有調(diào)用 bundle.generate()。
Vite 獨有hook
Vite 插件也可以提供鉤子來服務(wù)于特定的 Vite 目標(biāo)。當(dāng)然這些鉤子會被 Rollup 忽略。
config
在解析 Vite 配置前調(diào)用。鉤子接收原始用戶配置(命令行選項指定的會與配置文件合并)和一個描述配置環(huán)境的變量,包含正在使用的 mode 和 command。它可以返回一個將被深度合并到現(xiàn)有配置中的部分配置對象,或者直接改變配置(如果默認(rèn)的合并不能達到預(yù)期的結(jié)果)。
// 返回部分配置(推薦)
const partialConfigPlugin = () => ({
name: 'nanjiu-plugin',
config(config, { command }) {
console.log('config', config, command)
}
})
圖片
需要注意的是:用戶插件在運行這個鉤子之前會被解析,因此在 config 鉤子中注入其他插件不會有任何效果。
configResolved
在解析 Vite 配置后調(diào)用。使用這個鉤子讀取和存儲最終解析的配置。
const examplePlugin = () => {
let config
return {
name: 'read-config',
configResolved(resolvedConfig) {
// 存儲最終解析的配置
config = resolvedConfig
},
// 在其他鉤子中使用存儲的配置
transform(code, id) {
if (config.command === 'serve') {
// dev: 由開發(fā)服務(wù)器調(diào)用的插件
} else {
// build: 由 Rollup 調(diào)用的插件
}
},
}
}
configureServer
是用于配置開發(fā)服務(wù)器的鉤子。最常見的用例是在內(nèi)部 connect 應(yīng)用程序中添加自定義中間件
const myPlugin = () => ({
name: 'configure-server',
configureServer(server) {
server.middlewares.use((req, res, next) => {
// 自定義請求處理...
})
},
})
configurePreviewServer
與 configureServer 相同,但用于預(yù)覽服務(wù)器。configurePreviewServer 這個鉤子與 configureServer 類似,也是在其他中間件安裝前被調(diào)用。如果你想要在其他中間件 之后 安裝一個插件,你可以從 configurePreviewServer 返回一個函數(shù),它將會在內(nèi)部中間件被安裝之后再調(diào)用
const myPlugin = () => ({
name: 'configure-preview-server',
configurePreviewServer(server) {
// 返回一個鉤子,會在其他中間件安裝完成后調(diào)用
return () => {
server.middlewares.use((req, res, next) => {
// 自定義處理請求 ...
})
}
},
})
transformIndexHtml
轉(zhuǎn)換 index.html 的專用鉤子。鉤子接收當(dāng)前的 HTML 字符串和轉(zhuǎn)換上下文。上下文在開發(fā)期間暴露ViteDevServer實例,在構(gòu)建期間暴露 Rollup 輸出的包。
這個鉤子可以是異步的,并且可以返回以下其中之一:
- 經(jīng)過轉(zhuǎn)換的 HTML 字符串
- 注入到現(xiàn)有 HTML 中的標(biāo)簽描述符對象數(shù)組({ tag, attrs, children })。每個標(biāo)簽也可以指定它應(yīng)該被注入到哪里(默認(rèn)是在 <head> 之前)
- 一個包含 { html, tags } 的對象
默認(rèn)情況下 order 是 undefined,這個鉤子會在 HTML 被轉(zhuǎn)換后應(yīng)用。為了注入一個應(yīng)該通過 Vite 插件管道的腳本, order: 'pre' 指將在處理 HTML 之前應(yīng)用。order: 'post' 是在所有未定義的 order 的鉤子函數(shù)被應(yīng)用后才應(yīng)用。
const htmlPlugin = () => {
return {
name: 'nanjiu-plugin',
transformIndexHtml(html) {
return html.replace(/<title>(.*?)<\/title>/,
`<title> nanjiu plugin </title>`)
},
}
}
handleHotUpdate
執(zhí)行自定義 HMR 更新處理。鉤子接收一個帶有以下簽名的上下文對象
interface HmrContext {
file: string
timestamp: number
modules: Array<ModuleNode>
read: () => string | Promise<string>
server: ViteDevServer
}
- modules 是受更改文件影響的模塊數(shù)組。它是一個數(shù)組,因為單個文件可能映射到多個服務(wù)模塊(例如 Vue 單文件組件)。
- read 這是一個異步讀函數(shù),它返回文件的內(nèi)容。之所以這樣做,是因為在某些系統(tǒng)上,文件更改的回調(diào)函數(shù)可能會在編輯器完成文件更新之前過快地觸發(fā),并 fs.readFile 直接會返回空內(nèi)容。傳入的 read 函數(shù)規(guī)范了這種行為。
const hotPlugin = () => {
return {
name: 'nanjiu-plugin',
handleHotUpdate({ server, modules, timestamp}) {
console.log('handleHotUpdate', modules)
},
}
}
當(dāng)我修改App.vue文件時,modules可以獲取到如下信息:
圖片
插件順序
一個 Vite 插件可以額外指定一個 enforce 屬性(類似于 webpack 加載器)來調(diào)整它的應(yīng)用順序。enforce 的值可以是pre 或 post。解析后的插件將按照以下順序排列:
- Alias
- 帶有 enforce: 'pre' 的用戶插件
- Vite 核心插件
- 沒有 enforce 值的用戶插件
- Vite 構(gòu)建用的插件
- 帶有 enforce: 'post' 的用戶插件
- Vite 后置構(gòu)建插件
請注意,這與鉤子的排序是分開的,鉤子的順序仍然會受到它們的 order 屬性的影響,這一點 和 Rollup 鉤子的表現(xiàn)一樣
總結(jié)
vite 在 開發(fā)環(huán)境中,會使用 createPluginContainer 方法創(chuàng)建插件容器,插件容器有兩個核心功能:管理插件生命周期、傳遞插件上下文
圖片