从一个Button来谈谈组件规范

概述

什么是代码规范?代码为什么要规范?本篇文章将从横向对比Element UIAntd UI, Material UI 这几个常见的UI组件库的按钮组件,从组件的应用、业务逻辑和代码风格等角度来分析这些大厂是怎么来实现这些组件的,同时结合个人的看法来说说前端的代码规范。

本篇有用的JS框架以vuereact为例。所提观点皆是个人看法。

关于UI库

也叫UI组件库,一套专门用于前端快速开发交互或图例图表等功能的组件集合,通常一个成熟的组件都会包含按钮输入框表格提示等常见的功能组件,且每个组件都是相互独立的。

世面常见的UI 库有Element UI, Ant Design, Bootstrap, Material UI, Taro UI (京东凹凸实验室),We UI(微信 UI库) 这里则用到的就Elm UIAntdM 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 颜色改变
Antd UI

直观上,类型,颜色倒是没有Elm那么丰富

项目 说明
形状 方形,跑道型,圆形,长条全宽,文字按钮
颜色 主要, 默认, 危险 通过属性来显示颜色支持偏少
风格 实心,空心,虚线边框,图标,图标文字结合
大小 large, middle, small
状态 loading, disabled
事件 onClick 点击事件
交互 hover, click 按钮颜色变化
  • M UI Button
    基本的按钮

    按钮的颜色

    各种类型

吐槽下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, disabledclick 三个按钮都有,且表示的意思也一致,其他的都会有个类似的对应;

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


    Elm 按钮与class
// scss
// 部分代码
.el-button {
  ...
  // 具体样式省略
  &--primary { ... }
  &--success {...}
  &--danger { ... } // 危险色
  &.is-plain { ... } // 方形按钮
  &.is-disabled {...} // 禁用状态
  &.is-loading { ... } // loading状态
}

从以上可以看出,Elm UI的按钮,用的是bem风格, 其特点就是状态样式用两条横线--连接。对应组件上,属性 type = "primary", 则对应的样式是&--primary; 而其他的用boolean表示的数量,如disableplain等,则是用.is-di、sabled, is-plain来对应。

  • Antd UI


    Antd按钮与class
// scss
.ant-btn {
  ...
  &-primary { ... }  // 主要样式
  &-default { ... } // 默认样式
  &-dashed { ... } // 虚线边框样式
  &-circle { ... } // 圆形按钮
  &-loading { ... } // loading 状态
  &-dangerous { ... } // 危险状态
  &[disabled] { ... } // 禁用状态
}

Antd UI的样式,同样也是给样式.ant-btn定义一些基本的样式,然后不同状态类型的样式单独定义, 用scss的链接符号也很好表示。但对于状态disable[disabled], loading&-loading就显得其风格不是很同意。

  • M UI


    MUI 和 class
 .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的源码。

<!-- 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直接向外传递。

// 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 UIAntd 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都有该组件

// vue 2.x
<template>
  <div class="el-button-group">
    <slot></slot>
  </div>
</template>
<script>
  export default {
    name: 'ElButtonGroup'
  };
</script>

// 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}  />;
}

他们的button-group 似乎有点复杂,这里略过,感兴趣的还是自己看源码啦。

扩展组件总结

在button-group的源码中,可以看出,Elm UIAntd UI的设计思路是差不多的,都是可以在button 组件外层包一层父组件,以控制样式的方式来实现按钮组这个组件。这种实现方式,代码量足够简单,且不会影响到已有的button组件。

至于M UI , 其按钮组的使用,跟Elm UIAnt 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的修复性能优化等。如果代码写得大家都很累的的话,除了个维护就变得困难外,更多的还是要问候前开发者的祖宗了吧。

规范的唯一目的就是:降低维护成本

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271