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

書接上回。
我們掌握了Vite插件的常用鉤子函數(shù)及其作用,現(xiàn)在就來看看unplugin-vue-components到底做了什么吧。
細(xì)枝末節(jié)全部都說到的話篇幅太長,這里只關(guān)注核心點(diǎn)。

首先是入口文件unplugin.ts

// 入口函數(shù)
export default createUnplugin<Options>((options = {}) => {
  // 注冊插件時(shí)創(chuàng)建上下文對(duì)象,保存配置信息
  const ctx: Context = new Context(options)

  return {
    name: 'unplugin-vue-components',
    enforce: 'post',
    // 注冊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. 插件注冊時(shí)創(chuàng)建上下文對(duì)象,保存上下文信息
  3. 注冊了transform鉤子函數(shù),等待Vite調(diào)用

接下來看上下文對(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. 解析配置,這里不是核心邏輯,不展開說明
  2. 設(shè)置transformer
  3. 提供核心業(yè)務(wù)函數(shù)transform,入口函數(shù)的ctx.transform(code, id)就是調(diào)用這里

接下來就是看transformer(this, name || 'vue3')到底干了啥transformer.ts

// 一個(gè)工廠函數(shù),傳入上下文及
export default function transformer(ctx: Context, transformer: SupportedTransformer): Transformer {
  return async (code, id, path) => {
    // 查找目標(biāo)路徑下符合條件的所有文件,將其記錄下來
    // 目標(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)換指令

接下來我們著重關(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ǔ)的可供使用的組件路徑庫,查找符合的組件
    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)過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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容