Vue3组件库设计核心要素
专栏前几篇文章让我们重新认识了Vue3基础知识,从这篇文章开始,我们将以 Vue3 组件库的开发为线索,梳理设计一个企业级组件库需要考虑哪些核心要素,并实现常见组件。
在我们动手实现组件库之前,我们需要了解一下组件库设计有哪些常见的设计要素,我把它列举了下来:
- monorepo 管理组件代码
- 设计规范
- 响应式的栅格系统
monorepo 管理组件代码
首先我们来了解下什么是monorepo?
简单来说monorepo是一种项目架构,一个仓库管理多个子项目(模块,包),前端项目诸如vue3、element-plus都是采用这种架构模式,一个简单的monorepo的项目结构类似如下:
├── packages
| ├── examples
| | ├── package.json
| ├── components
| | ├── package.json
| ├── utils
| | ├── package.json
├── package.json
├── pnpm-workspace.yaml
monorepo 管理仓库多项目的方案有很多种,例如传统的 Lerna 技术方案、不过最近比较流行 pnpm 管理方案,这也是vue3和element-plus采用的方案,那我们就基于上述项目结构,采用pnpm的方式来实现一个案例:
实现步骤具体如下:
- 初始化项目和配置文件
- 配置 monorepo 项目代码
- 安装所有子项目依赖
初始化项目和配置文件
目录初始化就是对根项目和子项目目录下的 package.json 依赖初始化,首先初始化项目根目录package.json 文件,执行npm init -y
即可,接着我们对两个子项目的 package.json 文件进行初始化。
初始化packages/utils/package.json 文件,如下所示:
{
"name": "@element-plus-demo/utils",
"version": "1.0.1",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"lodash": "^4.17.21"
}
}
上述代码中,我们给utils子项目name属性加上了@element-plus-demo这个私有前缀名称。这个是可以自定义的,一般是自己所在企业 npm 源站使用的前缀名称进行定义,方便统一命名子项目,后续的其他子项目也可以加上同样的 “@xxx/”的前缀进行统一命名。
注意,这里使用 “@xxx/”私有命名前缀不是必要的操作,只是为了方便管理子项目 npm 模块名称,一般都要加上这类命名前缀。
此外,项目中我们添加了lodash依赖也只是为了更全面的演示各个子项目之前的依赖关系,也不是必须的。
然后我们初始化packages/components/package.json 文件,如下所示:
{
"name": "@element-plus-demo/components",
"version": "1.0.1",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@element-plus-demo/utils": "1.0.1",
"vue": "3.3.4"
}
}
上述代码中,我们将子项目name属性改为@element-plus-demo/components,并添加上项目依赖的@element-plus-demo/utils和vue。
接着在根目录pnpm-workspace.yaml 这个文件里,进行 monorepo 的项目配置,具体代码如下所示:
packages:
- 'packages/*'
声明 packages/* 目录是用来管理所有子项目的。详细配置可以参考pnpm官方文档。
配置 monorepo 项目代码
前面我们完成了配置文件的配置,不过我们想要检查配置文件是否正确,最好的方式就是通过代码进行校验。
首先我们添加上packages/utils/index.js 文件,如下所示:
import _ from "lodash";
//返回大写开头的字符串。
export const capitalize = (str) => {
return _.capitalize(str);
};
这里我们借用lodash
库,封装了capitalize
函数对外暴露。
接着我们完善packages/components目录,封装一个button.vue组件并引用子目录@element-plus-demo/utils下的capitalize
函数,代码如下:
<template>
<button class="button">{{ btn }}</button>
</template>
<script setup>
import { capitalize } from "@element-plus-demo/utils";
import { ref } from "vue";
const btn = ref(capitalize("FABCDEFG22"));
</script>
<style scoped>
.button {
color: #fff;
background-color: #409eff;
border-color: #409eff;
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
text-align: center;
box-sizing: border-box;
outline: none;
margin: 0;
font-weight: 500;
padding: 12px 20px;
font-size: 14px;
border-radius: 4px;
}
</style>
注意这里我们还需要创建packages/components/index.js 文件,引用上面封装的button.vue文件对外暴露,具体请查看完整的项目代码。
接着我们在packages目录下创建一个examples文件夹,基于vite搭建一个简单的vue脚手架工具引用前面封装的button.vue组件,项目packages/examples/package.json 文件,如下所示:
{
"name": "@element-plus-demo/examples",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus-demo/components": "1.0.1",
"vue": "^3.3.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"vite": "^4.3.9"
}
}
注意这里需要我们引用@element-plus-demo/components子项目,代码如下:
<script setup>
import { Button } from "@element-plus-demo/components";
</script>
<template>
<main>
<Button></Button>
</main>
</template>
安装所有子项目依赖
最后我们安装所有子项目依赖。这里我们要先保证本地电脑有全局的 pnpm 命令,可以通过以下脚本进行安装:
npm i -g pnpm
最后再在项目的根目录下执行:
pnpm i
到这里就可以实现 monorepo 项目的依赖安装和管理了。
我们启动examples项目后,会发现它可以调用我们前面封装的@element-plus-demo/components和@element-plus-demo/utils等模块的代码, 可以看到最终效果如图所示:
设计规范
在前端开发中,颜色设计规范和CSS开发规范都是非常重要的一部分。在开发具体的组件库之前,了解它们是很有必要的。
颜色设计规范
每个组件库一般都会有一个主题色,比如说Element plus就是蓝色系的,主题色的选择是根据业务需要定制的,优秀的组件库甚至支持自定义主题。所以,我们主题方案设计的第一步就是需要设计好组件库的颜色规范。
颜色规范设计通常是UI设计师的工作,不是前端开发者的职责工作。但是前端需要根据设计稿,用代码来实现原型,甚至于说参与设计稿的讨论,所以我们有必要简单的了解这些颜色规范设计的过程。
第一步是颜色种类的选择。一般设计师会选择几种大类型的颜色,例如红色、蓝色和绿色等。然后根据业务需要挑选这几类型中的一个基准色号。
第二步是基于上一步选择好的基准色号,进行颜色梯度的处理。举个例子,蓝色的颜色梯度处理如下所示:
第三步是辅助色的选择,比如Element Plus支持success,info,warning,danger等多种辅助主题风格,如图所示:
目前前端业界也有很多优秀的开源组件库都提供了颜色设计规范。例如Element Plus 官方团队的颜色规范,可以直接参考。
CSS开发规范
css开发规范主要指的是我们在开发组件样式的时候,要围绕css的两个特性附加和重写来设计,不同的样式可以叠加在一起使用;同名的样式,后面的会覆盖前面的,从而达到组合应用的效果。
接下来我们总结出常见的类型样式,然后根据每个组件的特性,来组装这些类型的特性,从而达到丰富多彩的配置效果。下面以Element Plus为例,来看看这些类型样式。
1、基础样式
任何一个CSS组件在刚开始都要先定义基本样式,一般都是定义该样式下的字体(font-),内外边距(padding,margin),显示方式(display),边框(border)等其它相关内容。
比如按钮组件基本样式:
.el-button {
display: inline-flex;
justify-content: center;
align-items: center;
line-height: 1;
height: 32px;
white-space: nowrap;
cursor: pointer;
color: var(--el-button-text-color);
text-align: center;
box-sizing: border-box;
outline: none;
transition: .1s;
font-weight: var(--el-button-font-weight);
user-select: none;
vertical-align: middle;
-webkit-appearance: none;
background-color: var(--el-button-bg-color);
border: var(--el-border);
border-color: var(--el-button-border-color);
padding: 8px 15px;
font-size: var(--el-font-size-base);
border-radius: var(--el-border-radius-base);
}
2、颜色样式
以按钮为例,Element Plus官方提供了5种颜色的样式,分别是primary(重点蓝),success(成功绿),info(信息灰),warning(警告橙),danger(危险红)。定义规则是:命名空间-组件名称-颜色类型,如el-button--primary,el-button--success。
在定义不同颜色样式的时候,主要是定义文本颜色,边框颜色,背景颜色等等,具体定义什么颜色和该组件的特性有关。比如按钮在定义颜色样式时的代码如下:
这里由于Element Plus代码用了var变量,为了让大家看到颜色变化,用图片进行的展示。需要注意的是对于一些可点击元素,比如按钮,还要进行特殊处理,在hover、focus、状态的颜色需要保持同样的风格。
3、尺寸样式
很多组件诸如按钮都提供了尺寸的快捷设置。提供了large,Default,small等尺寸。
调整尺寸主要是调整所对应元素的padding、圆角设置、行间距和字体大小。如图所示:
需要注意的是同一个组件的不同类型的样式可以组合在一起使用,不同颜色样式和尺寸样式一起使用完全没有问题,不会引起冲突。如下例所示:
<el-button type="success" size="large">Large</el-button>
4、状态样式
有一些可点击元素,比如说按钮需要根据状态显示其效果,高亮的时候用active样式,禁用的时候用disabled样式。
这类样式一般是处理元素的阴影,鼠标形状、透明度等方面的内容。如图所示:
5、并列元素样式
在很多情况下可能有多个相同元素并列在一起,又或者一个组件内有多个子元素,这种情况下为了避免元素挤在一起,会增加一点间距。
这里主要考虑两种情况,其一是水平并列的时候,其二是垂直的时候,一般都是通过增加内边距或者外边距来处理,如下所示:
.el-button+.el-button {
margin-left: 12px;
}
6、其它特殊样式
企业级的组件可能还会有一些特殊场景,诸如动画效果,多个不同的组件嵌套在一起的效果等等,这些需要具体情况具体分析,相对比较复杂。
总而言之,思路就是利用CSS的重载覆盖的概念,也因此需要注意样式定义的顺序,以免出现重载顺序错误。
栅格系统
栅格系统听着有点高大上,那什么是栅格系统?
其实就是一种页面布局方式,将一个平面划分成行(Row)和列(Col)来管理,每一行分成一个个等分的格子,通常是24份,开发人员可以自由按份组合,以便开发出简洁方便的程序。
那栅格系统有什么好处呢?
答案是栅格系统是响应式设计的核心,具体使用时可参考Element Plus布局。
那如何实现栅格系统呢?
主要工作原理如下:
- 一行数据必须包含在元素中,以便为其赋予合适的对齐方式和内外边距。
- 使用行Row在在水平方向创建一组列Col。
- 具体内容应当放置于列内,而且只有列可以作为行的直接子元素。
- 行Row提供 gutter 属性来指定列之间的间距,其默认值为0。通过设置padding从而创建列之间的间距,然后通过为第一列和最后一列设置负值的margin从而抵消padding的影响。
- 栅格系统中国的列是通过制定1到24的值来表示跨度的范围。例如,要让屏幕分为3个等宽的部分,可以使用3个:span="8"来创建。
首先,我们需要实现一个栅格的行组件,具体代码如下:
<template>
<div class="el-row">
<slot></slot>
</div>
</template>
<style scoped lang="scss">
.el-row {
display: flex;
flex-wrap: wrap;
position: relative;
box-sizing: border-box;
}
</style>
这里我们以div元素作为了容器,选择flex方式布局,接下来就是实现 Row 组件里的列组件 Col,具体代码如下:
<template>
<div :class="{ [baseClassName]: true, [spanClassName]: true }">
<slot></slot>
</div>
</template>
<script setup>
const props = defineProps(["span"]);
function getSpan(propSpan) {
if (typeof propSpan === "number") {
const span = Math.ceil(Number(propSpan));
if (span >= 1 && span <= 24) {
return span;
}
}
return 1;
}
const baseClassName = `baseClassName`; //基础样式
const spanClassName = `col-${getSpan(props.span)}`; //
</script>
<style scoped lang="scss">
$total-columns: 24; //列数
$column-width: 100% / $total-columns; //每列的宽度
.baseClassName {
box-sizing: border-box;
}
@for $i from 1 through $total-columns {
.col-#{$i} {
width: ($column-width * $i);
flex: 0 0 percentage((1 / $total-columns * $i));
}
}
</style>
上述代码把一行分成了24等份,每一份宽度为100% / $total-columns,通过span属性接收来自父容器指定的宽度。这里样式代码我们使用了sass框架,如果使用css代码将会变成如下格式:
.col-1{
4.1666666667%;
}
.col-1{
width: 8.3333333333%;
}
...
.col-23{
95.8333333333%;
}
正如你所看到的上面css代码,所以开源框架都使用了less或者sass框架。最后我们引用一下我们设计的栅格系统,代码如下:
<script setup>
import { Button, Row, Col } from "@element-plus-demo/components";
</script>
<template>
<div class="container">
<Button style="margin-bottom: 24px"></Button>
<Row>
<Col class="gird" :span="24">col-24</Col>
</Row>
<Row>
<Col class="gird" :span="12">col-12</Col>
<Col class="gird-dark" :span="12">col-12</Col>
</Row>
<Row>
<Col class="gird" :span="8">col-8</Col>
<Col class="gird-dark" :span="8">col-8</Col>
<Col class="gird" :span="8">col-8</Col>
</Row>
<Row>
<Col class="gird" :span="6">col-6</Col>
<Col class="gird-dark" :span="6">col-6</Col>
<Col class="gird" :span="6">col-6</Col>
<Col class="gird-dark" :span="6">col-6</Col>
</Row>
</div>
</template>
<style scoped lang="scss">
.container {
width: 960px;
margin: 0 auto;
}
.gird {
background: #8cbfe6;
border: 1px #8cbfe6 solid;
margin-bottom: 10px;
height: 50px;
color: #ffffff;
font-size: 18px;
text-align: center;
line-height: 50px;
}
.gird-dark {
background: #068aed;
border: 1px #068aed solid;
margin-bottom: 10px;
height: 50px;
color: #ffffff;
font-size: 18px;
text-align: center;
line-height: 50px;
}
</style>
最终的效果如图所示:
完善的栅格系统还需要支持分栏间隔,响应式布局等核心功能,如果大家想知道如何实现,欢迎留言,后续补上。
总结
本篇文章正式进入专栏的组件篇,在正式开发具体组件之前,简单了解了开发一个Vue3组件库需要考虑哪些要素:
1、我们认识并且搭建了monorepo项目架构,后续组件将基于该架构进行开发。
2、我们认识了开发一个组件库是需要UI支持的,需要与他们沟通颜色规范并且了解了CSS开发设计思想。
3、我们知道了响应式设计的核心:栅格系统。
最后专栏的下一篇我们将正式的进入组件开发:如何设计一个表单组件。
转载自:https://juejin.cn/post/7245176801492140093