WHAT - 手写组件库系列(二)

下面的内容我们都将参考 Element Plus UIArco Design 官方内容来进行阐述底层实现。

一、组件库引入方式

1.1 Element Plus UI 示例

以 Vue3 为例,参考 Element Plus UI 快速开始

  1. 安装
# npm
npm install element-plus --save

# yarn
yarn add element-plus

下载完毕,可以看到如下目录:
请添加图片描述

在 Element Plus UI 的 npm 包中,这些文件和目录通常承担以下作用:

  1. dist: 这个目录通常包含已经打包好的、用于生产环境的代码。里面包含了压缩过的 JavaScript 文件、样式表文件、国际化文件以及其他相关文件。

  2. es: 这个目录通常包含了 ES 模块(ECMAScript 模块)的源代码,通常是未经过编译的、用于现代构建工具的代码。

  3. lib: 这个目录通常包含了编译后的、用于旧版构建工具或浏览器的代码,例如 CommonJS 或者 UMD(Universal Module Definition)模块。

  4. them-chalk: 这个目录包含了一个主题(通常是命名为 “chalk”)的样式文件,该文件提供了 Element Plus UI 的默认主题。

  5. global.d.ts: GlobalComponents for Volar,用于提供类型检查和自动补全。Volar 是一个专为 Vue.js 生态系统设计的 TypeScript 语言服务器,它提供了强大的智能代码补全、类型检查、重构等功能,以提升 Vue.js 项目的开发效率和代码质量。

  6. package.json: 这个文件是 npm 包的描述文件,包含了该包的元数据(如名称、版本、依赖等)以及一些配置信息。

  7. README.md: 这个文件通常是包的说明文档,包含了使用说明、API 文档、贡献指南等信息。

这些文件和目录组成了 Element Plus UI 的 npm 包,开发者可以根据需要选择合适的文件来使用。

  1. 完整引入
// 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')
  1. 按需引入

如果使用模板方式进行开发,一般需要使用额外的插件来开启按需加载及自动导入的支持。

首先你需要安装 unplugin-vue-componentsunplugin-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) 做了以下几件事情:

  1. 调用 Element Plus 中的 install 方法。
  2. install 方法中,Element Plus 注册了它的所有组件到 Vue 实例中。
  3. 一旦组件被注册到 Vue 实例中,它们就可以在整个应用中使用了。

所以,当你调用 app.use(ElementPlus) 后,你就可以在你的 Vue 应用中使用 Element Plus 提供的所有组件了,因为这些组件已经被注册到了 Vue 实例中。

这里我们将通过本地运行一个 vite 项目并引入 Element Plus UI 的方式来深入源码进行学习。

  1. vite 创建项目
npm create vite@latest
  1. 引入 Element Plus UI
npm install element-plus --save

在这里插入图片描述

npm fund 是 Node Package Manager(npm)生态系统中的一个命令,允许包维护者指定其包的资助信息。这些信息可以包括到平台(如 Open Collective、GitHub Sponsors 或 Patreon)的链接,用户可以通过这些平台对他们依赖的包的开发和维护进行财务支持。

  1. 完整引入
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 函数的签名或类型定义。它接受两个参数:appoptionsapp 参数是一个 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 的高阶函数。该函数接受两个参数,mainextra。它的作用是增强一个 Vue.js 组件,通过为其添加一个 install 方法来实现,该方法用于在 Vue 的组件系统中注册组件。此外,如果提供了额外的组件,它还会将它们添加到主要组件中。

以下是函数的详细解释:

  1. main 组件上定义了一个 install 方法,该方法遍历传入的所有组件(mainextra),并使用 app.component(comp.name, comp) 将每个组件注册到 Vue.js 中。
  2. 如果提供了额外的组件,则会遍历 extra 中的每个键值对,并将它们作为属性添加到 main 组件上。
  3. 最后,函数返回增强后的 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) 中的一些属性注入到组件对象中。这样的处理通常是为了实现一些特定的功能或者约定,以方便在使用组件时进行配置和管理。

以下是一些可能的原因:

  1. 元数据注入: Vue 组件可以包含一些元数据,比如文件路径、组件名称等。通过在导出时将这些元数据注入到组件对象中,可以方便在组件内部或者外部获取这些信息,比如用于调试、文档生成等。

  2. 属性配置: 在组件库中,可能需要提供一些配置选项供用户进行定制。通过将配置选项传递给 _export_sfc 函数,可以将这些选项注入到组件对象中,从而实现在组件定义时配置组件的功能。

  3. 属性扩展: 有时候组件库会希望向组件对象中添加一些额外的属性或方法,以实现一些特定的功能或者增强组件的能力。通过在导出时使用 _export_sfc 函数,可以方便地向组件对象中添加这些属性或方法。

  4. 规范化处理: 组件库可能希望对组件的导出进行一些规范化处理,以确保所有的组件都符合一致的标准或者约定。通过将导出的组件对象传递给 _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-componentsunplugin-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 时,可以传入一个包含 sizezIndex 属性的全局配置对象。 size 用于设置表单组件的默认尺寸,zIndex 用于设置弹出组件的层级,zIndex 的默认值为 2000。后者配置在一些第三方组件覆盖自定义组件的场景修复非常实用。

  1. 完整引入
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 })
  1. 按需引入
<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 - 开源表格组件封装和使用(含国际化配置)

  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值