概述
什么是代码规范?代码为什么要规范?本篇文章将从横向对比Element UI, Antd UI, Material UI 这几个常见的UI组件库的按钮组件
,从组件的应用、业务逻辑和代码风格等角度来分析这些大厂是怎么来实现这些组件的,同时结合个人的看法来说说前端的代码规范。
本篇有用的JS框架以vue
和react
为例。所提观点皆是个人看法。
关于UI库
也叫UI组件库,一套专门用于前端快速开发交互或图例图表等功能的组件集合,通常一个成熟的组件都会包含按钮
,输入框
,表格
, 提示
等常见的功能组件,且每个组件都是相互独立的。
世面常见的UI 库有Element UI
, Ant Design
, Bootstrap
, Material UI
, Taro UI (京东凹凸实验室)
,We UI(微信 UI库)
这里则用到的就Elm UI
, Antd
, M UI
, 之所以用这个组件为例,倒不是这三个最受欢迎,而是刚好这三个我都用过。
- Elm UI
饿了么开发维护,适用于Vue 的组件库, 还有支持react 和angular版本。
官网宣传标语: 一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库。
- Antd
即Ant Design, 由蚂蚁金服开发维护。 一套适用于react 的组件库,社区里也有支持Vue 和 angular的版本。
官网宣传标语:企业级产品设计体系,创造高效愉悦的工作体验
- M Ui
Material UI 是在使用Gatsby 开发网页的时候,开发文档有推荐使用M UI;
官网的宣传标语:安装 MUI —— 世界上最受欢迎的 React UI 框架
各个UI库的Button。
为什么只说按钮组件呢?组件很多,不可能一个一个讲。但是有个特点呀,几乎很多UI 库的README
或说明文档,都会用一个Button
的使用来介绍其UI的特点和用法。以小见大,这里从一个简单的按钮也就就能窥探全貌。
1、按钮的外观
先来看看各个组件的Button都长啥样,有怎么样的形状,配色。
颜色,类型,形状都较为丰富;
项目 | 值 | 说明 |
---|---|---|
形状 | 圆角方形,跑道型,圆形, 文字按钮 | |
颜色 | 主要(蓝色), 成功(绿色), 信息(灰色), 警告(橘色),危险(红色) | 通过修改属性即可使用需要的颜色 |
风格 | 实心,空心(朴素),图标,图标文字结合 | |
大小 | 中等(medium), small, mini | 没有large的大小 |
状态 | disable 禁用,loading等待 | |
事件 | click 点击事件 | |
交互 | hover, click 颜色改变 |
直观上,类型,颜色倒是没有Elm那么丰富
项目 | 值 | 说明 |
---|---|---|
形状 | 方形,跑道型,圆形,长条全宽,文字按钮 | |
颜色 | 主要, 默认, 危险 | 通过属性来显示颜色支持偏少 |
风格 | 实心,空心,虚线边框,图标,图标文字结合 | |
大小 | large, middle, small | |
状态 | loading, disabled | |
事件 | onClick 点击事件 | |
交互 | hover, click 按钮颜色变化 |
吐槽下Mui 的文档,阅读起来就没有elm ui 和 antd ui 那么好读,因为不能以一张图直观看出这个ui有的button有哪些风格,所以多截了几张图
项目 | 值 | 说明 |
---|---|---|
形状 | 圆角方形,文字按钮 | 说明文档中,没看到跑道形和圆形的按钮示例 |
颜色 | 主要(主题色),成功(绿色), 错误(红色) | |
风格 | 实心, 空心, 图标,图标文字结合 | |
大小 | large, medium, small | |
状态 | disabled, loading | loading的风格有仅显示loading文字,或者loading 图标 + 文字 |
时间 | onClick | |
交互 | hover, click 颜色变化 | click 还有效果有一种向外的波纹 |
外观总结
不难看出,各个按钮,大同小异,该有的都有;elm的颜色比较全,可以直接通过属性,来确实要显示的按钮效果。M UI的交互教花哨。而Antd 的就显得素了很多。
2、按钮的使用
一个组件写的好不好,得看对一个开发者来说,好不好用,这里以一个禁用、带有icon图标 的主要搜索按钮为例
- Elm UI
// vue 2.x
// elm ui 2.15.x
<el-button
type="primary"
plain
icon="el-icon-search"
disabled
@click="handleClick"
>
搜索
</el-button>
属性参数说明(这里仅列出部分的属性)
属性 | 说明 | 值 |
---|---|---|
type | 按钮类型 | primary,success, danger, text... |
plain | 是否为朴素按钮, 朴素按钮就是上面那个空心,用透明色填充的按钮 | true / false |
circle | 是否圆形按钮 | Boolean, true / false |
icon | 显示图标 | 字符串,图标名称 |
disabled | 是否禁用状态 | Boolean,true / false |
click | 点击事件 | function |
- Antd UI
// react
// Antd UI 4.x
<Button>
type="primary"
shape="circle"
icon={<SearchOutlined />}
disabled
:onClick={handleClick}
>
搜索
</Button>
属性参数说明(这里仅列出部分的属性)
属性 | 说明 | 值 |
---|---|---|
type | 按钮类型 | primary,link, text... |
shape | 按钮形状 | circle(圆形), round(跑道) |
icon | 显示的按钮组件 | ReactNode 按钮组件 |
disabled | 禁用状态 | Boolean, true / false |
onClick | 点击事件 | (event) => void |
- M UI
// react
// M UI 5.8.x
<Button
variant="contained"
color="primary"
startIcon={<SearchIcon />}
disabled
:onCkick={handleClick}
>
搜索
</Button>
属性参数说明(这里仅列出部分的属性)
属性 | 说明 | 值 |
---|---|---|
variant | 变种 | contained(实心),outlined(空心), text (文字) |
color | 颜色 | primary(主题色), success(成功色), error(错误色) |
startIcon | 图标,在文字之前 | node 图标组件 |
endIcon | 图标,在文字之后 | node 图标组件 |
size | 按钮大小 | small, medium, large |
disabled | 禁用状态 | Boolean, true / false |
onClick | 点击事件 |
使用总结
在使用上,也是大差不差的,仅在个别参数的使用个表示有所不同,像size
, disabled
, click
三个按钮都有,且表示的意思也一致,其他的都会有个类似的对应;
Elm 的type
更多的是表示使用场景, 除了表示颜色是success 还是error外,还会表示这个按钮是不是text按钮;而Antd 的则是表示的比较直观,如空心按钮,文本按钮或者链接按钮,至于表示场景颜色的就没有了。M UI在表示类型和颜色则是分开的,在属性变种varinat
表示,与antd的type类似。颜色则是单独的一个属性color
来表示与elm的type差不多类似。
在形状上,elm的则是用round
, circle
来表示,因为就两三种可能,这么用到不会觉得浪费属性;而antd是用属性shape
来表示;至于M UI, 属于没有类似的属性。
3、按钮样式
这里从渲染出的dom去看这是三个UI的button的实际渲染的样式与代码,更对的是以分析class命名为主。 这里为了方便阅读,我整理成了scss格式的代码;
-
Elm UI
// scss
// 部分代码
.el-button {
...
// 具体样式省略
&--primary { ... }
&--success {...}
&--danger { ... } // 危险色
&.is-plain { ... } // 方形按钮
&.is-disabled {...} // 禁用状态
&.is-loading { ... } // loading状态
}
从以上可以看出,Elm UI
的按钮,用的是bem风格, 其特点就是状态样式用两条横线--
连接。对应组件上,属性 type = "primary"
, 则对应的样式是&--primary
; 而其他的用boolean表示的数量,如disable
, plain
等,则是用.is-di、sabled
, is-plain
来对应。
-
Antd UI
// scss
.ant-btn {
...
&-primary { ... } // 主要样式
&-default { ... } // 默认样式
&-dashed { ... } // 虚线边框样式
&-circle { ... } // 圆形按钮
&-loading { ... } // loading 状态
&-dangerous { ... } // 危险状态
&[disabled] { ... } // 禁用状态
}
Antd UI
的样式,同样也是给样式.ant-btn
定义一些基本的样式,然后不同状态类型的样式单独定义, 用scss的链接符号也很好表示。但对于状态disable
用[disabled]
, loading
用&-loading
就显得其风格不是很同意。
-
M UI
.MuiButton {
...
&-root { ... }
&-contained { ... }
&-containedPrimary { ... }
&.Mui-disabled { ... } // 禁用状态
}
// loading 状态
.MuiLoadingButton {
&-loading { ... }
}
对于M UI
的样式,有点看不懂,上面列出来的样式名,猜测可能是这么实现的,但是最后渲染出来的又是两回事;其次是,样式命名也是挺另类的,驼峰命名和中划线混合;而loading 状态又是另起一个样式MuiLoadingButton-loading
,两个单词都体现出了loading
, 怎么看都显得多余。
样式总结
从上面的class命名不难看出,在各个UI在样式命名上,都有类似的规律:基本样式
+ 特性样式
,而特性样式
命名都是在基本样式
用--
或-
来连接单词。不管怎样的命名都能明显看得出其样式要表达的意思。
个人认为样式用的比较好的是Elm UI
的, 是bem
命名规范,从样式上,就可以看出当前按钮是真么类型,处于什么状态, 并且 类型和状态都做了区分, 比如,主要类型的用&--primary
, 状态则用.is-loaidng
。
<!-- 方形,处理loading状态的 主要按钮 -->
<button class="el-button el-button--primary is-loading is-plain">文本</button>
而Antd UI
在命名,虽然也有跟Elm UI
有些类似的命名方式,但对于如何区分类型,状态上,就没那么同意。比如loading状态的时候,是&-loading
跟类型是一样的命名方式, 但是disabled 时就直接用[disabled]
了,但对应到dom上,获取能勉强说得通吧。
<!-- 处于禁用状态的 主要按钮 -->
<button class="ant-button ant-button-primary" disabled></button>
对于M UI
那是没看出啥规律,样式命名了跟实际渲染都不是那么一回事,这个得空得去翻下源码,这里不好做分析。
4、组件源码
接下来,通过源码起看下,各个UI对于Button组件是怎么封装的。这里对部分代码省略,想了解更多,可以阅读github的源码。
- Elm UI Button
<!-- vue 2.x -->
<!-- 部分代码省略 -->
<template>
<button
class="el-button"
@click="handleClick"
:disabled="buttonDisabled || loading"
:class="[
type ? 'el-button--' + type : '',
buttonSize ? 'el-button--' + buttonSize : '',
{
'is-disabled': buttonDisabled,
'is-loading': loading,
...
}
]"
>
<i class="el-icon-loading" v-if="loading"></i>
<i :class="icon" v-if="icon && !loading"></i>
<span v-if="$slots.default"><slot></slot></span>
</button>
</template>
<script>
export default {
name: 'ElButton',
props: {
type: { ... },
disabled: { ... },
loading: { .... }
},
method: {
handleClick(evt) {
this.$emit('click', evt);
}
}
...
}
</script>
通过props传进来的值type
,来拼接成样式'el-button--' + type
, 对于loading和disable之类的,属于Boolean值,就可以用对象样式实现。这样很好的说明了,最好渲染的class name 是类似el-button el-button--primary is-loading
了;
对于click事件则是用emit
直接向外传递。
- Antd UI Button
// react
// 仅显示部分代码,略有修改
const prefix = 'ant-btn-';
render {
const { type, onClick, children, loading } = this.props
const classes = classNames({
'ant-btn': true,
[prefix + type]: type,
[prefix + shape]: shape,
[prefix + 'loading']: ('loading' in props && loading !== false),
...
})
return (
<button
className={classes}
onClick={onClick}
>
{children}
<button>
)
}
antd的组件思路跟elm的也很接近,通过props传进来的参数来拼接成样式,不同的点是不管传什么值,都是利用拼接的形式,特别是loading, 比elm的实现,显得不是那么的简洁。
事件的实现也是,封装button的click事件,获取props的传值并执行。
- M UI Button
关于这块代码,还是自行看源码好了。
M UI 对于button的实现,看了半天,没明白他们设计者的思路是什么;他们这个,是一个组件,把相关的style都写在这个文件里了,其中又引用了<ButtonBase>的组件;就这么个button组件,代码量就有四百多行,逻辑结构也略显复杂。至少在维护上,感觉都是很麻烦的事。放上源码地址,自己看咯。
代码总结
从源码上看,Elm UI
和 Antd UI
的思路差不多,用于表示按钮外观的属性,都能清楚的对应到样式上。除了react和vue上的区别外,区别也不大;因为对M UI 源码没神研究,这里就不做详细的评论。
关于事件传递,vue和react的实现略有不同,vue的是用
@click="(event) => {}"
来接收函数,组件中用$emit('click', event)
来执行,而react的则跟其他的传参一样,通过props
传进来,不好区分,只能在命名参数的时候加上on
前缀来表明这个参数是函数类型,(当然穿件来的还可能是ReactNode
)。
关于样式,我以为按照vue的风格,样式都会写在同一个文件的style里,这样事件很方便。但后来,在实际写业务的时候遇到一个问题,那就是多主题样式,如果写在同一个文件里的话,必然要有很多兼容,这会让代码变得很冗余且不好维护。所以,需要将样式代码剥离开来。react的也是,组件的样式代码文件也不会在同一个文件夹下。
Elm 和 Antd 就是这么实现的,他们把所有组件的样式,每个都独立一个文件来实现。这不仅方便开发和维护,还可以很方便的进行二次开发来自定义主题。
-
Elm UI
的样式则是一个独立的扩展包,可以通过第三方来安装,默认的则是theme-chalk,这个是集成在项目中的。 -
Antd UI
的样式也是一个独立的文件夹,文件命名也是能让人轻易找到并修改。路劲在ant-design/style,
这里声明下,关于
M UI
他们对组件的实现,我只是强调其不怎么接地气,并没有说人家做的不好;我对于其实现的思路没去深究,或许可能有更为深层次的想法是我所不知道的,毕竟这个UI是全球react 最受欢迎的组件库。
5、组件的扩展
一个组件好不好,除了使用简单,源码清楚外,还有一个关键就是,该组件在扩展上怎么样。这里以button group
(组合按钮)为例,刚好三个UI都有该组件
- Elm UI button-group
// vue 2.x
<template>
<div class="el-button-group">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'ElButtonGroup'
};
</script>
- Antd UI button-group
// react
// 仅显示部分代码
render() {
const {size, className, ...others} = this.props;
// large => lg
// small => sm
const sizeCls = ({
'large': 'lg',
'small': 'sm'
})[size] || '';
const classes = classNames({
'ant-btn-group': true,
[prefix + sizeCls]: sizeCls,
[className]: className
});
return <div {...others} className={classes} />;
}
- M UI button-group
他们的button-group 似乎有点复杂,这里略过,感兴趣的还是自己看源码啦。
扩展组件总结
在button-group的源码中,可以看出,Elm UI
和Antd UI
的设计思路是差不多的,都是可以在button 组件外层包一层父组件,以控制样式的方式来实现按钮组
这个组件。这种实现方式,代码量足够简单,且不会影响到已有的button组件。
至于M UI
, 其按钮组的使用,跟Elm UI
和 Ant UI
的用法大差不差的,都是父组件标签,里面包着子组件标签,但是其源码却差别很大。
总结
一个组件实现的好坏可以分为两点,使用
和开发
使用就是已经封装好的组件给开发者直接使用,通过简单的传参就是快速实现相关功能,不需要写过多的代码。像上面讲的三个组件,都有类似的使用方法,代码属性的参数在页面上的渲染,都能清晰的被表达出来。
开发这里指的是对该组件的维护,扩展或者优化。源码的业务表达和实现都有明确的风格能让后来者
更容易的阅读个适应。像Elm 和 Antd 的源码,适当了解vue或react的话,就能轻松的明白其业务逻辑。而这点,在M UI 上就不是很友好。
总结出来,好的组件有这些规律:
- 简单易上手的使用方式;
- 明确的样式命名和表达;
- 统一的命名:
比如Button组件,他们的组件名是button, 标签是<Button />
(elm 因为vue的风格原因是<el-button />
),样式文件也是button; - 清晰的业务代码
不管是Elm UI
,还是Antd UI
,其实总结就一句话,别人好用,自己好维护的,就是好组件。那么问道开头,什么是规范,以及为什么要做到规范。
什么是规范?
说了那么多,上面的各种都是在举例,其实都是在做这几件事:找规律
,找异同
,做总结
;所谓的规范,确切讲,前端的规范,并不是一开始很明确就有的,也不是一家独大定义的;他是在不断迭代和优化中,总结出来的一套经验;
规范,更多的是类似一种规律,一种规则,一种能让其他开发者快速参与开发的规则;
规范就是一种规则,这种规则最大的一个特点: 保持统一
我们写代码可以有自己的风格,可以像elm那样写,也可以像antd那样写;但不管怎样,前后的风格都要保持统一,比如样式命名,我可以像elm那么命名: el-button--primary
; 也可以像m ui那样: MuiButton-containedPrimary
,但两种命名风格同时出现,那就不统一了,就不合适了;当然其前提是合理,不合理的代码,风格在统一也是瞎费劲。
让大家很容易产生误解的是,前端的规范并不是统一的,这个项目的规范,拿到另外一个项目中,可能就不适合了像elm 和 antd 的规范就不是一样的;这里的说的规范,指的的是在同一个项目中,样式要怎么命名,组件要怎么命名,要放那个文件夹,都是统一要求的,这种规则通常都是由项目管理者统一制定和控制的,后来开发者则需要遵守这种规则。
为什么要规范?
代码首先是写给人看的,机器他只管运行,他不关心代码写的怎么样。所以,这个“规范”也是对人写代码的一种要求。以往经验,一个人软件的生命周期中,维护成本是占绝大部分,这个维护指的可能是功能上的扩展,或者后期BUG的修复性能优化等。如果代码写得大家都很累的的话,除了个维护就变得困难外,更多的还是要问候前开发者的祖宗了吧。
规范的唯一目的就是:降低维护成本