書接上回。
我們掌握了Vite插件的常用鉤子函數(shù)及其作用,現(xiàn)在就來(lái)看看unplugin-vue-components到底做了什么吧。
細(xì)枝末節(jié)全部都說(shuō)到的話篇幅太長(zhǎng)余蟹,這里只關(guān)注核心點(diǎn)。
首先是入口文件unplugin.ts
// 入口函數(shù)
export default createUnplugin<Options>((options = {}) => {
// 注冊(cè)插件時(shí)創(chuàng)建上下文對(duì)象子刮,保存配置信息
const ctx: Context = new Context(options)
return {
name: 'unplugin-vue-components',
enforce: 'post',
// 注冊(cè)transform鉤子函數(shù)威酒,等待Vite調(diào)用
async transform(code, id) {
// 判斷是否為被忽略的文件
// 帶有下面注釋則會(huì)忽略
// '/* unplugin-vue-components disabled */'
if (!shouldTransform(code))
return null
try {
// 核心操作,轉(zhuǎn)換代碼
const result = await ctx.transform(code, id)
// 生成聲明文件挺峡,一般默認(rèn)為component.d.ts
ctx.generateDeclaration()
return result
}
catch (e) {
this.error(e)
}
}
}
})
這里做了三件事情:
- 導(dǎo)出默認(rèn)入口函數(shù)
- 插件注冊(cè)時(shí)創(chuàng)建上下文對(duì)象葵孤,保存上下文信息
- 注冊(cè)了transform鉤子函數(shù),等待Vite調(diào)用
接下來(lái)看上下文對(duì)象的構(gòu)造函數(shù)橱赠,看看這里做了些什么context.ts
constructor(
private rawOptions: Options,
) {
// 解析配置
this.options = resolveOptions(rawOptions, this.root)
// 設(shè)置transformer
this.setTransformer(this.options.transformer)
}
setTransformer(name: Options['transformer']) {
// 默認(rèn)設(shè)置transformer為vue3
this.transformer = transformer(this, name || 'vue3')
}
// 鉤子函數(shù)被調(diào)用時(shí)尤仍,執(zhí)行了這個(gè)方法
transform(code: string, id: string) {
const { path, query } = parseId(id)
// 調(diào)用構(gòu)造時(shí)生成的函數(shù),返回處理結(jié)果
return this.transformer(code, id, path, query)
}
這里做了三件事:
- 解析配置病线,這里不是核心邏輯,不展開說(shuō)明
- 設(shè)置transformer
- 提供核心業(yè)務(wù)函數(shù)transform鲤嫡,入口函數(shù)的
ctx.transform(code, id)
就是調(diào)用這里
接下來(lái)就是看transformer(this, name || 'vue3')
到底干了啥transformer.ts
// 一個(gè)工廠函數(shù)送挑,傳入上下文及
export default function transformer(ctx: Context, transformer: SupportedTransformer): Transformer {
return async (code, id, path) => {
// 查找目標(biāo)路徑下符合條件的所有文件,將其記錄下來(lái)
// 目標(biāo)路徑由以下幾個(gè)配置決定
// dirs暖眼、extensions惕耕、globs
ctx.searchGlob()
// 解析目標(biāo)SFC path
const sfcPath = ctx.normalizePath(path)
// 生成MagicString對(duì)象
const s = new MagicString(code)
// 轉(zhuǎn)換組件,非純函數(shù)诫肠,改變了MagicString對(duì)象值
await transformComponent(code, transformer, s, ctx, sfcPath)
// 轉(zhuǎn)換指令
if (ctx.options.directives)
await transformDirectives(code, transformer, s, ctx, sfcPath)
s.prepend(DISABLE_COMMENT)
// 將被處理后的MagicString值返回司澎,插件結(jié)束
const result: TransformResult = { code: s.toString() }
return result
}
}
這里是一個(gè)經(jīng)典的工廠函數(shù),完美利用閉包提供了一切執(zhí)行時(shí)上下文栋豫。
看看他生成的函數(shù)挤安。也就是最核心的轉(zhuǎn)換邏輯。
- 根據(jù)配置查找了全部需要插件導(dǎo)入的文件路徑丧鸯,保存到了上下文對(duì)象中
- 轉(zhuǎn)換組件
- 轉(zhuǎn)換指令
接下來(lái)我們著重關(guān)注轉(zhuǎn)換組件操作transformComponent(code, transformer, s, ctx, sfcPath)
export default async function transformComponent(code: string, transformer: any, s: MagicString, ctx: Context, sfcPath: string) {
let no = 0
const results = transformer === 'vue2' ? resolveVue2(code, s) : resolveVue3(code, s)
// 拿到需要置換的組件名及閉包函數(shù)
for (const { rawName, replace } of results) {
const name = pascalCase(rawName)
ctx.updateUsageMap(sfcPath, [name])
// 根據(jù)之前ctx.searchGlob()方法存儲(chǔ)的可供使用的組件路徑庫(kù)蛤铜,查找符合的組件
const component = await ctx.findComponent(name, 'component', [sfcPath])
if (component) {
// 匹配成功后,置換_resolveComponent("HelloWorldCopy")為`__unplugin_components_${no}`
// 并在文件最上方導(dǎo)入此組件
const varName = `__unplugin_components_${no}`
s.prepend(`${stringifyComponentImport({ ...component, as: varName }, ctx)};\n`)
no += 1
replace(varName)
}
}
}
function resolveVue3(code: string, s: MagicString) {
const results: ResolveResult[] = []
/**
* when using some plugin like plugin-vue-jsx, resolveComponent will be imported as resolveComponent1 to avoid duplicate import
*/
// Vue3的官方解析插件@vitejs/plugin-vue會(huì)將未知組件(沒有import的)解析為render函數(shù)
// 對(duì)于SFC中引用的組件,會(huì)解析為如下模樣
// const _component_HelloWorldCopy = _resolveComponent("HelloWorldCopy")
for (const match of code.matchAll(/_resolveComponent[0-9]*\("(.+?)"\)/g)) {
// 所以經(jīng)過(guò)match围肥,這里的matchedName就是目標(biāo)組件的名字HelloWorldCopy
const matchedName = match[1]
if (match.index != null && matchedName && !matchedName.startsWith('_')) {
// 記錄需要置換的位置
const start = match.index
const end = start + match[0].length
results.push({
rawName: matchedName,
replace: resolved => s.overwrite(start, end, resolved),
})
}
}
return results
}
這里就是一個(gè)匹配及轉(zhuǎn)換邏輯
- 根據(jù)
@vitejs/plugin-vue
插件產(chǎn)生的render函數(shù)特性剿干,找到未被import的組件 - 在之前收集到的組件列表內(nèi)進(jìn)行匹配
- 將匹配到的結(jié)果置換為變量,并在文件頭部重新導(dǎo)入
效果如下:
完結(jié)
至此穆刻,unplugin-vue-components對(duì)我們components
文件夾下組件的自動(dòng)導(dǎo)入功能就完全實(shí)現(xiàn)了置尔。
本次研究的代碼已提交至我的個(gè)人git上。
https://github.com/huangXuuu/initial/tree/f/%23000003unplugin-vue-components-learn/release