引入
ElementPlus
是我们在日常业务中经常会接触到的组件库,如果要在项目中引入它,我们一般会在打包工具的配置文件中使用ElementPlus
的按需导入插件,对相关API
和组件进行自动按需导入,从而减小打包产物的体积,使用起来就像这样:
ts
// 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()],
}),
],
})
尽管这样可以实现我们想要的效果,但是你在使用的时候可能有过这样的疑问:
AutoImport
和Components
这两个插件是做什么的?它帮我们做了什么事情?- 插件传入的参数
ElementPlusResolver
又是什么?
如果你也存在这些疑问,那这篇文章一定可以帮你弄清ElementPlus
的按需导入的底层原理,扫清你的疑惑。
原理简介
开门见山地说:AutoImport
插件的作用是自动导入项目中用到的API ,而Components
插件的作用是自动导入和注册组件。例如插件会自动把这个:
html
<script setup lang="ts">
ElMessage('AutoImport')
</script>
<template>
<ElButton>Components</ElButton>
</template>
变成:
html
<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>
通过使用这两个插件,我们在项目中可以不用显式引入ElementPlus
的API
和组件,因为在执行npm dev
或npm build
的时候,Vite
会调用插件来自动生成对应的导入语句。由于ElementPlus
提供了基于ES Module
的开箱即用的Tree Shaking
功能,所以打包的时候只会引入需要的组件。
此外,两个插件会分别自动生成auto-imports.d.ts
和components.d.ts
文件,用来声明自动导入的API
和组件的类型,通过在tsconfig.json
的include
中添加两个文件,便可以获得ts
的提示。
到这里我们知道了AutoImport
负责自动导入API
,Components
负责自动导入组件,那么ElementPlusResolver
是做什么的呢?其实答案是显然的,因为这两个组件都不认识ElementPlus
,不可能会知道自动导入哪些API
和组件,因此它们需要一个解析Vue
组件的工具,来为它们提供相关的信息,这个工具就是resolver
(组件库解析器)。
ElementPlusResolver
就属于resolver
的一种,它为unplugin
插件提供ElementPlus
的信息。除此之外还有诸如VantResolver
、ElementUIResolver
等等,如果你自己开发了一个组件库,你甚至也可以为它写一个resolver
,从而支持unplugin
插件。
调试源码
为了更详细地说明以上内容,我们以Components
插件为例,结合源码对原理进行进一步的介绍。
我们首先以一张图来对插件的核心工作原理进行展示:
Components
插件的核心逻辑位于transformComponent
函数中:
ts
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
,分别出现在template
和script
中,这两者重名了,因此自动导入的时候需要通过重新命名进行区分。
至此我们了解了Components
插件按需导入组件的原理:
- 首先获得所有需要自动导入的组件名称,存储到
results
中 - 对于每个组件,调用
findComponent
来获取组件对应的信息;findComponent
会遍历所有的resolver
,直到找到可以解析该组件的resolver
,并调用resolver
的resolve
方法来获取组件的相关信息,如导入路径、其他需要导入的资源等 - 将组件重命名,并将导入语句插入到源码中,最后替换该组件的名称
对于AutoImport
插件,其原理也完全类似。
如果看到这里你还有疑问,欢迎在评论区留言。或者(虽然不推荐),你也可以去自行调试源码。只需要打开插件的相关文件添加断点,并且调试build
命令,就可以对打包的过程进行调试了: