vite的插件基于rollup属提,在本地服務(wù)生成是會(huì)同事創(chuàng)建插件容器目代,按順序針對(duì)插件中的hook做批量調(diào)用塔橡,當(dāng)前支持的hook有(區(qū)分構(gòu)建和開(kāi)發(fā)):
通用hook
|hook名稱(chēng)|觸發(fā)時(shí)機(jī)|
|---|---|
|options
|服務(wù)器啟動(dòng)時(shí)被調(diào)用|
|buildStart
|服務(wù)器啟動(dòng)時(shí)被調(diào)用|
|resolveId
|每個(gè)傳入模塊請(qǐng)求時(shí)被調(diào)用|
|load
|每個(gè)傳入模塊請(qǐng)求時(shí)被調(diào)用|
|transform
|每個(gè)傳入模塊請(qǐng)求時(shí)被調(diào)用|
|buildEnd
|服務(wù)器關(guān)閉時(shí)被調(diào)用|
|closeBundle
|服務(wù)器關(guān)閉時(shí)被調(diào)用|
下一個(gè)問(wèn)題佳镜,vite是如何實(shí)現(xiàn)插件的設(shè)計(jì)的裆馒?答案我們從createPluginContainer里面找
// packages/vite/src/node/server/pluginContainer.ts
export async function createPluginContainer(
{ plugins, logger, root, build: { rollupOptions } }: ResolvedConfig,
watcher?: FSWatcher
): Promise<PluginContainer>
createPluginContainer傳入2個(gè)參數(shù):
- resolveConfig配置:包含了plugins姊氓,logger, root喷好, build(包含rollupOptions)翔横,這里實(shí)際對(duì)應(yīng)的就是創(chuàng)建server的config,只不過(guò)用resolveConfig函數(shù)解析了下梗搅。
- watcher:文件監(jiān)控器禾唁,在創(chuàng)建server的時(shí)候調(diào)用的是chokidar實(shí)例來(lái)實(shí)現(xiàn)
返回的就是PluginContainer。
函數(shù)體其中也包含很多內(nèi)容
function warnIncompatibleMethod(method: string, plugin: string){}
// 針對(duì)每個(gè)異步pipeline都創(chuàng)建一個(gè)上下文來(lái)跟蹤多pipeline下追蹤到當(dāng)前激活的插件
class Context implements PluginContext {}
function formatError(){}
class TransformContext extends Context
const container: PluginContainer
return container
在這里我們看到上面的主要container就是pluginContainer无切,我們具體來(lái)看看里面包含哪些內(nèi)容:
const container: PluginContainer = {
options: await (async ()=>{})(),
async buildStart(){}
async resolveId(rawId, importer = join(root, 'index.html'), skips, ssr){}
async load(id, ssr){}
async transform(code, id, inMap, ssr){}
watchChange(id, event = 'update'){}
async close(){}
}
可以看到插件容器主要就是包含選項(xiàng)的初始化荡短,rollup 各個(gè)特定hook的調(diào)用(buildStart,resolveId,load,transform),變化監(jiān)控同步函數(shù)watchChange,以及最后的close方法哆键。下面我們來(lái)逐個(gè)拆解:
- options異步函數(shù)解析:
- 將每個(gè)插件里的options方法執(zhí)行了一遍
for (const plugin of plugins) {
if (!plugin.options) continue
options =
(await plugin.options.call(minimalContext, options)) || options
}
- 如果有配置acornInjectPlugins:那就合并進(jìn)acore的parser中:
if (options.acornInjectPlugins) {
parser = acorn.Parser.extend(
...[
acornClassFields,
acornStaticClassFeatures,
acornNumericSeparator
].concat(options.acornInjectPlugins)
)
}
- 返回包含acorn及options的對(duì)象
return {
acorn,
acornInjectPlugins: [],
...options
}
- buildStart: 使用promise.all把所有插件里面但凡有該配置的都執(zhí)行下
await Promise.all(
plugins.map((plugin) => {
if (plugin.buildStart) {
return plugin.buildStart.call(
new Context(plugin) as any,
container.options as NormalizedInputOptions
)
}
})
)
- resolveId:resolveId階段屬于rollup的通用hook掘托,流程也類(lèi)似,針對(duì)每個(gè)plugin執(zhí)行對(duì)應(yīng)鉤子:
for (const plugin of plugins) {
// 沒(méi)有就跳過(guò)
if (!plugin.resolveId) continue
// 如果在跳過(guò)的列表里面洼哎,也跳過(guò)
if (skips?.has(plugin)) continue
// 將該插件設(shè)置為當(dāng)前激活插件
ctx._activePlugin = plugin
// 開(kāi)始調(diào)用方法
const result = await plugin.resolveId.call(
ctx as any,
rawId,
importer,
{},
ssr
)
// 如果沒(méi)結(jié)果就跳過(guò)到下一個(gè)
if (!result) continue
// 如果結(jié)果為string烫映,那結(jié)果就是對(duì)應(yīng)的id
if (typeof result === 'string') {
id = result
} else {
// 如果不是那就是object對(duì)象沼本,找到對(duì)應(yīng)id屬性
id = result.id
Object.assign(partial, result)
}
// resolveId() is hookFirst - first non-null result is returned.
break
}
// 如果id有了噩峦,就返回partial锭沟,沒(méi)有返回null
if (id) {
partial.id = isExternalUrl(id) ? id : normalizePath(id)
return partial as PartialResolvedId
} else {
return null
}
- load階段:遍歷plugin執(zhí)行hook, 有結(jié)果就返回,沒(méi)有返回null
for (const plugin of plugins) {
if (!plugin.load) continue
ctx._activePlugin = plugin
const result = await plugin.load.call(ctx as any, id, ssr)
if (result != null) {
return result
}
}
return null
- transform階段:遍歷plugin執(zhí)行hook, 有結(jié)果就返回识补,沒(méi)有返回null
for (const plugin of plugins) {
if (!plugin.transform) continue
ctx._activePlugin = plugin
ctx._activeId = id
ctx._activeCode = code
let result
try {
result = await plugin.transform.call(ctx as any, code, id, ssr)
} catch (e) {
ctx.error(e)
}
if (!result) continue
if (typeof result === 'object') {
code = result.code || ''
if (result.map) ctx.sourcemapChain.push(result.map)
} else {
code = result
}
}
// 返回code及對(duì)應(yīng)的sourcemap
return {
code,
map: ctx._getCombinedSourcemap()
}
- watchChange:遍歷plugin執(zhí)行hook
for (const plugin of plugins) {
if (!plugin.watchChange) continue
ctx._activePlugin = plugin
plugin.watchChange.call(ctx as any, id, { event })
}
- close: 按照先后順序執(zhí)行buildEnd 族淮,closeBundle hook, 配置close狀態(tài)位
if (closed) return
const ctx = new Context()
await Promise.all(
plugins.map((p) => p.buildEnd && p.buildEnd.call(ctx as any))
)
await Promise.all(
plugins.map((p) => p.closeBundle && p.closeBundle.call(ctx as any))
)
closed = true
}
總結(jié)
插件容器針對(duì)插件提供了統(tǒng)一的管理和調(diào)用方案凭涂,在每個(gè)階段調(diào)用并傳遞對(duì)應(yīng)上下文祝辣。