4

聊聊前端 UI 组件:组件设计

 3 years ago
source link: https://ourai.ws/posts/design-frontend-ui-components/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在本系列文章《 聊聊前端 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 CSSNormalize.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 组件的属性(prop)和事件(event)就是这个「函数」的参数。属性是 UI 组件的外部与其内部进行主动通信的数据,事件则是进行被动通信的回调函数。

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

在设计 UI 组件的属性时,先思考下要加的这个属性是不是属于这个 UI 组件本身的特性?若不是,那要加的属性的值所对应的 UI 组件的特性是什么?如果这两个问题都没有得到答案,那么这个属性可以不用加了。

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

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

Ant DesignElement 的做法是将其作为 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——

EjYB73Y.jpg!mobile 立旗

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


Recommend

  • 34
    • 掘金 juejin.im 5 years ago
    • Cache

    [译] 前端组件设计原则

    原文地址:Front end component design principles 原文作者:Andrew Dinihan 文中示例代码:传送门 限于个人能力,如有错漏之处,烦请不吝赐教。 前言 我在最近的工作中开始使用 Vue 进行开发,但是我在上一家

  • 14
    • 微信 mp.weixin.qq.com 4 years ago
    • Cache

    前端组件设计原则

    译者:@没有好名字了 译文:https://github.com/lightningminers/article/issues/36,https://juejin.im/post/5c49cff56fb9a049bd42a90f 作者:@Andrew Dinihan 原文:https://engineering.carsguide.com.au/...

  • 22

    本文是一个文章系列的第一篇,主要说明几个基本概念以及所要探讨的目标主体,目的是统一认知上的「上下文」以尽量避免因信息不对称而造成理解障碍。 这一系列文章是关于前端 UI 组件的,我想通过这个系列静下心来好好聊聊与之相关...

  • 23

    本文是文章系列「聊聊前端 UI 组件」的第二篇,内容与本系列的上篇文章《 聊聊前端 UI 组件:核心概念 》有所关联,如果还没看过,建议去看下。

  • 30

    本文是文章系列「聊聊前端 UI 组件」的第三篇。 在本系列的上篇文章《 聊聊前端 UI 组件:组件特征 》中,通过从关注点分离的角度进行前端 UI...

  • 5

    看到标题,一般会有两种反应: 「哇~好高大上啊!」 「嗯,这个话题真大……」 ——的确如此。 我不生搬硬套那个什么百科来说啥是「面向组件」和为啥这么做,而是从工作现状以及自己思考的角度来阐述,并...

  • 8

    聊聊中后台前端应用:业务中的组件体系 欧雷 发表于 1 天之前 0 条评论 标...

  • 8

    react函数组件写法十分灵活,数据传递也非常方便,但如果对react的理解不够深入,就会遇到很多问题,比如数据变了视图没变,父组件状态变了子组件状态没有及时更新等等,对于复杂的组件来说,可能产生的问题会更多,混乱的代码也更容易出现。随着自己踩...

  • 2

    JELLY | 从组件化角度聊聊设计工程化从组件化角度聊聊设计工程化上传日期:2022.10.13近几年围绕业务中台化的场景,涌现出了许多低代码平台。面对多组件、多页面、跨平台的复杂场景,如何保证整体的用户体验一致...

  • 6

    在上篇文章,我们已经设计了一个简单的设计态的Canvas,能够显示经过BuildEngine生成的ReactNode进行渲染。本文,我们将继续上一篇文章的成果,设计并实现一个能够显示组件节点大纲树的组件。 什么是组件大纲树? 我们希望用...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK