关于前端:聊聊前端-UI-组件组件设计

31次阅读

共计 6611 个字符,预计需要花费 17 分钟才能阅读完成。

本文首发于欧雷流。因为我会时不时对文章进行补充、修改和润色,为了保障所看到的是最新版本,请浏览原文。

在本系列文章《聊聊前端 UI 组件:组件体系》中初步阐明了 UI 组件的架构设计,本文将在此基础上进一步开展说说那篇文章中一笔带过的局部,并论述在设计一个 UI 组件时应该留神的点有哪些。

目录构造

在《聊聊前端 UI 组件:组件体系》中列出的目录构造的根底上做了些许调整——

component
   ├── demo                       # 示例相干文件
   │   └── ...
   ├── test                       # 测试相干文件
   │   └── ...
   ├── style                      # 款式相干文件
   │   ├── _functions.scss        # Sass 函数(可选)│   ├── _properties.scss       # CSS 自定义属性(必须),格调组件的一部分,供内部运行时自定义主题格调
   │   ├── _variables.scss        # Sass 变量(必须),格调组件的一部分,供内部编辑时 / 编译时自定义主题格调
   │   ├── _mixins.scss           # Sass 混入(可选)│   └── _rules.scss            # CSS 规定(必须),视觉组件,具备束缚构造的作用
   ├── typing                     # 类型相干文件
   │   ├── custom-properties.ts   # CSS 自定义属性配置项(必须),用于运行时生成 CSS 自定义属性
   │   ├── aliases.ts             # 类型别名(可选)│   ├── interfaces.ts          # 结构组件接口(必须)│   └── index.ts               # 类型对立导出
   ├── HeadlessComponent.ts       # 无头组件,UI 组件与构造无关的逻辑
   ├── Component.vue              # 结构组件,受生成 HTML 的 JS 库 / 框架的源码、平台限定的视图构造描述语言影响
   ├── index.ts                   # 模块对立导出
   ├── changelog.md               # 组件变更记录
   ├── readme.md                  # 组件阐明文档
   ├── metadata.yml
   └── package.json

命名约定

HTML & CSS class

在基于组件开发(Component-based Development),即大家所说的「组件化」,在 web 前端畛域遍及之前,风行过一种神奇的 class 命名形式,能够说是一种方法论了——原子类(atomic classes)。

预计一入行就是 React、Vue 横行的前端,压根儿就没听过更没见过「原子类」是个什么货色——

<style>
.w-100 {width: 100px;}
.w-150 {width: 150px;}
.h-100 {height: 100px;}
.h-150 {height: 150px;}

.m-10 {margin: 10px;}
.m-20 {margin: 20px;}
.mt-10 {margin-top: 10px;}
.ml-15 {margin-left: 15px;}

.bgc-red {background-color: red;}
.bgc-greed {background-color: green;}

.c-fff {color: #fff;}
.c-000 {color: #000;}

.f-l {float: left;}
.f-r {float: right;}
</style>

<div class="w-150 h-150 f-l mt-10 ml-15 bgc-red c-000">
  <div class="w-100 h-100 f-r m-20 bgc-green c-fff">Atomic classes</div>
</div>

看到了吧,这种方法论强调的就是尽可能将 CSS 的每个属性和值的组合拆成 class,命名形式也根本是「属性名 + 属性值」的模式,并且属性名和属性值是否进行「简写」以及两头有没有 -_ 等分隔符就看编写的人的素养和情绪了。

原子类的「长处」是,它把 class 拆分到足够细,很好很「原子」;原子化带来的特点就是可组合性很强,这样任何页面都能够通过原子类的有机组合去实现,只有想不到,没有做不到!哪天设计师说要把按钮间隔右边的 15 像素改为 10 像素——没问题!把 <button>.ml-15 换成 .ml-10 就好!小菜一碟!

为什么下面说的「长处」是加了引号的?我就想晓得,原子类除了写的时候字符数可能会略微少些,跟写内联款式(inline style)有什么区别?有更语义化吗?可读性有变更好吗?人脑累赘有升高吗?中、大型项目保护起来更不便吗?

随着基于组件开发在 web 前端畛域的遍及,原子类的身影逐步隐没;但最近因为某个 CSS 框架人气走高的起因,原子类再度死灰复燃……

那么,原子类或者说款式原子化是错的吗?不是,都是时臣的错!啊,不!都是 utility-first 思维的错!

class 应该是语义化的,尤其是在基于组件开发时,让在视图构造中一眼看到 class 后,就晓得它是个什么货色,而不是它长什么样。

另外,基于组件开发的特点之一就是封装,对外屏蔽外部细节;而 utility-first 思维恰好是裸露细节,这与基于组件开发的理念「三观不合」。

在基于组件开发的体系下,class 理当是 component-first,即利用 CSS 组件(CSS component),那些 utility class 作为辅助存在。也就是说,当 CSS 组件自带款式与理论需要有些许不符时,利用 utility class 进行「微调」,而不是在内部重写 CSS 组件的款式——这也是一种组合形式。

比方,按钮 CSS 组件自身是不会在程度方向撑满容器的,但设计师想让它占满一行——

<style>
.Button {
  display: inline-block;
  text-align: center;
}

.u-block {display: block !important;}
</style>

<div>
  <button class="Button u-block">CSS component</button>
</div>

CSS 组件在本系列文章所论述的 UI 组件体系中,叫做「视觉组件」,class 的命名遵循 BEM 的变体——SUIT CSS 命名约定。

SUIT CSS 是 Normalize.css 的作者 Nicolas Gallagher 于 2013 年左右时创建,尽管当初曾经处于根本不保护的状态了,但它基于组件开发的思维仍施展着余热。

SUIT CSS 命名约定我从 2014 年用到当初,并且会持续用上来。本系列文章 CSS 相干的示例代码中 class 的命名皆遵循此命名约定。在基于组件开发的体系下,强烈建议 class 命名遵循 SUIT CSS 命名约定——

/* 组件 */
.ComponentName {}

/* 组件修饰符 */
.ComponentName--modifierName {}

/* 组件后辈 */
.ComponentName-descendentName {}

/* 组件状态 */
.ComponentName.is-stateOfComponent {}

/* 辅助工具 */
.u-utilityName {}

组件基类 .ComponentName 及其后辈 .ComponentName-descendentName 很好了解,它们人造具备层级关系,独特形容了一个 UI 组件的构造——

<!-- 用语义化 HTML 标签 -->
<article class="Article">
  <header class="Article-header">
    <h1 class="Article-title"> 文章题目 </h1>
  </header>
  <section class="Article-section">
    <h2> 章节题目 </h2>
    <p> 章节段落 </p>
  </section>
  <footer class="Article-footer"> 一些其余信息 </footer>
</article>

<!-- 用非语义化 HTML 标签,更能凸显出 class 命名语义化的作用 -->
<div class="Article">
  <div class="Article-header">
    <h1 class="Article-title"> 文章题目 </h1>
  </div>
  <div class="Article-section">
    <h2> 章节题目 </h2>
    <p> 章节段落 </p>
  </div>
  <div class="Article-footer"> 一些其余信息 </div>
</div>

而组件修饰符 .ComponentName--modifierName 和组件状态 .ComponentName.is-stateOfComponent 有时就不能很好地区分何时该用哪个了。就拿按钮 CSS 组件来说,它的色彩、是否可用与尺寸,哪个该用修饰符?哪个算是状态?

我给出一个比较简单的判断规范:如果是 UI 组件的个性,即不会因为什么条件而扭转的,用修饰符;假使会因某个条件满足与否而变动,那就是状态——

<!-- 用语义化 HTML 标签,大号(尺寸)的次要(性能色)操作按钮 -->
<button class="Button Button--primary Button--large"> 新增 </button>

<!-- 用非语义化 HTML 标签,不可用(状态)的危险(性能色)操作按钮 -->
<span class="Button Button--danger is-disabled"> 批量删除 </span>

应该留神的是,组件修饰符和组件状态都是间接加在 UI 组件的根节点上的,也就是要跟在组件基类的前面,不能用于组件后辈上。如果一个组件后辈须要程序化地扭转它自身的款式,要用辅助工具类而不是状态类。当一个组件后辈的构造、性能等变得复杂时,要将其封装成一个新的组件。

Sass 变量与 CSS 自定义属性

在本系列文章所论述的 UI 组件体系中,Sass 变量和 CSS 自定义属性合称为「格调组件」,它们负责主题格调的定制,是与设计体系(Design System)的结合点。其中,Sass 变量是在编辑时 / 编译时,CSS 自定义属性则是在运行时。

在这里,Sass 变量与 CSS 自定义属性的命名形式比拟相似,它们大略都是 <namespace>-<component-name>[-descendent-name|-modifier-name][-state]-(variable-name|property-name) 的模式。

因为我在基于本系列文章所论述的思维做一套叫做「Petals」的半成品 UI 组件,因而之后的示例代码中波及到的 <namespace> 局部根本都会用 petals

Sass 变量是以 $__petals$petals 结尾,与组件名之间用 -- 连贯,前者是外部应用(公有)的,下层开发者无需关怀,后者是供内部在编辑时 / 编译时定制用;CSS 自定义属性则用 --petals 结尾,以 - 与组件名相连——

/* 理论模式:<namespace>-<component-name>-(variable-name|property-name) */
$__petals--button-font-size: --petals-button-font-size;
$__petals--button-line-height: --petals-button-line-height;

/* 理论模式:<namespace>-<component-name>-<modifier-name>-<state>-(variable-name|property-name) */
$petals--button-primary-focus-color: var($__petals--primary-active-color, $petals--primary-active-color) !default;
$petals--button-primary-focus-bg: var($__petals--primary-active-bg, $petals--primary-active-bg) !default;

上文所说的 CSS 组件,即视觉组件,它是将款式进行封装,对外屏蔽细节;而格调组件相同,通过将视觉组件所用到的 CSS 属性值动态化的形式达到款式可定制化的目标,这就变得像 utility-first 的原子类一样裸露了款式细节。

但与 utility-first 的 CSS 框架不同的是,格调组件只给进行主题格调定制的人带来了心智累赘,对其余的下层开发者并无影响。

业务无关

本系列文章次要探讨的对象是业务无关的 UI 组件,在单说「UI 组件」或「组件」时也是指这个;而业务相干的 UI 组件,在本系列文章所论述的 UI 组件体系中叫做「部件」。

依据 UI 组件的通用性,可分为「通用组件」和「专用组件」。「通用组件」是可能满足大部分惯例场景的 UI 组件,它们的汇合通常会作为「组件库」整体打包公布为一个软件包;「专用组件」是为了解决某些非凡场景需要而存在的,像数据网格、各种编辑器等,这类个别都是独自发包。

——欧雷《聊聊前端 UI 组件:组件特色》

下面提到的「通用组件」和「专用组件」都是业务无关的 UI 组件。

UI 组件是什么?能够认为它是一个返回视图构造的函数,而 UI 组件的属性(prop)和事件(event)就是这个「函数」的参数。属性是 UI 组件的内部与其外部进行被动通信的数据,事件则是进行被动通信的回调函数。

一个封装得好的函数,它的参数应尽可能少,要想明确每个参数的语义,且必须的确有其存在的意义——UI 组件的属性和事件的设计也该如此。

在设计 UI 组件的属性时,先思考下要加的这个属性是不是属于这个 UI 组件自身的个性?若不是,那要加的属性的值所对应的 UI 组件的个性是什么?如果这两个问题都没有失去答案,那么这个属性能够不必加了。

UI 组件的属性只应与其自身的个性无关,与业务意义无关——本身个性是天然个性,业务意义是附加个性。

比方,一个按钮组件通常会有「次要」、「主要」和「危险」这几种多少与业务沾边的语义,那么组件的属性该如何设计来满足这种需要呢?

Ant Design 和 Element 的做法是将其作为 type 属性的值或独立成一个属性——

<Button type="primary">Ant Design 中的次要按钮 </Button>
<Button>Ant Design 中的主要(默认)按钮 </Button>
<Button danger>Ant Design 中的危险按钮 </Button>

<el-button type="primary">Element 中的次要按钮 </el-button>
<el-button>Element 中的主要(默认)按钮 </el-button>
<el-button type="danger">Element 中的危险按钮 </el-button>

依照下面说的 UI 组件属性设计准则来看,「次要」、「主要」和「危险」作用到按钮组件上的体现次要是色彩产生了变动,所以应该去用示意按钮的天然个性「色彩」的 color 属性来满足同样的需要——

<button color="primary"> 次要按钮 </button>
<button> 主要(默认)按钮 </button>
<button color="danger"> 危险按钮 </button>

<!-- 还能够扩大出其余任意多色彩的按钮 -->
<button color="f00"> 红色按钮 </button>
<button color="yellow"> 黄色按钮 </button>
<button color="blue"> 蓝色按钮 </button>

若 UI 组件的某组个性是二元对抗的,如「禁用」与「启用」,则抉择默认不失效的那个作为属性,且属性值是布尔型,默认值为 false

还是拿按钮组件来举例:如果默认是「禁用」,那就设计一个代表「启用」的 enabled 属性,其默认值是 false,只有组件在被应用时传入了 enabled,就变成了「启用」状态;反之亦然。

另外,UI 组件的属性值尽可能是简略数据类型,也就是数字、字符串等。

业务相干

业务相干的 UI 组件,即上文所说的「部件」,因其关注点与业务无关的 UI 组件不同,所以在设计时所恪守的准则和思考的事件也不尽相同,甚至会天壤之别。一般来说,会用到上下文与依赖注入等技术。

因为业务相干的 UI 组件不是本系列文章次要探讨的对象,在此就不开展说了。

总结

前几天在朋友圈立了个 flag——

本文就是该 flag 的「引子」。


欢送关注微信公众号【Coding as Hobby】(微信中搜「coding-as-hobby」)以及时浏览最新的技术文章~ ;-)

正文完
 0