你好,我是大圣。上一讲TypeScript加餐学完,你是不是想赶紧巩固一下TypeScript在Vue中的使用呢?那么从今天开始,我们就重点攻克Vue中组件库的实现难点,我会用7讲的篇幅带你进入组件库的世界。

学习路径大致是这样的,首先我会给你拆解一下Element3组件库的代码,其次带你剖析组件库中一些经典的组件,比如表单、表格、弹窗等组件的实现细节,整体使用Vite+TypeScript+Sass的技术栈来实现。而业务中繁多的页面也是由一个个组件拼接而成的,所以我们可以先学习一下不同类型的组件是如何去设计的,借此举一反三。

环境搭建

下面我们直奔主题,开始搭建环境。这个章节的代码我已经推送到了Github 上,由于组件库是模仿Element实现的,所以我为其取名为ailemente。

接下来我们就一步步实现这个组件库吧。首先和开发项目一样,我们要在命令行里使用下面的命令创建Vite项目,模板选择vue-ts,这样我们就拥有了一个Vite+TypeScript的开发环境。

npm init vite@latest

图片

关于ESLint和Sass的相关配置,全家桶实战篇我们已经详细配置了,这里只补充一下husky的内容。husky这个库可以很方便地帮助我们设置Git的钩子函数,可以允许我们在代码提交之前进行代码质量的监测。

下面的代码中,我们首先安装和初始化了husky,然后我们使用 npx husky add命令新增了commit-msg钩子,husky会在我们执行git commit提交代码的时候执行 node scripts/verifyCommit命令来校验commit信息格式。

npm install -D husky # 安装husky
npx husky install    # 初始化husky
# 新增commit msg钩子
npx husky add .husky/commit-msg "node scripts/verifyCommit.js" 

然后我们来到项目目录下的verifyCommit文件。在下面的代码中,我们先去 .git/COMMIT_EDITMSG文件中读取了commit提交的信息,然后使用了正则去校验提交信息的格式。如果commit的信息不符合要求,会直接报错并且终止代码的提交。



const msg = require('fs')
  .readFileSync('.git/COMMIT_EDITMSG', 'utf-8')
  .trim()
  
const commitRE = /^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release)(\(.+\))?: .{1,50}/
const mergeRe = /^(Merge pull request|Merge branch)/
if (!commitRE.test(msg)) {
  if(!mergeRe.test(msg)){
    console.log('git commit信息校验不通过')

    console.error(`git commit的信息格式不对, 需要使用 title(scope): desc的格式
      比如 fix: xxbug
      feat(test): add new 
      具体校验逻辑看 scripts/verifyCommit.js
    `)
    process.exit(1)
  }

}else{
  console.log('git commit信息校验通过')
}

这样就确保在GitHub中的提交日志都符合type(scope): message 的格式。你可以看下Vue 3的代码提交记录,每个提交涉及的模块,类型和信息都清晰可见,能够很好地帮助我们管理版本日志,校验正则的逻辑。如下图,feat代表新功能,docs代表文档,perf代表性能。下面的提交日志就能告诉我们这次提交的是组件相关的新功能,代码中新增了Button.vue。

feat(component): add Button.vue

图片

commit-msg是代码执行提交的时候执行的,我们还可以使用代码执行之前的钩子pre-commit去执行ESLint代码格式。这样我们在执行git commit的同时,就会首先进行ESLint校验,然后执行commit的log信息格式检查,全部通过后代码才能提交至Git,这也是现在业界通用的解决方案,学完你就快去优化一下手里的项目吧!

npx husky add .husky/pre-commit "npm run lint"

布局组件

好,现在环境我们就搭建好了,接着看看怎么布局组件。

我们可以参考Element3组件列表页面,这里的组件分成了基础组件、表单组件、数据组件、通知组件、导航组件和其他组件几个类型,这些类型基本覆盖了组件库的适用场景,项目中的业务组件也是由这些类型组件拼接而来的。

我们还可以参考项目模块的规范搭建组件库的模板,包括Sass、ESLint等,组件库会在这些规范之上加入单元测试来进一步确保代码的可维护性。

接下来我们逐一讲解下各个组件的负责范围。

首先我们需要设计基础的组件,也就是整个项目中都会用到的组件规范,包括布局、色彩,字体、图标等等。这些组件基本没有JavaScript的参与,实现起来也很简单,负责的就是项目整体的布局和色彩设计。

