Element Plus 组件库相关技术揭秘:7. 组件实现的基本流程及 Icon 组件的实现

·  阅读 7946

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究

前言

本章节我们将要实现 Icon 组件,Icon 组件应该是所有组件里面最简单的一个组件了,所以我们由简入深,循序渐进进行学习。Icon 组件虽然简单,但它却包含了一个组件的全部基础流程,通过实现 Icon 组件进行理解 Element Plus 组件实现的基本原理。

我们其实在上篇《6. CSS 架构模式之 BEM 在组件库中的实践》已经实现了最简易的一个 Icon 组件,本章节将继续完善它。

组件目录结构

首先我们按以下目录结构完善我们的 Icon 组件目录,其他组件的基本目录结构跟此类似。

├── packages
│   ├── components
│   │   ├── icon
│   │   │   ├── __tests__       # 测试目录
│   │   │   ├── src             # 组件入口目录
│   │   │   │   ├── icon.ts     # 组件属性与 TS 类型
│   │   │   │   └── icon.vue    # 组件模板内容
│   │   │   ├── style           # 组件样式目录
│   │   │   └── index.ts        # 组件入口文件
│   │   └── package.json
复制代码

通过上面的 Icon 组件目录结构,我们看出 Vue3 Composition API 的优势,可以根据逻辑功能来组织代码,从而实现高内聚,低耦合。上述 Icon 组件具体操作就是把组件属性与 TS 类型抽离放在了独立的一个文件中,这样就使得我们的程序代码在可维护性和灵活性方面可以做得非常好,从而让我们的项目维护成本降低。 没有 Composition API 之前 Vue 相关业务的代码需要配置到 Option 的特定的区域,导致代码可复用性不高,这样当项目非常庞大的时候会让后期的维护变得比较困难,从而导致项目成本增加。其实当你的项目非常庞大的时候,共享和复用代码则变得尤为重要。

定义组件属性 prop

我们知道父组件可以通过 prop 向子组件传递数据。首先需要在组件内部注册一些自定义的属性,称为 prop,这些 prop 是在组件的 props 选项中定义的。在使用组件的时候,就可以将在组件 props 选项中定义的属性名称作为组件元素的属性名来使用,通过属性向组件传递数据。

同时定义组件属性也是组件封装的一项重要步骤,首先我们在封装组件的时候,就要考虑我们的组件需要哪些属性,比如我们 Element Plus 中的 Icon 组件就只有下面两项属性。

属性名说明类型默认值
colorsvg 的 fill 颜色Pick<CSSProperties, 'color'>继承颜色
sizeSVG 图标的大小,size x sizenumber 、 string继承字体大小

我们希望父组件通过 prop 传递的数据类型是符合要求的,可以例如 Vue 提供的 prop 验证机制,在定义 props 选项时,可以使用一个带验证需求的对象。即在 packages/components/icon/src/icon.ts 文件进行如下定义:

export const iconProps = {
  color: String,
  size: [Number, String], // size 可以是数字,也可以是字符串
}
复制代码

验证的类型(type)可以是下列原生构造函数中的一个:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbool

单向数据流

通过 prop 传递数据是单向的,父组件的属性变化会向下传递给子组件,但是反过来不行。这可以防止子组件意外改变父组件的状态,从而导致组件的数据流难以理解。基于这个特性在我们使用 TypeScript 开发的时候,就会对 props 定义成只读属性。通过 as const 则可以快速将一个对象变成只读类型,常量断言可以把一个值标记为一个不可篡改的常量,从而让 TS 以最严格的策略来进行类型推断。

export const iconProps = {
  color: String,
  size: [Number, String], // size 可以是数字,也可以是字符串
} as const
复制代码

as const 是 TS 的语法,它告诉 TS 它所断言的值以及该值的所有层级的子属性都是不可篡改的,故对每一级子属性都会做最严格的类型推断。

但 TypeScript 的自动类型推断并不能推断出我们想要对象的类型。

autoType.png

在 TypeScript 中类有 2 种类型, 静态类型实例类型, 如果是构造函数类型, 那么返回的则是实例类型。我们在原生 Vue3 中定义 props 类型,其实是一个构造函数,比如上述我们定义 color 的类型是 String,但 String 只是一个构造函数,并不是 TypeScript 中的 string 类型,String 构造函数在 TypeScript 的类型是它的构造函数类型: StringConstructor ,但这并不是我们需要的,我们希望 String 构造函数返回的是字符串类型 string

在 Vue3 中提供了自带的 Props 类型声明:ExtractPropTypes ,它的作用是接收一个类型,然后把对应的所接收的 props 类型返回出来,同时如果是构造函数类型则转换成对应的类型,比如 StringConstructor 转换成 string

import type { ExtractPropTypes, PropType } from 'vue'

export const iconProps = {
  color: String,
  size: [Number, String] as PropType<number | string>,
} as const

export type Props = ExtractPropTypes<typeof iconProps>
复制代码

其中数组项,还需要通过 Vue3 内置的 PropType 类型声明进行具体的类型断言声明。

此外我们看到在导入相关类型声明的时候使用的是 import type,在此我们也稍微补充一些 import type 小知识:import type 仅仅导入被用于类型注解或声明的声明语句,它总是会被完全删除,因此在运行时将不会留下任何代码。与此类似的 export type 也是仅仅提供一个用于类型的导出,在 TypeScript 输出文件中,它也将会被删除。那么使用 import 的话,TypeScript 是无法判断你是想导出类型还是一个 JavaScript 的方法或者变量,而当你导入的是仅仅是类型的时候,当 TypeScript 编译之后,类型会被删除,你的代码就会报错,但通过TypeScript 的 isolatedModules 编译选项也可以进行预警这种写法是错误的。所以 TypeScript 提供了 import type or export type,用来明确表示我引入/导出的是一个类型,而不是一个变量或者方法。

import type xxx from 'xxx'
export type xxx
复制代码

最后我们还需要把 SFC 的 icon.vue 文件的实例类型返回出去:

import type Icon from './icon.vue'
export type IconInstance = InstanceType<typeof Icon>
复制代码

TypeScript 中的 InstanceType 函数:该函数返回(构造) 由某个构造函数构造出来的实例类型组成的类型。

在 Element Plus 中在创建 props 的类型定义的 TypeScript 类型是非常复杂的,日后有机会将在单独 TypeScript 的章节来展开说明,这里更多讲解的是组件的逻辑流程。

通过 script setup 编写 SFC 组件

我们在上一篇文章《6. CSS 架构模式之 BEM 在组件库中的实践》中已经实现了以下内容:

<template>
  <i :class="bem.b()">
    <slot />
  </i>
</template>

<script setup lang="ts">
import { useNamespace } from '@cobyte-ui/hooks'
const bem = useNamespace('icon')
</script>
复制代码

Icon 组件只是一个标签然后接收一个图标,所以 template 部分非常简单,一个 i 标签通过插槽接收图标内容,插槽也是父子组件通讯方式的一种,同时我们在上一篇中实现了 CSS 的 BEM 相关的逻辑,这一块内容本文便不再进行过多讲解了。

我们在上文中已经在 icon.ts 文件中定义好了 Icon 组件的 Props,接下来我们要在 icon.vue 中实现它。我们是通过 script setup 方式编写的 SFC 组件,那么通过这种方式编写的组件,我们则是通过 defineProps 编译宏命令来进行声明 props,同时声明的 props 会自动暴露给模板。

import { iconProps } from './icon'
const props = defineProps(iconProps)
复制代码

defineProps 会返回一个对象,其中包含了可以传递给组件的所有 props。

我们在 icon.ts 中定义了两个 Icon 组件的 props: size 和 color,然后用户可以通过这两个属性设置 Icon 组件的样式。接下来我们则要去实现这两个功能。

import type { CSSProperties } from 'vue'
// CSSProperties 是 Vue3 提供的 CSS 属性的类型
const style = computed<CSSProperties>(() => {
  if (!props.size && !props.color) return {}

  return {
    fontSize: isUndefined(props.size) ? undefined : addUnit(props.size),
    '--color': props.color, // 通过 CSS 变量方式进行设置 color
  }
})
复制代码

我们通过计算属性去计算出通过 props 传递过来的 size 和 color 属性得到 Icon 组件的样式。最后我们的 template 中需要在 i 标签中添加 style 的属性绑定::style="style"

Vue 组件中的 CSS 变量

CSS 变量(CSS variable)又叫做 "CSS 自定义属性"(CSS custom properties),声明变量的时候,变量名前面要加两根连词线(--),例如上文的颜色变量:--color。为什么选择两根连词线(--)表示变量?因为 $ 被 Sass 用掉了,@ 被 Less 用掉了。为了不产生冲突,官方的 CSS 变量就改用两根连词线了。

我们知道 Icon 的图标有可能是一个字体类型的图标,或者是 SVG 的图标,字体类型的图标直接可以通过设置 CSS 的 color 属性来设置图标颜色;而 SVG 图标可以在 SVG 文件中更改 fill 属性进行修改图片,将 fill 属性改成 currentColor,然后通过继承父元素 color 属性可以改变颜色。这样就同样可以通过设置 CSS 的 color 属性来设置图标的颜色了。

在 Vue 中可以通过在行内的 style 属性中定义 CSS 变量,然后就可以通过 Vue 的动态变量控制 CSS 变量,再在 style 标签中使用行内定义好的 CSS 变量。使用 CSS 变量可以通过 var 关键进行获取定义的 CSS 变量,例如:var(--color)

template 中的设置:

<template>
	<div class="content">
       <i class="el-icon" :style="{'--color': color}">
           
        </i> 
    </div>
</template>
复制代码

script setup 中的设置:

import { computed, ref } from 'vue'
const color = ref('green')
复制代码

通过以上设置就可以在 style 标签中通过 var 获取 .info 行内设置的 CSS 变量了。

.info {
    color: var(--color)
}
复制代码

最终渲染到页面的结果如下图:

--color.png

变量范围

  • CSS 变量可以在任何元素内定义
  • 将 CSS 自定义属性添加到 :root 使其可用于页面中的所有元素
  • 如果在某个选择器内添加变量,则只可以在该选择器中可使用,这也就是 CSS 的变量作用域
  • 在 Vue 组件中,如果要该组件都可以使用,则必须放置在根元素下

在 Vue3 中的 SFC 组件中,可以在 style 标签通过 vars 进行绑定:<style vars="{ color }">

<template>
  <div class="text">稀土掘金</div>
</template>

<script>
export default {
  data() {
    return {
      color: "green",
    };
  },
};
</script>

<style vars="{ color }">
.text {
  color: var(--color);
}
</style>
复制代码

其中的原理就是这些变量会直接绑定到组件的根元素上,这就符合我们上面所说的 CSS 变量范围的规则了。

最后我们的主题样式中的 icon.scss 需要修改成以下内容:

@include b(icon) {
  --color: inherit;
  height: 1em;
  width: 1em;
  line-height: 1em;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  position: relative;
  fill: currentColor; /* 常规css中是没有fill属性的,只在XML-CSS中存在,用于设置当前元素的填充内容,例如颜色,图片 */ 
  color: var(--color);
  font-size: inherit;

  svg {
    height: 1em;
    width: 1em;
  }
}
复制代码

主要是字体颜色是通过 CSS 变量进行设置的,另外添加 fill 属性的内容,常规 CSS 中是没有 fill 属性的,只在XML-CSS 中存在,用于设置当前元素的填充内容,例如颜色,图片。这里主要是将 fill 属性改成 currentColor,然后 SVG 图片可以通过继承父元素 color 属性可以改变颜色。

组件注册的方式

我们去到 play 目录下的 src 目录中的 App.vue 文件中把上面写的 Icon 组件进行引入测试。在 Vue3 中有两种写 SFC 组件的方式,一种就是我们上面所介绍到的 script setup 方式,如果是通过 script setup 方式,那么相关代码如下:

<template>
  <div>
    <el-icon :color="'green'" :size="12">Icon</el-icon>
  </div>
</template>
<script setup lang="ts">
import ElIcon from '@cobyte-ui/components/icon'
import '@cobyte-ui/theme-chalk/src/index.scss'
</script>
<style scoped></style>
复制代码

还有一种就是不是使用 script setup 的方式,也就是使用 defineComponent 定义组件的方式,代码如下:

<template>
  <div>
    <el-icon :color="'green'" :size="12">Icon</el-icon>
  </div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import ElIcon from '@cobyte-ui/components/icon'
import '@cobyte-ui/theme-chalk/src/index.scss'

