Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unplugin-vue-components 原理浅析 #32

Open
chiyan-lin opened this issue Dec 11, 2022 · 0 comments
Open

unplugin-vue-components 原理浅析 #32

chiyan-lin opened this issue Dec 11, 2022 · 0 comments

Comments

@chiyan-lin
Copy link
Owner

在做组件库的时候,发现了一个自动引入组件的插件,简单研究了下

以 vite + vue3 的使用为例子

使用

<script setup lang='ts'>
import { ref } from 'vue'
</script>

<template>
  <div class="block">
    <ComponentA msg="a" />
  </div>
</template>
// vite.config.ts
import Components from 'unplugin-vue-components/vite'

export default defineConfig({
  plugins: [
    Components({ /* options */ }),
  ],
})

原理

  1. 定义 trasform 钩子,劫持源码

路径 src/core/unplugin.ts

    async transform(code, id) {
      if (!shouldTransform(code))
        return null
      try {
        const result = await ctx.transform(code, id)
        ctx.generateDeclaration()
        return result
      }
      catch (e) {
        this.error(e)
      }
    }

上面的 code 是每个单文件的 vue3 编译好的代码

import { defineComponent as _defineComponent } from "vue";
const _sfc_main = /* @__PURE__ */ _defineComponent({
  __name: "App",
  setup(__props, { expose }) {
    expose();
    const __returned__ = {};
    Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
    return __returned__;
  }
});
import { resolveComponent as _resolveComponent, createVNode as _createVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue";
const _hoisted_1 = { class: "block" };
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_ComponentA = _resolveComponent("ComponentA");
  return _openBlock(), _createElementBlock("div", _hoisted_1, [
    _createVNode(_component_ComponentA, { msg: "a" })
  ]);
}
  1. 匹配 _resolveComponent 找到引入的组件名
    路径:src/core/transformer.ts
export default function transformer(ctx: Context, transformer: SupportedTransformer): Transformer {
  return async (code, id, path) => {
    ctx.searchGlob()

    const sfcPath = ctx.normalizePath(path)
    debug(sfcPath)

    const s = new MagicString(code)

    await transformComponent(code, transformer, s, ctx, sfcPath)
    s.prepend(DISABLE_COMMENT)

    const result: TransformResult = { code: s.toString() }
    if (ctx.sourcemap)
      result.map = s.generateMap({ source: id, includeContent: true })
    return result
  }
}

其中 transformComponent 的作用就是通过正则 取出 _resolveComponent 引入的组件名

export default async function transformComponent(code: string, transformer: SupportedTransformer, s: MagicString, ctx: Context, sfcPath: string) {
  let no = 0
  const results: ResolveResult[] = []
  for (const match of code.matchAll(/_resolveComponent[0-9]*\("(.+?)"\)/g)) {
    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),
      })
    }
  }
  for (const { rawName, replace } of results) {
    debug(`| ${rawName}`)
    const name = pascalCase(rawName)
    ctx.updateUsageMap(sfcPath, [name])
    const component = await ctx.findComponent(name, 'component', [sfcPath])
    if (component) {
      const varName = `__unplugin_components_${no}`
      s.prepend(`${stringifyComponentImport({ ...component, as: varName }, ctx)};\n`)
      no += 1
      replace(varName)
    }
  }
}

stringifyComponentImport 方法的作用就是构造出 ComponentA import 代码

import { ${info.name} as ${info.as} } from '${info.from}'
// => import ComponentA as ComponentA from 'ComponentA path'
  1. transform 最后返回的文件
import __unplugin_components_0 from 'xxxx/vite-vue3/src/components/ComponentA.vue';
import { defineComponent as _defineComponent } from "vue";
const _sfc_main = /* @__PURE__ */ _defineComponent({
  __name: "App",
  setup(__props, { expose }) {
    expose();
    const __returned__ = {};
    Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
    return __returned__;
  }
});
import { resolveComponent as _resolveComponent, createVNode as _createVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue";
const _hoisted_1 = { class: "block" };
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_ComponentA = __unplugin_components_0;
  return _openBlock(), _createElementBlock("div", _hoisted_1, [
    _createVNode(_component_ComponentA, { msg: "a" })
  ]);
}
  1. 在一开始的配置的时候,可以配置引入组件的组件库或者自定义组件的文件夹,插件会动态监听,只有在 find 到引入的组件在对应的组件库或者组件文件中才会执行上面的操作

总结

插件的思路其实跟我们手动 import 的操作是一样的,帮我们做了这样的引入操作

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant