unplugin-vue-components[1]是一个按需自动引入组件的工具插件。因为有 unplugin[2]的加持,他可以在 vite, rollup, 和 webpack 等多种构建工具中引入使用。

基本使用

  1. // vite.config.ts
  2. import Components from 'unplugin-vue-components/vite'
  3. import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
  4. export default defineConfig({
  5. plugins: [
  6. Components({
  7. /* options */
  8. resolvers: [ ElementPlusResolver() ]
  9. }),
  10. ],
  11. })

element-plus resolver 可以根据具体的命名规则来进行定位,然后完成自动的引入。除此之外,还包括其他的一些常用 UI 库的 resolver,例如 antd, vant, arco 等。

作用流程

  1. 在 vite 对应的 configResolvedbuild hooks 中查询是否有 vite-plugin-vue2 若有则设置 vue 版本为2(默认3)
  2. 调用 vite 对应的 transform(code, id) build hooks 来处理代码,code 是转换后的代码,id 为对应的路径
  3. 使用 fast-glob [3]来获取全局的符合项目后缀的文件路径,即位于 src/components路径下的所有 vue 文件,然后再返回 { fileName, filePath }
  4. 将对应的文件转换成 MagicString[4]便于之后的操作
  5. 使用 transformComponent 和 transformDirective 对文件中存在的组件和指令名进行修改,并在当前组件的头部添加当前组件内含的第三方组件名和路径

这就是 vue-components 组件自动按需引入的基本流程,结合 vite 的 hmr 功能,当用户对文件进行编辑时,会触发细粒度的文件扫描,并生成新的文件引入代码。这样就实现了按需引入的功能。对应的 UI 库或自定义的 resolver 主要是作用于阶段3,根据 resolver 具体的配置来找到目标文件及其路径。

详情分析

unplugin hooks

unplugin 内层对API进行了封装,我们只需要利用它对外暴露的 API 来进行使用,主要使用到的就是 transform 这个 build hooks,在这里对转换的代码进行二次操作,其中还包括 vite 和 webpack 特有的 hooks,我们可以进行额外的配置。具体内容请看框架相关 hooks.

  1. // unplugin-vue-components/src/core/unplugin.ts
  2. // ...
  3. return {
  4. name: 'unplugin-vue-components',
  5. enforce: 'post',
  6. transformInclude(id) {
  7. return filter(id)
  8. },
  9. async transform(code, id) {
  10. if (!shouldTransform(code))
  11. return null
  12. try {
  13. // 利用 code 和 id 对转换的文件进行修改
  14. // code 是转换的代码,id 是文件的绝对路径
  15. const result = await ctx.transform(code, id)
  16. // 针对 TS 文件生成声明文件
  17. ctx.generateDeclaration()
  18. return result
  19. }
  20. catch (e) {
  21. this.error(e)
  22. }
  23. },
  24. vite: {
  25. configResolved(config: ResolvedConfig) {
  26. // 设置文件默认的根目录
  27. ctx.setRoot(config.root)
  28. ctx.sourcemap = true
  29. if (config.plugins.find(i => i.name === 'vite-plugin-vue2'))
  30. // 对 plugin 进行查询,若有 vue2 相关的则设置为 vue2 默认为 vue3
  31. ctx.setTransformer('vue2')
  32. if (options.dts) {
  33. // 若深度参数为 true 则直接进行全局搜寻并生成TS声明文件
  34. ctx.searchGlob()
  35. ctx.generateDeclaration()
  36. }
  37. if (config.build.watch && config.command === 'build')
  38. ctx.setupWatcher(chokidar.watch(ctx.options.globs))
  39. },
  40. configureServer(server: ViteDevServer) {
  41. ctx.setupViteServer(server)
  42. },
  43. },
  44. }
  45. // ...

