unplugin-vue-components核心代碼領(lǐng)讀

書接上回。
我們掌握了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)
      }
    }
  }
})

這里做了三件事情:

  1. 導(dǎo)出默認(rèn)入口函數(shù)
  2. 插件注冊(cè)時(shí)創(chuàng)建上下文對(duì)象葵孤,保存上下文信息
  3. 注冊(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)
  }

這里做了三件事:

  1. 解析配置病线,這里不是核心邏輯,不展開說(shuō)明
  2. 設(shè)置transformer
  3. 提供核心業(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)換邏輯。

  1. 根據(jù)配置查找了全部需要插件導(dǎo)入的文件路徑丧鸯,保存到了上下文對(duì)象中
  2. 轉(zhuǎn)換組件
  3. 轉(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)換邏輯

  1. 根據(jù)@vitejs/plugin-vue插件產(chǎn)生的render函數(shù)特性剿干,找到未被import的組件
  2. 在之前收集到的組件列表內(nèi)進(jìn)行匹配
  3. 將匹配到的結(jié)果置換為變量,并在文件頭部重新導(dǎo)入

效果如下:


image.png

完結(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末氢伟,一起剝皮案震驚了整個(gè)濱河市榜轿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌腐芍,老刑警劉巖差导,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異猪勇,居然都是意外死亡设褐,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門泣刹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)助析,“玉大人,你說(shuō)我怎么就攤上這事椅您⊥饧剑” “怎么了?”我有些...
    開封第一講書人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵掀泳,是天一觀的道長(zhǎng)雪隧。 經(jīng)常有香客問(wèn)我,道長(zhǎng)员舵,這世上最難降的妖魔是什么脑沿? 我笑而不...
    開封第一講書人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮马僻,結(jié)果婚禮上庄拇,老公的妹妹穿的比我還像新娘。我一直安慰自己韭邓,他們只是感情好措近,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著女淑,像睡著了一般瞭郑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鸭你,一...
    開封第一講書人閱讀 49,821評(píng)論 1 290
  • 那天凰浮,我揣著相機(jī)與錄音我抠,去河邊找鬼。 笑死袜茧,一個(gè)胖子當(dāng)著我的面吹牛菜拓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播笛厦,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼纳鼎,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了裳凸?” 一聲冷哼從身側(cè)響起贱鄙,我...
    開封第一講書人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎姨谷,沒想到半個(gè)月后逗宁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡梦湘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年瞎颗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片捌议。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡哼拔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瓣颅,到底是詐尸還是另有隱情倦逐,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布宫补,位于F島的核電站檬姥,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏粉怕。R本人自食惡果不足惜健民,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望斋荞。 院中可真熱鬧荞雏,春花似錦虐秦、人聲如沸平酿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蜈彼。三九已至,卻和暖如春俺驶,著一層夾襖步出監(jiān)牢的瞬間幸逆,已是汗流浹背棍辕。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留还绘,地道東北人楚昭。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像拍顷,于是被迫代替她去往敵國(guó)和親抚太。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容