export default defineComponent({
  name: 'App',
  components: {
    ElIcon,
  },
  setup() {},
})
</script>
<style scoped></style>
复制代码

我们可以看到使用 defineComponent 定义组件,引入其他组件需要使用 components 选项进行注册,才能在 template 中使用。这两种都是属于局部注册组件的方式,我们知道除了局部注册组件,还可以进行全局注册组件。我们在本专栏的第一篇文章《Vue3 组件库的设计和实现原理》中已经解析了组件库的设计原理。在这里我们再进行复习和实践。

通过《Vue3 组件库的设计和实现原理》这篇文章我们可以知道 Vue3 组件库的基本实现原理:就是为每一个组件进行安装一个插件,然后通过插件进行组件的全局安装,再把所有的组件设置到一个数组中组成一个组件库(其实就是一个包含各种组件的数组),再编写一个组件库插件,在组件库插件里面进行循环数组组件库里的每一个组件,因为每一个组件都拥有插件所需的 install() 方法,所以每一个组件又是一个插件,又可以调用 use() 方法进行安装,最后就会执行每一个组件的 install() 方法,然后进行进行组件的全局安装,这样组件库里面的每个组件都将被注册到全局组件中去了。

又因为我们的组件库又许多组件,我们需要给每一个组件都添加一个 install 方法,我们可以把这个方法进行封装成为一个公共方法。我们在 packages/utils/vue/install.ts 中添加以下代码:

import type { Plugin } from 'vue'
// 通过 Vue 提供的 Plugin 类型和传进来的组件类型 T 的集合进行确定我们的组件类型具有 Plugin 类型方法,如 install 方法
export type SFCWithInstall<T> = T & Plugin
export const withInstall = <T>(comp: T) => {
  ;(comp as SFCWithInstall<T>).install = function (app) {
    // 组件的注册名称参数暂时是写死了 ElIcon,在后面的小节,我们再详细说明如何进行设置动态组件名称
    app.component('ElIcon', comp as SFCWithInstall<T>)
  }
  return comp as SFCWithInstall<T>
}
复制代码

注意,此时我们在 app.component('ElIcon', comp as any) 组件的注册名称参数是写死了 ElIcon,因为我们的组件是通过 script setup 方式实现的,无法设置组件的名称。在后面的小节,我们再详细说明如何进行设置动态组件名称。

packages/components/icon/index.ts 中的代码为:

import { withInstall } from '@cobyte-ui/utils'
import Icon from './src/icon.vue'
// 通过 withInstall 方法给 Icon 添加了一个 install 方法
const ElIcon = withInstall(Icon)
export default ElIcon
// 导出 Icon 组件的 props
export * from './src/icon'
复制代码

接下来,我们按组件库的方式在 play/main.ts 中进行全局安装引入 Icon 组件。

import { createApp } from 'vue'
import ElIcon from '@cobyte-ui/components/icon'
import '@cobyte-ui/theme-chalk/src/index.scss'
import App from './src/App.vue'
// 组件库
const components = [ElIcon]
// 是否已安装标识
const INSTALLED_KEY = Symbol('INSTALLED_KEY')
// 组件库插件
const ElementPlus = {
  install(app: any) {
    // 如果该组件库已经安装过了,则不进行安装
    if (app[INSTALLED_KEY]) return
    // 将标识值设置为 true,表示已经安装了
    app[INSTALLED_KEY] = true
    // 循环组件库中的每个组件进行安装
    components.forEach((c) => app.use(c))
  },
}

const app = createApp(App)
// 安装组件库
app.use(ElementPlus)
app.mount('#app')
复制代码

这个时候我们把 play/src/App.vue 中原来按需引入的 Icon 组件代码进行注释:

<script setup lang="ts">
// import ElIcon from '@cobyte-ui/components/icon'
// import '@cobyte-ui/theme-chalk/src/index.scss'
</script>
复制代码

然后我们再把 play 测试项目运行起来:pnpm run dev

icon.png

这样我们通过全局注册的方式也把组件运行起来了。

全局组件类型声明

Vue3 并没有对自定义全局组件做 TypeScript 类型支持处理。

ts-type.png

我们可以看到通过全局注册的 Icon 组件此时在模板中是没有任何类型提示的。我们如果想要全局注册的组件在模板中获得类型提示,就需要利用 TypeScript 的增强类型系统,进行扩展 Vue3 原来的类型系统。具体就是声明一个的 *d.ts 类型文件,通过使用声明文件对类型接口进行类型模块扩充并导出。