转换器

  1. // src/core/context.ts
  2. // ...
  3. transform(code: string, id: string) {
  4. // 格式化文件路径,并使用 transformer 进行转换
  5. const { path, query } = parseId(id)
  6. return this.transformer(code, id, path, query)
  7. }
  8. // ...
  1. // src/core/transformer.ts
  2. export default function transformer(ctx: Context, transformer: SupportedTransformer): Transformer {
  3. return async(code, id, path) => {
  4. // 使用 fast-glob 进行全局搜寻,获取所有 vue 文件的路径
  5. ctx.searchGlob()
  6. const sfcPath = ctx.normalizePath(path)
  7. // 将文件转换成 MagicString 便于之后的操作
  8. const s = new MagicString(code)
  9. await transformComponent(code, transformer, s, ctx, sfcPath)
  10. if (ctx.options.directives)
  11. await transformDirectives(code, transformer, s, ctx, sfcPath)
  12. s.prepend(DISABLE_COMMENT)
  13. const result: TransformResult = { code: s.toString() }
  14. if (ctx.sourcemap)
  15. result.map = s.generateMap({ source: id, includeContent: true })
  16. return result
  17. }
  18. }

全局搜索文件路径

  1. // src/core/fs/glob.ts
  2. export function searchComponents(ctx: Context) {
  3. // 从 context 中获取根路径
  4. const root = ctx.root
  5. // 使用fast-glob进行全局搜索,globs 参数见25行代码
  6. const files = fg.sync(ctx.options.globs, {
  7. ignore: ['node_modules'],
  8. onlyFiles: true,
  9. cwd: root,
  10. absolute: true,
  11. })
  12. if (!files.length && !ctx.options.resolvers?.length)
  13. // eslint-disable-next-line no-console
  14. console.warn('[unplugin-vue-components] no components found')
  15. debug(`${files.length} components found.`)
  16. // 将所有的文件添加到 context 中的 )_componentsPaths 和 _componentNameMap 中
  17. ctx.addComponents(files)
  18. }
  19. // src/core/options.ts
  20. // 根据是否为 deep 选择 **/*.ext 或 *.ext
  21. resolved.globs = resolved.resolvedDirs.map(i => resolved.deep
  22. ? slash(join(i, `**/*.${extsGlob}`))
  23. : slash(join(i, `*.${extsGlob}`)),
  24. )

image.png
如图示,使用 src/components/**/*.{vue,md,svg}作为参数,在全局找到了13个文件。

组件、指令转换

  1. // src/core/transforms/component.ts
  2. export default async function transformComponent(code: string, transformer: SupportedTransformer, s: MagicString, ctx: Context, sfcPath: string) {
  3. let no = 0
  4. const results = transformer === 'vue2' ? resolveVue2(code, s) : resolveVue3(code, s)
  5. for (const { rawName, replace } of results) {
  6. // 名称转换
  7. const name = pascalCase(rawName)
  8. ctx.updateUsageMap(sfcPath, [name])
  9. // 找到对应的组件路径和组件名
  10. const component = await ctx.findComponent(name, 'component', [sfcPath])
  11. if (component) {
  12. // 使用 MagicString 对名字进行修改和添加
  13. const varName = `__unplugin_components_${no}`
  14. s.prepend(`${stringifyComponentImport({ ...component, name: varName }, ctx)};\n`)
  15. no += 1
  16. // 将引入文件名和路径添加到文件头部
  17. replace(varName)
  18. }
  19. }
  20. }
  21. // src/core/context.tx
  22. //...
  23. async findComponent(name: string, type: 'component' | 'directive', excludePaths: string[] = []): Promise<ComponentInfo | undefined> {
  24. // resolve from fs
  25. let info = this._componentNameMap[name]
  26. if (info && !excludePaths.includes(info.path) && !excludePaths.includes(info.path.slice(1)))
  27. return info
  28. // custom resolvers
  29. // 调用设置的 resolvers,自定义的 resolvers 或 elementPlusResolver
  30. for (const resolver of this.options.resolvers) {
  31. if (resolver.type !== type)
  32. continue
  33. const result = await resolver.resolve(name)
  34. if (result) {
  35. if (typeof result === 'string') {
  36. info = {
  37. name,
  38. path: result,
  39. }
  40. // 添加自定义或者
  41. this.addCustomComponents(info)
  42. return info
  43. }
  44. else {
  45. info = {
  46. name,
  47. ...result,
  48. }
  49. this.addCustomComponents(info)
  50. return info
  51. }
  52. }
  53. }
  54. return undefined
  55. }
  56. //...

image.png
如图示,右侧文件就是添加完组件引入片段的代码。

引用

  1. unplugin-vue-components
  2. unplugin
  3. fast-glob
  4. MagicString