likes
comments
collection
share

你真的了解 ElementPlus 的按需导入吗?

作者站长头像
站长
· 阅读数 8

引入

ElementPlus是我们在日常业务中经常会接触到的组件库,如果要在项目中引入它,我们一般会在打包工具的配置文件中使用ElementPlus的按需导入插件,对相关API和组件进行自动按需导入,从而减小打包产物的体积,使用起来就像这样:

// vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
	 // 疑问:
	 // AutoImport 和 Components 这两个插件是做什么的?
    AutoImport({
      // ElementPlusResolver 又是什么?
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})

尽管这样可以实现我们想要的效果,但是你在使用的时候可能有过这样的疑问:

  • AutoImportComponents这两个插件是做什么的?它帮我们做了什么事情?
  • 插件传入的参数ElementPlusResolver又是什么?

如果你也存在这些疑问,那这篇文章一定可以帮你弄清ElementPlus的按需导入的底层原理,扫清你的疑惑。

原理简介

开门见山地说:AutoImport插件的作用是自动导入项目中用到的API,而Components插件的作用是自动导入和注册组件。例如插件会自动把这个:

<script setup lang="ts">
ElMessage('AutoImport')
</script>

<template>
	<ElButton>Components</ElButton>
</template>

变成:

<script setup lang="ts">
// AutoImport 生成:导入 ElMessage API 和样式文件
import { ElMessage } from 'element-plus/es';
import 'element-plus/es/components/message/style/css'
// Components 生成:导入 ElButton 组件和样式文件
import { ElButton } from 'element-plus/es';
import 'element-plus/es/components/button/style/css'
  
ElMessage('AutoImport')
</script>

<template>
	<ElButton>Components</ElButton>
</template>

通过使用这两个插件,我们在项目中可以不用显式引入ElementPlusAPI和组件,因为在执行npm devnpm build的时候,Vite会调用插件来自动生成对应的导入语句。由于ElementPlus提供了基于ES Module的开箱即用的Tree Shaking功能,所以打包的时候只会引入需要的组件。

此外,两个插件会分别自动生成auto-imports.d.tscomponents.d.ts文件,用来声明自动导入的API和组件的类型,通过在tsconfig.jsoninclude中添加两个文件,便可以获得ts的提示。

到这里我们知道了AutoImport负责自动导入APIComponents负责自动导入组件,那么ElementPlusResolver是做什么的呢?其实答案是显然的,因为这两个组件都不认识ElementPlus,不可能会知道自动导入哪些API和组件,因此它们需要一个解析Vue组件的工具,来为它们提供相关的信息,这个工具就是resolver(组件库解析器)。

ElementPlusResolver就属于resolver的一种,它为unplugin插件提供ElementPlus的信息。除此之外还有诸如VantResolverElementUIResolver等等,如果你自己开发了一个组件库,你甚至也可以为它写一个resolver,从而支持unplugin插件。

调试源码

为了更详细地说明以上内容,我们以Components插件为例,结合源码对原理进行进一步的介绍。

我们首先以一张图来对插件的核心工作原理进行展示:

你真的了解 ElementPlus 的按需导入吗?

Components插件的核心逻辑位于transformComponent函数中:

async function transformComponent(code, transformer2, s, ctx, sfcPath) {
  let no = 0;
  const results = transformer2 === "vue2" ? resolveVue2(code, s) : resolveVue3(code, s);
  // results:
  // {
  //   rawName: "ElButton",
  //   replace: (resolved) => s.overwrite(start, end, resolved),
  // }
  for (const { rawName, replace } of results) {
    debug2(`| ${rawName}`);
    const name = pascalCase(rawName); // 'ElButton'
    ctx.updateUsageMap(sfcPath, [name]);
    const component = await ctx.findComponent(name, "component", [sfcPath]);
    // component:
    // {
    //   as: "ElButton",
    //   name: "ElButton",
    //   from: "element-plus/es",
    //   sideEffects: [
    //     "element-plus/es/components/base/style/css",
    //     "element-plus/es/components/button/style/css",
    //   ],
    // }
    if (component) {
      const varName = `__unplugin_components_${no}`; // "__unplugin_components_0"
      
      // 将 import 语句插入到源码中
      // "import { ElButton as __unplugin_components_0 } from 'element-plus/es';
      // import 'element-plus/es/components/base/style/css';
      // import 'element-plus/es/components/button/style/css';\n"
      s.prepend(`${stringifyComponentImport(__spreadProps(__spreadValues({}, component), { as: varName }), ctx)};
`);
      no += 1;
      replace(varName); // 将 ElButton 替换为 __unplugin_components_0
    }
  }
  debug2(`^ (${no})`);
}

该函数首先通过调用ctx.findComponent获得ElButton组件的相关信息,例如从哪里导入(from),其他需要导入的相关文件(sideEffects)。

接下来将ElButton重新命名为__unplugin_components_0并导入相关资源,最后将文件中用到的ElButton替换为__unplugin_components_0

这里我猜测Components插件对导入的模块重新命名的原因是为了防止命名冲突。例如组件中使用了一个组件叫Foo,同时还使用了一个API也叫Foo,分别出现在templatescript中,这两者重名了,因此自动导入的时候需要通过重新命名进行区分。

至此我们了解了Components插件按需导入组件的原理

  1. 首先获得所有需要自动导入的组件名称,存储到results
  2. 对于每个组件,调用findComponent来获取组件对应的信息;findComponent会遍历所有的resolver,直到找到可以解析该组件的resolver,并调用resolverresolve方法来获取组件的相关信息,如导入路径、其他需要导入的资源等
  3. 将组件重命名,并将导入语句插入到源码中,最后替换该组件的名称

对于AutoImport插件,其原理也完全类似。

如果看到这里你还有疑问,欢迎在评论区留言。或者(虽然不推荐),你也可以去自行调试源码。只需要打开插件的相关文件添加断点,并且调试build命令,就可以对打包的过程进行调试了:

你真的了解 ElementPlus 的按需导入吗?