而表单组件则负责用户的输入数据管理,包括我们常见的输入框、滑块、评分等等,总结来说,需要用户输入的地方就是表单组件的应用场景,其中对用户的输入校验是比较重要的功能点。

数据组件负责显示后台的数据,最重要的就是表格和树形组件。

通知组件负责通知用户操作的状态,包括警告和弹窗,如何用函数动态渲染组件是警告组件的重要功能点。

接下来我们就动手设计一个基础的布局组件,这个组件相对是比较简单的。你可以访问Element3布局容器页面,这里一共有container、header、footer、aside、main五个组件,这个组合可以很方便地实现常见的页面布局。

这几个组件只是提供了不同的class,这里就涉及到CSS的设计内容。在Element3中所有的样式前缀都是el开头,每次都重复书写维护太困难,所以我们设计之初就需要涉及Sass的Mixin来提高书写CSS的代码效率。

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

// bem

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

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

代码看着有些抽象,不要急,我们再在 container.vue中写上下面的代码。使用@import导入mixin.scss后,就可以用include语法去使用Mixin注册的代码块。

<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)生成.el-container样式类,在内部使用when(vertical)生成.el-container.is-vertical样式类,去修改flex的布局方向。

.el-container {
  display: flex;
  flex-direction: row;
  flex: 1;
  flex-basis: auto;
  box-sizing: border-box;
  min-width: 0;
}
.el-container.is-vertical {
  flex-direction: column;
}

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

我们在Container.vue中写下下面的代码,template中使用el-container容器包裹,通过:class来实现样式控制即可。然后你肯定会疑惑,为什么会有两个script标签?

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

然后在<script setup>标签中,添加lang="ts"来声明语言是TypeScript。Typescript实现组件的时候,我们只需要使用interface去定义传递的属性类型即可。使用defineProps()实现参数类型校验后,我们再使用computed去判断container的方向是否为垂直,手动指定direction和子元素中有el-header或者el-footer的时候是垂直布局,其他情况是水平布局。

<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。

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

<template>
  <header
    class="el-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>

组件注册

aside、footer和main组件代码和header组件基本一致,你可以在这次提交中看到组件的变更。

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

import {App} from 'vue'
import ElContainer from './Container.vue'
import ElHeader from './Header.vue'
import ElFooter from './Footer.vue'
import ElAside from './Aside.vue'
import ElMain from './Main.vue'

export default {
  install(app:App){
    app.component(ElContainer.name,ElContainer)
    app.component(ElHeader.name,ElHeader)
    app.component(ElFooter.name,ElFooter)
    app.component(ElAside.name,ElAside)
    app.component(ElMain.name,ElMain)
  }
}

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

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')

组件使用

后面我会教你设计组件库的文档系统,这里我们就先用App.vue来演示一下组件效果。

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

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

<el-container>
  <el-header>Header</el-header>
  <el-container>
    <el-aside width="200px">Aside</el-aside>
    <el-main>Main</el-main>
  </el-container>
</el-container>
  <hr>
<el-container>
  <el-aside width="200px">Aside</el-aside>
  <el-container>
    <el-header>Header</el-header>
    <el-main>Main</el-main>
    <el-footer>Footer</el-footer>
  </el-container>
</el-container>
</template>
<script setup lang="ts">
</script>
<style>

body{
  width:1000px;
  margin:10px auto;
}
.el-header,
.el-footer {
  ..设置额外的css样式
}
</style>

图片

总结

今天的课程到这里就结束了,我们来总结一下今天学到的内容吧。

首先我们介绍了组件库中的组件分类,基础组件、表单组件、数据组件和通知组件,这些组件类型组合拼接,就可以开发出功能繁多的项目。其中,基础组件负责整体布局和色彩的显示,表单组件负责用户的输入,数据组件负责数据的显示,通知组件用于数据的反馈。

然后我们基于Vite和TypeScript搭建了组件库的代码环境,使用husky实现了代码提交之前的commit信息和ESLint格式校验,确保只有合格的代码能够提交成功。

最后我们使用TypeScript+Sass开发了布局相关的组件,container负责容器,header、footer、aside和main负责渲染不同模块。

这节课我们学会了如何使用Sass来提高管理CSS代码的效率,以及如何使用TypeScript来开发组件,现在相信你对Vue 3中如何使用TypeScript已经有了更进一步的认识。

思考题

最后我们留个思考题:你负责的业务里有什么组件适合放在组件库里维护呢?

欢迎在评论区分享你的答案,也欢迎你把这一讲分享给你的同事和朋友们,我们下一讲再见!