前语
组件库已成为,前端每日最长打交道的东西。当前不管公司规模巨细,他必定都会运用一套组件库来建立自己的项目。
-
小公司,他或许会挑选一套满意自我功能需求的第三方组件库。也有人挑选一套老练的第三方组件库,再针对个别组件进行二次封装,如常见的表格,表单;
-
再大一点的公司,或许会挑选一套伟人肩膀上二次封装的内部组件库。表面上现已是自己公司的组件库,其实底层仍是内藏一个优质的第三方。
-
而专业的互联网公司,会有一个专门的团队,来建立自己的内部组件库。满意自己一切的内需的一起,开源出来让整个社区进行认可。
那么,为什么一般只有”专业的互联网公司”,才会挑选自己建立内部的组件库呢?中小公司,建立的难点在哪里?笔者今日剖析一下,最简略的Button,需求考虑什么。
背景
本文运用运用 vue3 + vite + typescript,以移动端组件库为目标。
笔者将使用额外的时刻,共享整个组件库的建立。本文先共享一个最简略的Button。
下边是笔者文章对应开源项目的截图:
Button按钮事例:
需求剖析
此刻假如你不是一个技能(或许非前端人员),脑海中一个原生button控件,加个点击事情处理。
然后事情或许没有这么简略。咱们看看一个老练的第三方组件库,button究竟做了什么?
1)老练组件库参阅
咱们先看看其他相对老练的组件库,处理了什么。
Ant Design供给的API
Element供给的API
Vant供给的API
Varlet供给的API
2)承认需求目标
此刻,看到一个老练组件库的供给的Api, 咱们能够考虑一下咱们终究需求完成的一个Button, 笔者结合第三方组件库为参阅,以及日常工作所需,确定本章想要的Button:
-
- 支撑文本,图标等
-
- 支撑不同的巨细,小号,中号,大号等。
-
- 支撑不同的类型,主题按钮,危险按钮,警告按钮等
-
- 支撑加载作用
-
- 支撑通明的形式
-
- 支撑圆角
-
- 支撑禁用
-
- 支撑块级展现
-
- 支撑触发中状态
-
- 展现上的优化
-
- 点击事情,防重,支撑异步等
3)全体图例
4)相关技能与规范
由上述的需求承认,咱们从而剖析部分咱们运用咱们需求的技能与规范。
本文主要描绘button的构建,规范等后续会有专文汇总,敬请关注。
这儿简略提及button几个涉及的技能与规范:Scss, PostCss兼容处理, Bem规范,CSS原子类等
5)声明
本文已引导Button的建立为准,事例的原意是帮助咱们更好的理解。详细源码还需看github。
完成剖析
依据上边的需求,咱们先手写一个最简略的Button, 再依据需求逐步推进:
1)手写最简略的Button
<template>
<button class="cb-button"><slot /></button>
</template>
<script lang="ts">
export default {
name: "Button",
setup() {
return {
}
}
}
</script>
<style>
...省掉
</style>
- 作用图
2)支撑图标
上述现已支撑插槽,理论上现已满意图标的显现。
但这样很简略造成图标的巨细不一致,位置不一致等问题。假如咱们由组件去操控常用的按钮内置图标的款式,这样会使项目愈加的规范。
<template>
<div class="cb-button">
<img v-if="icon" :class="cb-button__img" :src="icon" />
<slot />
</div>
</template>
<script lang="ts">
export default {
name: "Button",
props:{
//嵌入图标
icon: {
type: String,
default: ""
},
},
setup() {
return {
}
}
}
</script>
- 作用图
3)支撑不同的字号
在项目的建立进程,展现上按钮有巨细之分现已是十分常见的需求。常见的项目一般有5号字体,一般定义为: smaller, small, normal, large, larger.
笔者这儿用small, normal, large为事例:
<template>
<div :class="['cb-button', [`cb-button__size-${size}`]]">
<img v-if="icon" :class="cb-button__img" :src="icon" />
<slot />
</div>
</template>
<script lang="ts">
import { PropType } from "vue"
export type SizeItem = "small" | "normal" | "large"
export default {
name: "Button",
props:{
...,
// 字体巨细
size: {
type: String as PropType<SizeItem>,
default: "normal",
validator: (str: string) => {
return ["small", "normal", "large"].includes(str)
}
},
},
setup() {
return {
}
}
}
</script>
<style lang="scss">
$size-array: (
key: "small",
fontSize: 12,
minWidth: 30,
height: 20,
borderRadius: 10,
padding: "0 4px"
),
(
key: "normal",
fontSize: 14,
minWidth: 70,
height: 30,
borderRadius: 15,
padding: "0 6px"
),
(
key: "large",
fontSize: 16,
minWidth: 80,
height: 40,
borderRadius: 20,
padding: "0 8px"
);
@include b(button) {
@for $i from 1 through length($size-array) {
$item: nth($size-array, $i);
@include e("size") {
@include m(map-get($item, key)) {
font-size: #{map-get($item, fontSize)}px;
min-width: #{map-get($item, minWidth)}px;
height: #{map-get($item, height)}px;
line-height: #{map-get($item, height)}px;
border-radius: #{map-get($item, borderRadius)}px;
padding: #{map-get($item, padding)};
flex: 0 0 auto;
}
}
}
}
</style>
- 作用图
4)支撑不同的主题
同理,按钮也需求支撑不同的主题: “primary” , “info” , “danger” , “warning ” , “link”
<template>
<div :class="['cb-button', [`cb-button__type-${type}`]]">
<img v-if="icon" :class="cb-button__img" :src="icon" />
<slot />
</div>
</template>
<script lang="ts">
import { PropType } from "vue"
export type ThemeType = "primary" | "info" | "danger" | "warning " | "link"
export default {
name: "Button",
props:{
...,
// 类型
type: {
type: String as PropType<ThemeType>,
default: "primary",
validator: (str: string) => {
return ["primary", "info", "danger", "warning", "link"].includes(str)
}
},
},
setup() {
return {
}
}
}
</script>
<style lang="scss">
...后续款式省掉,详细看下方源码。
</style>
5)支撑加载作用
点击加载作用。这儿经过异步使命Promise去操控是否加载状态愈加合理,不过有些场景确实需求页面用loading去操控,笔者这儿先暴露为api供页面操控:
<template>
<div :class="['cb-button', ...]">
<span v-if="loading" :class="`${be}loading`" />
<img v-else-if="icon" :class="`${be}img`" :src="icon" />
<slot />
</div>
</template>
<script lang="ts">
...
export default {
name: "Button",
props:{
...,
// 是否加载状态
loading: {
type: Boolean,
default: false
},
},
}
</script>
<style lang="scss">
...后续款式省掉,详细看下方源码。
</style>
- 作用图
6)支撑通明形式
通明形式,也是现在的UI常用的规范,跟主题常规形式混搭,能够达到更抱负的展现作用。
源码跟”主题形式”差不多,这儿不再重复。需求看github、
7)支撑圆角设置
圆角假如大局一致,应该经过config文件进行操控。但是假如个别圆角特殊,咱们也需求支撑自定义:
<template>
<div
:class="[...]"
:style="[baseStyles]"
>
<span v-if="loading" :class="`${be}loading`" />
<img v-else-if="icon" :class="`${be}img`" :src="icon" />
<slot></slot>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, toRefs } from "vue"
import { useCommon } from "../common/hooks/index"
import { props } from "./props"
// 按钮
export default defineComponent({
// 按钮组件,用于事情触发
name: "Button",
props,
setup(props, { emit }) {
const baseStyles = ref(props.radius !== -1 ? { borderRadius: props.radius + "px" } : {})
return {
...,
baseStyles,
}
}
})
</script>
8)支撑禁用
禁用形式,源码跟”主题形式”差不多,这儿不再重复。需求看github。
9)支撑块级展现
块级形式。同理,实践中,咱们还需求占满整个快。源码跟”loading”差不多,需求看github。
10)支撑点击(click)事情
假如咱们不处理click,页面也会触发。假设不声明 emits: [“click”], 页面还会触发两次。
那么是否要包装click呢?
答案是必定的。否则loading,disabled, 防重复点击等,怎么处理?
笔者这儿先出个初版, 且处理防重,详细看github源码:
<template>
<div
...
@click="onClick"
>
...
<slot></slot>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, toRefs } from "vue"
import { useCommon } from "../common/hooks/index"
import { props } from "./props"
// 按钮
export default defineComponent({
// 按钮组件,用于事情触发
name: "Button",
props:{
...,
onClick: {
type: Function as PropType<(e: Event) => void | Promise<any>>,
},
},
setup(props, { emit }) {
const pending: Ref<boolean> = ref(false)
const onClick = e => {
const { loading, disabled, onClick } = props
if (!onClick || loading || disabled || pending.value) {
return
}
props.onClick(e)
pending.value = false;
}
return {
...,
baseStyles,
}
}
})
</script>
11)支撑触发(touchstart)事情
touchstart事情仍是有一定场景的。这儿简略遍及一下跟click的区别:
-
touchstart: 手指触碰开始就能触发
-
click: 1.手指触碰
2.手指未在屏幕上移动
3.在这个dom上手指脱离屏幕
4.触摸和脱离屏幕之间的时刻距离较短
详细代码完成可参阅click。
12)事情是否支撑冒泡
常用的事情,都不支撑冒泡。但是有一些特殊的需求,如区域埋点等,他需求冒泡。咱们也把该事情暴露
porps:{
...,
//是否支撑冒泡
isStopPropagation: {
type: Boolean,
default: true
},
}
const onClick = e => {
const { loading, disabled, onClick } = props
if (!onClick || loading || disabled || pending.value) {
return
}
props.onClick(e)
pending.value = false;
if (props.isStopPropagation) {
e.stopPropagation()
}
}
13)支撑异步loading
常见等待后台执行异步使命,或许等待前端处理,需求一定的等待时刻方可二次触发。此刻咱们用autoLoading一个特点支撑:
porps:{
...,
// 自动loading形式
autoLoading: {
type: Boolean,
default: false
},
}
// setup:
...
const loading = ref(props.loading)
const onClick = async e => {
if (loading.value) {
return
}
if (props.autoLoading) {
loading.value = true
await props.onClick(e)
loading.value = false
// 最长防重复点击
setTimeout(() => {
if (loading.value) {
loading.value = false
}
}, 5000)
} else {
props.onClick(e)
}
if (props.isStopPropagation) {
e.stopPropagation()
}
}
14)参数操控
一个开放的组件库,毫无疑问款式上,色彩上,永远无法满意一切的业务场景。此刻就需求经过装备一些参数,来操控按钮的展现。
如:主题色怎么操控等。这儿后续再供给专文解说。
15)其他款式作用完成
此刻,你会发现按钮体会上还缺了一点什么?
是的。例如hover作用,active作用等,鼠标聚集事情等还未处理。还要进行额外的细节优化。这儿不再描绘。有兴趣看源码
源码链接
github: github.com/zhuangweizh…
gitee: 后续留意置顶评论。
预览:zhuangweizhan.github.io/cb-ui/dist/…
结语
看到这儿,手写button。还觉得是一个十分简略事情么?
假如文章对你有帮忙,欢迎继续关注,下一篇:怎么写好一个Input组件。