unplugin-vue-components核心代码领读

书接上回。
我们掌握了Vite插件的常用钩子函数及其作用,现在就来看看unplugin-vue-components到底做了什么吧。
细枝末节全部都说到的话篇幅太长,这里只关注核心点。

首先是入口文件unplugin.ts

// 入口函数
export default createUnplugin<Options>((options = {}) => {
  // 注册插件时创建上下文对象,保存配置信息
  const ctx: Context = new Context(options)

  return {
    name: 'unplugin-vue-components',
    enforce: 'post',
    // 注册transform钩子函数,等待Vite调用
    async transform(code, id) {
      // 判断是否为被忽略的文件
      // 带有下面注释则会忽略
      //  '/* unplugin-vue-components disabled */'
      if (!shouldTransform(code))
        return null
      try {
        // 核心操作,转换代码
        const result = await ctx.transform(code, id)
        // 生成声明文件,一般默认为component.d.ts
        ctx.generateDeclaration()
        return result
      }
      catch (e) {
        this.error(e)
      }
    }
  }
})

这里做了三件事情:

  1. 导出默认入口函数
  2. 插件注册时创建上下文对象,保存上下文信息
  3. 注册了transform钩子函数,等待Vite调用

接下来看上下文对象的构造函数,看看这里做了些什么context.ts

constructor(
    private rawOptions: Options,
  ) {
    // 解析配置
    this.options = resolveOptions(rawOptions, this.root)
    // 设置transformer
    this.setTransformer(this.options.transformer)
  }

  setTransformer(name: Options['transformer']) {
    // 默认设置transformer为vue3
    this.transformer = transformer(this, name || 'vue3')
  }

  // 钩子函数被调用时,执行了这个方法
  transform(code: string, id: string) {
    const { path, query } = parseId(id)
    // 调用构造时生成的函数,返回处理结果
    return this.transformer(code, id, path, query)
  }

这里做了三件事:

  1. 解析配置,这里不是核心逻辑,不展开说明
  2. 设置transformer
  3. 提供核心业务函数transform,入口函数的ctx.transform(code, id)就是调用这里

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

// 一个工厂函数,传入上下文及
export default function transformer(ctx: Context, transformer: SupportedTransformer): Transformer {
  return async (code, id, path) => {
    // 查找目标路径下符合条件的所有文件,将其记录下来
    // 目标路径由以下几个配置决定
    // dirs、extensions、globs
    ctx.searchGlob()

    // 解析目标SFC path
    const sfcPath = ctx.normalizePath(path)

    // 生成MagicString对象
    const s = new MagicString(code)

    // 转换组件,非纯函数,改变了MagicString对象值
    await transformComponent(code, transformer, s, ctx, sfcPath)
    // 转换指令
    if (ctx.options.directives)
      await transformDirectives(code, transformer, s, ctx, sfcPath)

    s.prepend(DISABLE_COMMENT)

    // 将被处理后的MagicString值返回,插件结束
    const result: TransformResult = { code: s.toString() }
    return result
  }
}

这里是一个经典的工厂函数,完美利用闭包提供了一切执行时上下文。
看看他生成的函数。也就是最核心的转换逻辑。

  1. 根据配置查找了全部需要插件导入的文件路径,保存到了上下文对象中
  2. 转换组件
  3. 转换指令

接下来我们着重关注转换组件操作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)

  // 拿到需要置换的组件名及闭包函数
  for (const { rawName, replace } of results) {
    const name = pascalCase(rawName)
    ctx.updateUsageMap(sfcPath, [name])
    // 根据之前ctx.searchGlob()方法存储的可供使用的组件路径库,查找符合的组件
    const component = await ctx.findComponent(name, 'component', [sfcPath])
    if (component) {
      // 匹配成功后,置换_resolveComponent("HelloWorldCopy")为`__unplugin_components_${no}`
      // 并在文件最上方导入此组件
      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会将未知组件(没有import的)解析为render函数
  // 对于SFC中引用的组件,会解析为如下模样
  // const _component_HelloWorldCopy = _resolveComponent("HelloWorldCopy")
  for (const match of code.matchAll(/_resolveComponent[0-9]*\("(.+?)"\)/g)) {
    // 所以经过match,这里的matchedName就是目标组件的名字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
}

这里就是一个匹配及转换逻辑

  1. 根据@vitejs/plugin-vue插件产生的render函数特性,找到未被import的组件
  2. 在之前收集到的组件列表内进行匹配
  3. 将匹配到的结果置换为变量,并在文件头部重新导入

效果如下:


image.png

完结

至此,unplugin-vue-components对我们components文件夹下组件的自动导入功能就完全实现了。

本次研究的代码已提交至我的个人git上。
https://github.com/huangXuuu/initial/tree/f/%23000003unplugin-vue-components-learn/release

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容