You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

363 lines
15 KiB
Markdown

2 years ago
# 20组件库如何设计你自己的通用组件库
你好我是大圣。上一讲TypeScript加餐学完你是不是想赶紧巩固一下TypeScript在Vue中的使用呢那么从今天开始我们就重点攻克Vue中组件库的实现难点我会用7讲的篇幅带你进入组件库的世界。
学习路径大致是这样的首先我会给你拆解一下Element3组件库的代码其次带你剖析组件库中一些经典的组件比如表单、表格、弹窗等组件的实现细节整体使用Vite+TypeScript+Sass的技术栈来实现。而业务中繁多的页面也是由一个个组件拼接而成的所以我们可以先学习一下不同类型的组件是如何去设计的借此举一反三。
## 环境搭建
下面我们直奔主题,开始搭建环境。这个章节的代码我已经推送到了[Github](https://github.com/shengxinjing/ailemente) 上由于组件库是模仿Element实现的所以我为其取名为ailemente。
接下来我们就一步步实现这个组件库吧。首先和开发项目一样我们要在命令行里使用下面的命令创建Vite项目模板选择vue-ts这样我们就拥有了一个Vite+TypeScript的开发环境。
```bash
npm init vite@latest
```
![图片](https://static001.geekbang.org/resource/image/3a/9f/3a7862fc756674a61c2ed85965208b9f.png?wh=1106x444)
关于ESLint和Sass的相关配置全家桶实战篇我们已经详细配置了这里只补充一下husky的内容。husky这个库可以很方便地帮助我们设置Git的钩子函数可以允许我们在代码提交之前进行代码质量的监测。
下面的代码中我们首先安装和初始化了husky然后我们使用 npx husky add命令新增了commit-msg钩子husky会在我们执行git commit提交代码的时候执行 node scripts/verifyCommit命令来校验commit信息格式。
```bash
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的信息不符合要求会直接报错并且终止代码的提交。
```typescript
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的[代码提交记录](https://github.com/vuejs/vue-next/commits/master)每个提交涉及的模块类型和信息都清晰可见能够很好地帮助我们管理版本日志校验正则的逻辑。如下图feat代表新功能docs代表文档perf代表性能。下面的提交日志就能告诉我们这次提交的是组件相关的新功能代码中新增了Button.vue。
```typescript
feat(component): add Button.vue
```
## ![图片](https://static001.geekbang.org/resource/image/04/f4/04f18eaf3be849e31cd3629a91d4e3f4.png?wh=1920x1020)
commit-msg是代码执行提交的时候执行的我们还可以使用代码执行之前的钩子pre-commit去执行ESLint代码格式。这样我们在执行git commit的同时就会首先进行ESLint校验然后执行commit的log信息格式检查全部通过后代码才能提交至Git这也是现在业界通用的解决方案学完你就快去优化一下手里的项目吧
```typescript
npx husky add .husky/pre-commit "npm run lint"
```
## 布局组件
好,现在环境我们就搭建好了,接着看看怎么布局组件。
我们可以参考[Element3组件列表页面](https://e3.shengxinjing.cn/#/component/installation),这里的组件分成了基础组件、表单组件、数据组件、通知组件、导航组件和其他组件几个类型,这些类型基本覆盖了组件库的适用场景,项目中的业务组件也是由这些类型组件拼接而来的。
我们还可以参考项目模块的规范搭建组件库的模板包括Sass、ESLint等组件库会在这些规范之上加入单元测试来进一步确保代码的可维护性。
**接下来我们逐一讲解下各个组件的负责范围。**
首先我们需要设计基础的组件也就是整个项目中都会用到的组件规范包括布局、色彩字体、图标等等。这些组件基本没有JavaScript的参与实现起来也很简单负责的就是项目整体的布局和色彩设计。
而表单组件则负责用户的输入数据管理,包括我们常见的输入框、滑块、评分等等,总结来说,**需要用户输入的地方就是表单组件的应用场景**,其中对用户的输入校验是比较重要的功能点。
数据组件负责显示后台的数据,最重要的就是表格和树形组件。
通知组件负责通知用户操作的状态,包括警告和弹窗,如何用函数动态渲染组件是警告组件的重要功能点。
接下来我们就动手设计一个基础的布局组件,这个组件相对是比较简单的。你可以访问[Element3布局容器页面](https://e3.shengxinjing.cn/#/component/container)这里一共有container、header、footer、aside、main五个组件这个组合可以很方便地实现常见的页面布局。
* el-container组件负责外层容器当子元素中包含 <el-header> 或 <el-footer> 时,全部子元素会垂直上下排列,否则会水平左右排列。
* el-header、el-aside、el-main、el-footer 组件分别负责顶部和侧边栏页面主体和底部容器组件。这个功能比较简单只是渲染页面的布局。我们可以在src/components目录下新建文件夹container新建Container.vue布局组件没有交互逻辑只需要通过flex布局就可以实现。
这几个组件只是提供了不同的class这里就涉及到CSS的设计内容。在Element3中所有的样式前缀都是el开头每次都重复书写维护太困难所以我们设计之初就需要涉及Sass的Mixin来提高书写CSS的代码效率。
接着我们在src/styles下面新建mixin.scss。在下面的代码中我们定义了namespace变量为el使用Mixin注册一个可以重复使用的模块b可以通过传进来的block生成新的变量$B并且变量会渲染在class上并且注册了when可以新增class选择器实现多个class的样式。
```scss
// 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注册的代码块。
```xml
<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的布局方向。
```scss
.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"TypeScriptTypescript使interface使defineProps()使computedcontainerdirectionel-headerel-footer
```xml
<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中注册方便多个组件之间的变量共享。
```xml
<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组件基本一致你可以在[这次提交中](https://github.com/shengxinjing/ailemente/commit/8902877cf3e7361da0c9aa78d6b6f4b0b344d431)看到组件的变更。
组件注册完毕之后我们在src/App.vue中使用import语法导入后就可以直接使用了。但是这里有一个小问题我们的组件库最后会有很多组件对外暴露用户每次都import的话确实太辛苦了所以我们还需要使用插件机制对外暴露安装的接口我们在container目录下新建index.ts。在下面的代码中我们对外暴露了一个对象对象的install方法中我们使用app.component注册这五个组件。
```typescript
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方法](https://github.com/hug-sun/element3/blob/master/packages/element3/packages/container/index.js),可以很方便地根据项目的需求按需加载。
```typescript
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的代码中我们使用组件嵌套的方式就可以实现下面的页面布局。
```xml
<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>
```
## ![图片](https://static001.geekbang.org/resource/image/50/8d/50fc754a942ae92f0362e1e9d62fb28d.png?wh=1920x1527)
## 总结
今天的课程到这里就结束了,我们来总结一下今天学到的内容吧。
首先我们介绍了组件库中的组件分类,基础组件、表单组件、数据组件和通知组件,这些组件类型组合拼接,就可以开发出功能繁多的项目。其中,基础组件负责整体布局和色彩的显示,表单组件负责用户的输入,数据组件负责数据的显示,通知组件用于数据的反馈。
然后我们基于Vite和TypeScript搭建了组件库的代码环境使用husky实现了代码提交之前的commit信息和ESLint格式校验确保只有合格的代码能够提交成功。
最后我们使用TypeScript+Sass开发了布局相关的组件container负责容器header、footer、aside和main负责渲染不同模块。
这节课我们学会了如何使用Sass来提高管理CSS代码的效率以及如何使用TypeScript来开发组件现在相信你对Vue 3中如何使用TypeScript已经有了更进一步的认识。
## 思考题
最后我们留个思考题:你负责的业务里有什么组件适合放在组件库里维护呢?
欢迎在评论区分享你的答案,也欢迎你把这一讲分享给你的同事和朋友们,我们下一讲再见!