Vue3 SFC 文件的智能提示是 Volar 提供的, 其实在 Volar 的 README.md 文件中就有相关的提示:

Define Global Components

PR: vuejs/core#3399

Local components, Built-in components, native HTML elements Type-Checking is available with no configuration.

For Global components, you need to define GlobalComponents interface, for example:

// components.d.ts
declare module '@vue/runtime-core' {  // Vue 3
// declare module 'vue' {   // Vue 2.7
// declare module '@vue/runtime-dom' {  // Vue <= 2.6.14
  export interface GlobalComponents {
    RouterLink: typeof import('vue-router')['RouterLink']
    RouterView: typeof import('vue-router')['RouterView']
  }
}

export {}
/** 当我们的 tsconfig.json 中的 isolatedModules 设置为 true 时,如果某个 ts 文件中没有一个
import or export 时,ts 则认为这个模块不是一个 ES Module 模块,它被认为是一个全局的脚本,
这个时候在文件中添加任意一个 import or export 都可以解决这个问题。
**/
复制代码

参考 Volar 的文档,我们可以在根目录的 typings 文件夹下新建一个 components.d.ts 文件进行以下代码的实现:

import type Icon from '@cobyte-ui/components/icon'
// For this project development
import '@vue/runtime-core'

declare module '@vue/runtime-core' {
  // GlobalComponents for Volar
  export interface GlobalComponents {
    ElIcon: typeof Icon
  }
}

export {}
复制代码

通过上述设置我们的通过全局注册的 Icon 组件便有类型提示了。

el-icon-ts.png

script setup 组件的名称设置

我们在前面的小节进行全局注册组件的时候,组件名称是写死了的,那么写死了肯定是不行,我们需要动态设置组件的名称,我们现在这个小节就来解决这个问题。

我们知道通过 defineComponent 定义的 Vue3 组件是可以通过 name 属性进行设置组件的名称的

export default defineComponent({
  name: 'App',
  setup() {},
})
复制代码

而通过 script setup 方式则 Vue3 并没有提供设置组件名称的 API,但可以通过一种别捏的方式实现,代码如下:

<script lang="ts">
    export default{
        name: 'App'
    }
</script>
<script setup lang="ts">
// ...
</script>
复制代码

可以通过两个 script 标签来实现,但这种方式太不优雅了,在 Element Plus 中,官方开发者三咲智子 则提供了一个叫 unplugin-vue-define-options 来解决这个问题。

unplugin-vue-define-options

在 <script setup> 中可使用 defineOptions 宏,以便在 <script setup> 中使用 Options API。 尤其是能够在一个函数中设置 namepropsemit 和 render 属性。

特性

  • ✨ 有了这个宏,你就可以在 <script setup> 使用 Options API;
  • 💚 开箱即用支持 Vue 2 和 Vue 3;
  • 🦾 完全支持 TypeScript;
  • ⚡️ 支持 Vite、Webpack、Vue CLI、Rollup、esbuild 等, 由 unplugin 提供支持。

在根目录下进行安装

pnpm install unplugin-vue-define-options -D -w
复制代码

TypeScript 支持,在 tsconfig.web.json 文件中进行以下设置:

{
  "compilerOptions": {
    // ...
    "types": ["unplugin-vue-define-options/macros-global" /* ... */]
  }
}
复制代码

又因为我们的 play 项目中有使用到了 Vite,所以我们还需要 play 项目中的 vite.config.ts 文件中进行以下设置:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import DefineOptions from 'unplugin-vue-define-options/vite'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(), DefineOptions()],
})
复制代码

这样我们就可以在 packages/components/icon/src/icon.vue 中使用 defineOptions 宏,在 <script setup> 中通过 Options API 设置组件的名称了。代码如下:

<script setup lang="ts">
    defineOptions({
  		name: 'ElIcon',
	})
</script>
复制代码

最后我们就可以在组件的安装插件的方法上进行动态设置组件名称了。

