{T}

设计通用组件库

环境搭建

创建 Vite 项目模板选择 vue-ts

javascript
npm create vite@latest

布局组件

组件分成基础组件、表单组件、数据组件、通知组件、导航组件和其他组件几个类型,这些类型基本覆盖了组件库的适用场景,项目中的业务组件也是由这些类型组件拼接而来的

  • 基础组件 包括布局、色彩,字体、图标等等。这些组件基本没有 JavaScript 的参与,实现起来也很简单,负责的就是项目整体的布局和色彩设计
  • 表单组件 则负责用户的输入数据管理,包括常见的输入框、滑块、评分等等。总结来说,需要用户输入的地方就是表单组件的应用场景,其中对用户的输入校验是比较重要的功能点。
  • 数据组件负责显示后台的数据,最重要的就是表格和树形组件
  • 通知组件负责通知用户操作的状态,包括警告和弹窗

布局容器

设计基础的布局组件,这个组件相对是比较简单的。共有 container、header、footer、aside、main 五个组件,这个组合可以很方便地实现常见的页面布局

  • el-container 组件负责外层容器
  • el-header、el-aside、el-main、el-footer 组件分别负责顶部和侧边栏,页面主体和底部容器组件

src/components 目录下新建 container/Container.vue 布局组件没有交互逻辑,只需要通过 flex 布局就可以实现

在 element-plus 中所有的样式前缀都是 el 开头,每次都重复书写维护太困难,设计之初就需要涉及 Sass 的 Mixin 来提高书写 CSS 的代码效率

src/styles/mixin.scss 定义了 namespace 变量为 xy,使用 Mixin 注册一个可以重复使用的模块 b,可以通过传进来的 block 生成新的变量 $B,并且变量会渲染在 class 上,并且注册了 when 可以新增 class 选择器,实现多个 class 的样式

scss
// bem

$namespace: 'xy';
@mixin b($block) {
  $B: $namespace + '-' + $block !global;
  .#{$B} {
    @content;
  }
}

// 添加ben后缀啥的
@mixin when($state) {
  @at-root {
    &.#{$state-prefix + $state} {
      @content;
    }
  }
}

container.vue

Vue SFC
<style lang="scss">
  @import '../styles/mixin';
  @include b(container) {
    display: flex;
    flex-direction: row;
    flex: 1;
    flex-basis: auto;
    box-sizing: border-box;
    min-width: 0;
    @include when(vertical) {
      flex-direction: column;
    }
  }
</style>

使用 b(container) 生成 .xy-container 样式类,在内部使用 when(vertical) 生成 .el-container.is-vertical 样式类,去修改 flex 的布局方向

css
.xy-container {
  display: flex;
  flex-direction: row;
  flex: 1;
  flex-basis: auto;
  box-sizing: border-box;
  min-width: 0;
}

.xy-container.is-vertical {
  flex-direction: column;
}

container 组件如果内部没有 header 或者 footer 组件,就是横向布局,否则就是垂直布局。根据上面的 CSS 代码可以知道,只需要新增 is-vertical 这个 class,就可以实现垂直布局

因为开发组件库的时候要确保每个组件都有自己的名字,script setup 中没法返回组件的名字,需要一个单独的标签,使用 options 语法设置组件的 name 属性

Vue SFC
<template>
  <section class="el-container":class="{ 'is-vertical': isVertical }">
    <slot />
  </section> 
</template>
<script lang="ts">
  export default{
    name:'ElContainer'
  }
</script>
<script setup lang="ts">

  import {useSlots,computed,VNode,Component} from 'vue'

  interface Props {
    direction?:string
  }
  const props = defineProps<Props>()

  const slots = useSlots()

  const isVertical = computed(() => {
    if (slots && slots.default) {
      return slots.default().some((vn:VNode) => {
        const tag = (vn.type as Component).name
        return tag === 'ElHeader' || tag === 'ElFooter'
      })
    } else {
      return props.direction === 'vertical'
    }
  })
</script>

这样 container 组件就实现,其他四个组件实现起来大同小异。以 header 组件举例,在 container 目录下新建 Header.vue。

Header.vue

template 中渲染 xy-header 样式类,通过 defineProps 定义传入的属性 height,并且通过 withDefaults 设置 height 的默认值为 60px,通过 b(header) 的方式实现样式,用到的变量都在 style/mixin 中注册,方便多个组件之间的变量共享

Vue SFC
<template>
  <header class="xy-header" :style="{ height }">
    <slot />
  </header>
</template>

<script lang="ts">
export default{
  name:'ElHeader'
}
</script>

<script setup lang="ts">
import {withDefaults} from 'vue'

interface Props {
  height?:string
}
withDefaults(defineProps<Props>(),{
  height:"60px"
})

</script>

<style lang="scss">
@import '../styles/mixin';
@include b(header) {
  padding: $--header-padding;
  box-sizing: border-box;
  flex-shrink: 0;
}
</style>

组件注册

组件注册完毕之后,在 src/App.vue 中使用 import 语法导入后就可以直接使用了。但是这里有一个问题,组件库最后会有很多组件对外暴露,用户每次都 import 的话确实太辛苦了,所以还需要使用插件机制对外暴露安装的接口,在 container 目录下新建 index.ts

对外暴露了一个对象,对象的 install 方法中使用 app.component 注册这五个组件

javascript
import { App } from "vue"

import XyContainer from "./Container.vue"
import XyHeader from "./Header.vue"
import XyFooter from "./Footer.vue"
import XyAside from "./Aside.vue"
import XyMain from "./Main.vue"

export default {
  install(app: App) {
    app.component(XyContainer.name!, XyContainer)
    app.component(XyHeader.name!, XyHeader)
    app.component(XyFooter.name!, XyFooter)
    app.component(XyAside.name!, XyAside)
    app.component(XyMain.name!, XyMain)
  }
}

src/main.ts 文件中使用 app.use(ElContainer) 的方式注册全部布局组件,这样在项目内部就可以全局使用 contain、header 等五个组件。实际的组件库开发过程中,每个组件都会提供一个 install 方法,可以很方便地根据项目的需求按需加载

javascript
import ElContainer from './Container.vue'

/* istanbul ignore next */
ElContainer.install = function (app) {
  app.component(ElContainer.name, ElContainer)
}
export default ElContainer

main.js 中安装组件

javascript
import { createApp } from 'vue'
import App from './App.vue'
import ElContainer from './components/container'

const app = createApp(App)
app.use(ElContainer).use(ElButton).mount('#app')

组件使用

src/App.vue 的代码中使用组件嵌套的方式就可以实现下面的页面布局

html
<template>
  <xy-container>
    <xy-header>Header</xy-header>
    <xy-main>Main</xy-main>
    <xy-footer>Footer</xy-footer>
  </xy-container>
  <hr />

  <xy-container>
    <xy-header>Header</xy-header>
    <xy-container>
      <xy-aside width="200px">Aside</xy-aside>
      <xy-main>Main</xy-main>
    </xy-container>
  </xy-container>
  <hr />
  <xy-container>
    <xy-aside width="200px">Aside</xy-aside>
    <xy-container>
      <xy-header>Header</xy-header>
      <xy-main>Main</xy-main>
      <xy-footer>Footer</xy-footer>
    </xy-container>
  </xy-container>
</template>

<script setup lang="ts"></script>
<style>
  body {
    width: 1000px;
    margin: 10px auto;
  }
</style>