目录
下面的内容我们都将参考 Element Plus UI 和 Arco Design 官方内容来进行阐述底层实现。
一、组件库引入方式
1.1 Element Plus UI 示例
以 Vue3 为例,参考 Element Plus UI 快速开始
- 安装
# npm
npm install element-plus --save
# yarn
yarn add element-plus
下载完毕,可以看到如下目录:
在 Element Plus UI 的 npm 包中,这些文件和目录通常承担以下作用:
-
dist: 这个目录通常包含已经打包好的、用于生产环境的代码。里面包含了压缩过的 JavaScript 文件、样式表文件、国际化文件以及其他相关文件。
-
es: 这个目录通常包含了 ES 模块(ECMAScript 模块)的源代码,通常是未经过编译的、用于现代构建工具的代码。
-
lib: 这个目录通常包含了编译后的、用于旧版构建工具或浏览器的代码,例如 CommonJS 或者 UMD(Universal Module Definition)模块。
-
them-chalk: 这个目录包含了一个主题(通常是命名为 “chalk”)的样式文件,该文件提供了 Element Plus UI 的默认主题。
-
global.d.ts: GlobalComponents for Volar,用于提供类型检查和自动补全。Volar 是一个专为 Vue.js 生态系统设计的 TypeScript 语言服务器,它提供了强大的智能代码补全、类型检查、重构等功能,以提升 Vue.js 项目的开发效率和代码质量。
-
package.json: 这个文件是 npm 包的描述文件,包含了该包的元数据(如名称、版本、依赖等)以及一些配置信息。
-
README.md: 这个文件通常是包的说明文档,包含了使用说明、API 文档、贡献指南等信息。
这些文件和目录组成了 Element Plus UI 的 npm 包,开发者可以根据需要选择合适的文件来使用。
- 完整引入
// main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
- 按需引入
如果使用模板方式进行开发,一般需要使用额外的插件来开启按需加载及自动导入的支持。
首先你需要安装 unplugin-vue-components
和 unplugin-auto-import
这两款插件。插件会自动解析模板中的使用到的组件,并导入组件和对应的样式文件。
npm install -D unplugin-vue-components unplugin-auto-import
// 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({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
1.2 完整引入底层实现
1. Vue 插件:app.use & install
首先,对于完整引入。
当你调用 app.use(ElementPlus)
时,你实际上是在告诉 Vue 应用使用 Element Plus 插件。这里的 ElementPlus
实际上是一个 Vue 插件,它提供了一系列的 Vue 组件供你使用。
在 Vue 中,插件是一种能够为 Vue 添加全局功能的方式。当你调用 app.use(ElementPlus)
时,Vue 会执行 Element Plus 插件提供的 install
方法。在 Element Plus 中,这个 install
方法会注册所有的 Element Plus 组件,使其变为全局可用。这里其实等价于 将所有组件通过 Vue 全局注册组件的方式引入。
具体地说,app.use(ElementPlus)
做了以下几件事情:
- 调用 Element Plus 中的
install
方法。 - 在
install
方法中,Element Plus 注册了它的所有组件到 Vue 实例中。 - 一旦组件被注册到 Vue 实例中,它们就可以在整个应用中使用了。
所以,当你调用 app.use(ElementPlus)
后,你就可以在你的 Vue 应用中使用 Element Plus 提供的所有组件了,因为这些组件已经被注册到了 Vue 实例中。
这里我们将通过本地运行一个 vite 项目并引入 Element Plus UI 的方式来深入源码进行学习。
- vite 创建项目
npm create vite@latest
- 引入 Element Plus UI
npm install element-plus --save
npm fund 是 Node Package Manager(npm)生态系统中的一个命令,允许包维护者指定其包的资助信息。这些信息可以包括到平台(如 Open Collective、GitHub Sponsors 或 Patreon)的链接,用户可以通过这些平台对他们依赖的包的开发和维护进行财务支持。
- 完整引入
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
通过编辑器智能提示,我们可以定位到 Element Plus UI 的入口文件:
可以在入口文件处发现其提供的 install
方法:
由于我们的 vite 项目启动了 Typescript,我们可以直接阅读接口设计,即类型定义文件:
其中关于 install
方法定义如下:
//node_modules/element-plus/es/index.d.ts
export declare const install: (app: import("vue").App<any>, options?: Partial<import("element-plus/es/components").ConfigProviderProps> | undefined) => void;
这段代码导出一个名为 install
的常量。
-
export declare
: 这表示将要导出一个声明。在 TypeScript 中,export
关键字用于导出模块的成员(如变量、函数、类等),而declare
则用于告诉编译器,这是一个声明而不是实现,可以在其他地方定义,declare 的主要用途是在类型声明文件(.d.ts 文件)中为已有的 JavaScript 代码提供类型信息,以便在 TypeScript 项目中使用。 -
const install: (app: import("vue").App<any>, options?: Partial<import("element-plus/es/components").ConfigProviderProps> | undefined) => void
: 这是install
函数的签名或类型定义。它接受两个参数:app
和options
。app
参数是一个 Vue 应用程序实例,options
参数是一个可选的配置对象,其类型是Partial<import("element-plus/es/components").ConfigProviderProps> | undefined
。该函数没有返回值 (void
)。
更多关于 Typescript 的内容可以阅读 HOW - Typescript 类型声明文件
接下来我们看看在 install 里发生了什么?
// node_modules/element-plus/es/make-installer.mjs
const install = (app, options) => {
if (app[INSTALLED_KEY])
return;
app[INSTALLED_KEY] = true;
components.forEach((c) => app.use(c));
if (options)
provideGlobalConfig(options, app, true);
};
//node_modules/element-plus/es/defaults.mjs
import { makeInstaller } from './make-installer.mjs';
import Components from './component.mjs';
import Plugins from './plugin.mjs';
var installer = makeInstaller([...Components, ...Plugins]);
export { installer as default };
关键在这一行 components.forEach((c) => app.use(c));
。这行代码遍历一个名为 components 的数组,对其中的每个组件执行 app.use(c)
,即将每个组件注册到 Vue 应用程序实例中。
我们看看 component.mjs
里提供了什么组件,然后找一个组件来分析。
//node_modules/element-plus/es/component.mjs
import { ElAlert } from './components/alert/index.mjs';
import { ElBadge } from './components/badge/index.mjs';
import { ElButton, ElButtonGroup } from './components/button/index.mjs';
//...
var Components = [
ElAlert,
ElBadge,
ElButton,
ElButtonGroup
]
export { Components as default };
以 ElBadge
为例:该模块目录组成如下
- src:包括了组件具体实现
- style:包括了组件样式
- index.d.ts:类型定义
- index.mjs:组件统一入口封装
我们先看入口文件定义:
//node_modules/element-plus/es/components/badge/index.mjs
import '../../utils/index.mjs';
import Badge from './src/badge2.mjs';
export { badgeProps } from './src/badge.mjs';
import { withInstall } from '../../utils/vue/install.mjs';
const ElBadge = withInstall(Badge);
export { ElBadge, ElBadge as default };
这里面关键在于 withInstall
:
//node_modules/element-plus/es/utils/vue/install.mjs
const withInstall = (main, extra) => {
;
main.install = (app) => {
for (const comp of [main, ...Object.values(extra != null ? extra : {})]) {
app.component(comp.name, comp);
}
};
if (extra) {
for (const [key, comp] of Object.entries(extra)) {
;
main[key] = comp;
}
}
return main;
};
可以发现,上述代码使用到了我们熟悉的 app.component()
全局注册方式。关于更多组件组成的内容请阅读 Vue 深入组件 - 注册
让我们具体解释一下其实现。
这段代码定义了一个名为 withInstall
的高阶函数。该函数接受两个参数,main
和 extra
。它的作用是增强一个 Vue.js 组件,通过为其添加一个 install
方法来实现,该方法用于在 Vue 的组件系统中注册组件。此外,如果提供了额外的组件,它还会将它们添加到主要组件中。
以下是函数的详细解释:
- 在
main
组件上定义了一个install
方法,该方法遍历传入的所有组件(main
和extra
),并使用app.component(comp.name, comp)
将每个组件注册到 Vue.js 中。 - 如果提供了额外的组件,则会遍历
extra
中的每个键值对,并将它们作为属性添加到main
组件上。 - 最后,函数返回增强后的
main
组件。
这种函数通常用于 Vue.js 插件开发中,以简化注册组件和为现有组件添加额外功能的流程。
2. 组件实现:Badge 示例
最后,我们再分析一下 Badge
组件实现。
//node_modules/element-plus/es/components/badge/index.mjs
import { defineComponent } from 'vue';
import { badgeProps } from './badge.mjs';
import _export_sfc from '../../../_virtual/plugin-vue_export-helper.mjs';
const _hoisted_1 = ["textContent"];
const __default__ = defineComponent({
name: "ElBadge"
});
const _sfc_main = /* @__PURE__ */ defineComponent({
..._default__,
props: badgeProps,
setup(__props, { expose }) {
/* ... */
return (_ctx, _cache) {
/* ... */
}
}
});
var Badge = /* @__PURE__ */ _export_sfc(_sfc_main, [["__file", "badge.vue"]]);
export { Badge as default };
这段代码是一个 Vue 组件,用于创建一个徽章(badge)元素。其实现和我们在正常项目里定义 vue3 组件是没有什么区别的,组件主体结构还是比较清晰和容易理解的。
先看一下 const _hoisted_1 = ["textContent"];
,这在 Vue 的编译器中被称为 “提升的静态节点”。提升的静态节点是 Vue 编译器中的一个优化技术,用于在渲染过程中减少不必要的计算和重复操作,从而提高渲染性能。具体来说,当编译器发现某个节点是静态的(即在组件的生命周期内不会改变),并且它在多个地方被使用时,编译器会将这个节点提升为一个变量,在渲染函数中引用这个变量,而不是在每次渲染时都重新创建这个节点。这样可以减少渲染函数的体积,并且避免不必要的计算。
接着看 var Badge = /* @__PURE__ */ _export_sfc(_sfc_main, [["__file", "badge.vue"]]);
,这一行代码使用了 _export_sfc
函数将 _sfc_main
组件导出为 Badge
。在这里,它被用来导出名为 Badge 的组件,并传入了一个包含元数据的数组 [['__file', 'badge.vue']]
,其中包括了组件文件的相关信息。
我们继续深入 _export_sfc
这个封装的具体实现:
//node_modules/element-plus/es/_virtual/plugin-vue_export-helper.mjs
//var Badge = /* @__PURE__ */ _export_sfc(_sfc_main, [["__file", "badge.vue"]]);
var _export_sfc = (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
};
export { _export_sfc as default };
它的实现非常简单,作用是将 Badge
组件对象中的属性添加或更新为指定的值。
为什么 Element Plus UI 组件库的组件对于 Vue 的 sfc 加了一层 _export_sfc
这样的处理?
_export_sfc
函数的作用是将 Vue 单文件组件 (SFC) 中的一些属性注入到组件对象中。这样的处理通常是为了实现一些特定的功能或者约定,以方便在使用组件时进行配置和管理。
以下是一些可能的原因:
-
元数据注入: Vue 组件可以包含一些元数据,比如文件路径、组件名称等。通过在导出时将这些元数据注入到组件对象中,可以方便在组件内部或者外部获取这些信息,比如用于调试、文档生成等。
-
属性配置: 在组件库中,可能需要提供一些配置选项供用户进行定制。通过将配置选项传递给
_export_sfc
函数,可以将这些选项注入到组件对象中,从而实现在组件定义时配置组件的功能。 -
属性扩展: 有时候组件库会希望向组件对象中添加一些额外的属性或方法,以实现一些特定的功能或者增强组件的能力。通过在导出时使用
_export_sfc
函数,可以方便地向组件对象中添加这些属性或方法。 -
规范化处理: 组件库可能希望对组件的导出进行一些规范化处理,以确保所有的组件都符合一致的标准或者约定。通过将导出的组件对象传递给
_export_sfc
函数进行处理,可以统一规范组件的导出形式。
总的来说,这种对 Vue 组件的导出进行处理的方式,可以提供一种灵活、统一的管理方式,方便组件库的开发和使用。
最后,我贴出 Badge 的 setup 实现,里面涉及到很多 Vue 组件渲染函数实现需要用到的 API 感兴趣的朋友可以自行学习:
setup(__props, { expose }) {
/* ... */
return (_ctx, _cache) => {
return openBlock(), createElementBlock("div", {
class: normalizeClass(unref(ns).b())
}, [
renderSlot(_ctx.$slots, "default"),
createVNode(Transition, {
name: `${unref(ns).namespace.value}-zoom-in-center`,
persisted: ""
}, {
default: withCtx(() => [
withDirectives(createElementVNode("sup", {
class: normalizeClass([
unref(ns).e("content"),
unref(ns).em("content", _ctx.type),
unref(ns).is("fixed", !!_ctx.$slots.default),
unref(ns).is("dot", _ctx.isDot),
_ctx.dotClass,
_ctx.badgeClass
]),
style: normalizeStyle(unref(style)),
textContent: toDisplayString(unref(content))
}, null, 14, _hoisted_1), [
[vShow, !_ctx.hidden && (unref(content) || _ctx.isDot)]
])
]),
_: 1
}, 8, ["name"])
], 2);
};
}
1.3 按需引入底层实现
您需要使用额外的插件来导入要使用的组件。因此要学习的也是插件的实现。对于组件本身来说,我们前面介绍过:
//node_modules/element-plus/es/components/badge/index.mjs
import '../../utils/index.mjs';
import Badge from './src/badge2.mjs';
export { badgeProps } from './src/badge.mjs';
import { withInstall } from '../../utils/vue/install.mjs';
const ElBadge = withInstall(Badge);
export { ElBadge, ElBadge as default };
其中的 export { ElBadge, ElBadge as default };
的 ElBadge
就是支持单个组件被注册使用。只是说,还需要插件来支持样式的自动引入。甚至做到组件和样式都自动引入。
1. 手动导入
Element Plus 提供了基于 ES Module 的开箱即用的 Tree Shaking 功能。但你需要安装 unplugin-element-plus 来导入样式。配置文档参考 docs.
// vite.config.ts
import { defineConfig } from 'vite'
import ElementPlus from 'unplugin-element-plus/vite'
export default defineConfig({
// ...
plugins: [ElementPlus()],
})
<template>
<el-button>我是 ElButton</el-button>
</template>
<script>
import { ElButton } from 'element-plus'
export default {
components: { ElButton },
}
</script>
unplugin-element-plus/vite
可以让上述 import 自动引入相关样式:
import { ElButton } from 'element-plus'
// ↓ ↓ ↓ ↓ ↓ ↓
import { ElButton } from 'element-plus'
import 'element-plus/es/components/button/style/css'
2. 自动导入
自动导入会根据组件的使用情况自动引入所需的组件和样式,无需手动导入和注册。即不需要如下代码:
import { Button } from 'element-plus'
首先你需要安装 unplugin-vue-components 和 unplugin-auto-import 这两款插件:
npm install -D unplugin-vue-components unplugin-auto-import
然后把下列代码插入到你的 Vite 或 Webpack 的配置文件中,以 Vite 为例:
// 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({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
二、全局配置
以 Vue3 为例,参考 Element UI 快速开始
在引入 ElementPlus 时,可以传入一个包含 size
和 zIndex
属性的全局配置对象。 size
用于设置表单组件的默认尺寸,zIndex
用于设置弹出组件的层级,zIndex
的默认值为 2000。后者配置在一些第三方组件覆盖自定义组件的场景修复非常实用。
- 完整引入
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus, { size: 'small', zIndex: 3000 })
- 按需引入
<el-config-provider :size="size" :z-index="zIndex">
<app />
</el-config-provider>
import { defineComponent } from 'vue'
import { ElConfigProvider } from 'element-plus'
export default defineComponent({
components: {
ElConfigProvider,
},
setup() {
return {
zIndex: 3000,
size: 'small',
}
},
})
国际化配置也可以通过上述方式,更多细节可以参考阅读 HOW - 开源表格组件封装和使用(含国际化配置)