import type { Plugin } from 'vue'
// 通过 Vue 提供的 Plugin 类型和传进来的组件类型 T 的集合进行确定我们的组件类型具有 Plugin 类型方法,如 install 方法
export type SFCWithInstall<T> = T & Plugin
export const withInstall = <T>(comp: T) => {
  ;(comp as SFCWithInstall<T>).install = function (app) {
    // 动态设置组件的名称
    const { name } = comp as unknown as { name: string }
    app.component(name, comp as SFCWithInstall<T>)
  }
  return comp as SFCWithInstall<T>
}
复制代码

Icon 中的图标

SVG 组件图标

我们在上面已经稍微讲过,Icon 中的图标有两种方式实现,其中一种是通过 SVG 图片,而通过 SVG 图片的实现方式本质就是实现一个 SVG 的组件。在 Vue3 中的实现是非常简单的,代码如下:

<template>
  <svg
    viewBox="0 0 1024 1024"
    xmlns="http://www.w3.org/2000/svg"
    data-v-029747aa=""
  >
    <path
      fill="currentColor"
      d="M832 512a32 32 0 1 1 64 0v352a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32h352a32 32 0 0 1 0 64H192v640h640V512z"
    />
    <path
      fill="currentColor"
      d="m469.952 554.24 52.8-7.552L847.104 222.4a32 32 0 1 0-45.248-45.248L477.44 501.44l-7.552 52.8zm422.4-422.4a96 96 0 0 1 0 135.808l-331.84 331.84a32 32 0 0 1-18.112 9.088L436.8 623.68a32 32 0 0 1-36.224-36.224l15.104-105.6a32 32 0 0 1 9.024-18.112l331.904-331.84a96 96 0 0 1 135.744 0z"
    />
  </svg>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  name: 'EditIcon',
})
</script>
复制代码

其中 template 中的内容便是一张 SVG 图片的源码,而 SVG 图片的源码获取方式有很多,我们这里提供其中一种,我们可以把设计师设计的图标上传到阿里的 iconfont 图标库,然后在下载该图标的时候,可以选择复制 SVG 源码。

SVG-code.png

有了 SVG 图标的组件之后,我们就可以直接导入在 Icon 组件中进行使用了。

<template>
  <div>
    <el-icon :color="'green'" :size="15"><EditIcon /></el-icon>
  </div>
</template>
<script setup lang="ts">
import EditIcon from './EditIcon.vue'
</script>
<style scoped></style>
复制代码

运行结果如下:

edit-icon-run.png

我们可以看到我们的图标成功渲染出来了。

字体图标

我们还可以通过字体图标进行设置:

<template>
  <div>
      <el-icon :color="'green'" :size="15">
      	<i class="iconfont iconlogistics-car"></i>
      </el-icon>
  </div>
</template>
<script setup lang="ts"></script>
<style>
@font-face {
  font-family: 'iconfont';
  src: url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAPIAAsAAAAACDAAAAN8AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACDMgqDKIJ8ATYCJAMUCwwABCAFhGcHXRszB8ieGjszMAURLRpbqK1NZLgai+Bpjd+83b37rkkUPJlGTXgTT1Q8iYdGtZBInqmizTP5JueqHACqHCL0ukm3u5zmUBE5JMlCIiqkp/Fvf/7/Tv0JTozFtidAC2TM7gl21TvSDl1Am7qfB7DYYs8/WVNbJGPOvRCigKCAWDUvXH2FqqsDgBEpuczPzki9YezKi3iOQLvV4mnu8nJLgemFOlUAR9qMziDOjQt9jgH63KDU5IZWoW67sYhHKtLtdMFD//vjn7WhT1JlxjH7T6WJwO4vsajNnU0n+Su3Q4CTEypkLF+ZKS7WOy7QCsPLaWkvG65tG6BdqySN2i+2qPxZahpiyd5U7eY/PEKSFaJmZHeCbShS+GlrIwQ/YxESPysRMj9rRW+nvALtsOg1wHOsmRzzL5IQXV0r2kzGaq3jaLnF+/VicyC71TjxsrzcdtZ20pCJG88Y4c4TKNefsuzjtxfkuCfx/iB25DlF3RiftC3VEO8xZFM2A0dvvO7J4xbdb/Dzbn5g56Vx48CdycHBDQeFt09l735ICJWYiystE0rLmTzH+aew04a1cR72nVcdHZHCyh8UhZbPmZ99R0vcYoHNW8yxRb45HFSQVL7tbneSLJMwwGzJPEoPP7L7Kzr+FhzVSmlQNP3r8HLHTW1uTh22uJ5mODzg2H9/M2RxRIVPtldGhle2zxbiIm7ETaq3xGEVEWLX0etr2H3G1qBe/aFD8yrrADTzzQyQLtFTkKU/+jve5Sz732AW+1+/o4AfK/GoX9YoMv6IUmvB/1Mea4laVB5SlrJm2yaoRvsTXom/1PDDPP3exlENtM4mtBqIIWkxAVmrKWwhLkGlw1qotdoM7ZZJ295hgIqWKG2Y0wMg9FqFpNsLyHqdYAvxHirDfkOt1z9odyBMDuswF6xNEEORCDa1Q55VKrClpn6vxYhrljGkKFCOiMBDHwzyD8zGMpACkSGWCC1cMKUYYqKUw3R6HZLJlFBFlBLEUn8xparogABc9iX+rFIOUhAIg0JEoCbtIB5LSQG76DT984ohnGYyDNISWx4mBDzsHQriL7AFNkOhaNVyJccIWnCCURQGYQRE5aB0bkNkSlgJUpWPk0BYlD9xj6BKtAATDbdV+M+vkj/gLmhnHFFFihwlqqh1XZhyvHAdKYIfyxCDO8jzo0ighmFbELUHAwA=')
    format('woff2');
}

.iconfont {
  font-family: 'iconfont' !important;
  font-size: 16px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.iconlogistics-car:before {
  content: '\e616';
}
</style>
复制代码

运行结果:

font-icon.png

字体图标也是可以通过阿里的 iconfont 图标库进行设置,这里就不作过多深入讲解了。

Element Plus 中的图标库

Element Plus 中的图标库又是通过一个独立的项目进行实现的,也就是你安装了 Element Plus 库之后,还要使用她的图标库,那么你还需要安装一个 npm 包:@element-plus/icons-vue。它的实现原理也就是我们上面的第一种 SVG 图标组件的方式,通过遍历读取每个 SVG 图标的源码,然后生成各自的 SVG 图标组件,然后引入的时候是通过全局注册的方式进行使用。这里就不作过多深入解析了。

总结

在本章节中我们通过实现 Icon 组件进行理解 Element Plus 组件实现的基本原理,Icon 组件虽然简单,但它却包含了一个组件的全部基础流程。

通过 icon 组件目录结构,我们看出 Vue3 Composition API 的优势,可以根据逻辑功能来组织代码。通过定义组件属性 prop,我们了解一些组件 prop 的特性,通过 prop 传递数据是单向的,父组件的属性变化会向下传递给子组件,但是反过来不行。这可以防止子组件意外改变父组件的状态,从而导致组件的数据流难以理解。

接着在实现 Icon 组件的过程,我们学习了如何在 Vue 组件中使用 CSS 变量。 Vue 中可以通过在行内的 style 属性中定义 CSS 变量,然后就可以通过 Vue 的动态变量控制 CSS 变量,再在 style 标签中使用行内定义好的 CSS 变量。

接着我们通过局部注册组件和全局注册组件的方式实现了 Icon 组件在 play 项目中的渲染展示。然而 Vue3 并没有对自定义全局组件做 TypeScript 类型支持处理,我们学习了利用 TypeScript 的增强类型系统,进行扩展 Vue3 原来的类型系统。

我们还学习了 Element Plus 中通过使用 defineOptions 宏,在 <script setup> 中使用 Options API 来解决 <script setup> 无法设置组件名称的问题。

最后我们讲解了在 Icon 组件中如何实现图标的,主要有两种方式,一种是 SVG 组件图标,一种是字体图标。

欢迎关注本专栏,了解更多 Element Plus 组件库知识。

本专栏文章:

1. Vue3 组件库的设计和实现原理

2. 组件库工程化实战之 Monorepo 架构搭建

3. ESLint 核心原理剖析

4. ESLint 技术原理与实战及代码规范自动化详解

5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

6. CSS 架构模式之 BEM 在组件库中的实践

7. 组件实现的基本流程及 Icon 组件的实现

8. 为什么组件库或插件需要定义 peerDependencies

分类:
前端
收藏成功!
已添加到「」, 点击更改