关于前端:聊聊前端-UI-组件组件体系

52次阅读

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

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

本文是文章系列「聊聊前端 UI 组件」的第三篇。

在本系列的上篇文章《聊聊前端 UI 组件:组件特色》中,通过从关注点拆散的角度进行前端 UI 组件的形成剖析,并以较为形象的视角对 UI 组件分门别类,以及形容了让组件间能够体现复用的继承关系,从而建设出前端 UI 组件的特色模型。

本文将以上篇文章中所得出的特色模型为根底,探讨下如何设计并建设一个前端 UI 组件体系。

在做组件体系设计的时候,最重要的一点就是——要真真正正地想着把 UI 组件弄成可复用的,就像制造业生产时所用的物料一样——结构可替换的 UI 组件。

因为 UI 组件形成元素的易变性对组件体系的设计有着很大的影响,为了不便查看,将上篇文章中的易变性及其影响因素的表格搬过去:

形成 易变性 影响因素
构造 视觉构造 不易变 内容构造、布局类款式
内容构造 较易变 生成 HTML 的 JS 库 / 框架的源码、平台限定的视图构造描述语言
体现 主题格调 很易变 GUI 设计人员的审美和想法、非布局类款式、图标与图片
行为 交互逻辑 不易变 交互设计人员的想法
业务逻辑 很易变 业务规定

组件架构

表格中列出的 UI 组件形成元素都能够作为独自的组件存在。如果把 UI 组件看作是「最终产品」的话,那么 UI 组件形成元素所对应的那些组件就是「两头产品」。

在软件工程中,「组件(component)」个别是指软件的可复用块,好比制造业所应用的「构件」。这是一个比拟宽泛的概念,它能够是软件包,能够是 web 服务,也能够是模块等。

但在前端眼里,「组件」通常是指页面上的视图单元,即「UI 组件」。能够说,「UI 组件」是「组件」的子集。

——欧雷《聊聊前端 UI 组件:外围概念》

鉴于上述起因,这里须要特地阐明下:上文所说的「作为独自的组件存在」中的「组件」是指「软件的可复用块」,而不是「UI 组件」。

格调组件

在上篇文章中提到了「虚构组件」的概念——

在持续往下之前,先引入一个「虚构组件」的概念。正如它的名字所示,是一个虚构的,理论不存在的,只是概念上的组件。它是几个主题格调属性的汇合。

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

与之类似,「格调组件」也是一些主题格调属性的汇合,大略包含:

如果要用代码来体现的话,能够借助 CSS 预处理器中的变量。这里用 Sass 来举例:

// 主题色
$sc--primary: #cce5ff !default;
$sc--secondary: #e2e3e5 !default;
$sc--info: #d1ecf1 !default;
$sc--success: #d4edda !default;
$sc--warning: #fff3cd !default;
$sc--danger: #f8d7da !default;

// 文本色
$sc--text-primary: #303133 !default;
$sc--text-secondary: #696c71 !default;
$sc--text-heading: #2c405a !default;
$sc--text-regular: #333 !default;
$sc--text-placeholder: #c0c4cc !default;

// 字体尺寸
$sc--font-size: 14px !default;
$sc--font-size-lg: 16px !default;
$sc--font-size-sm: 12px !default;

// 字体粗细
$sc--font-weight-light: 300 !default;
$sc--font-weight-normal: 400 !default;
$sc--font-weight-bold: 700 !default;

// 边框粗细
$sc--border-width: 1px !default;

// 边框色彩
$sc--border-color: #dcdfe6 !default;

// 边框圆角
$sc--border-radius: 4px !default;
$sc--border-radius-lg: 6px !default;
$sc--border-radius-sm: 2px !default;

格调组件与体现复用的继承密切相关——

输入框组件、下拉列表组件等都属于表单控件(form control),它们都继承自「表单控件」这个虚构组件,如果各自没有指定色彩、字体、边框等主题格调属性的话,将会依照虚构组件中所设定的来显示。相似地,下拉列表组件、下拉菜单组件等都有弹出层(pop-up),它们都继承了「弹出层」这个虚构组件。

想必你曾经发现了,下拉列表组件同时继承了「表单控件」和「弹出层」这两个虚构组件,这就是下面提到的「多重继承」。

那些所谓的「虚构组件」,它们也遵循着同样的继承规定——如果本身没有指定特定的主题格调属性,则会依照父级所设定的显示。那么,虚构组件的「父级」是啥呢?——是根底格调。

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

下面示例中所定义的 Sass 变量就是「根底格调」。

以「表单控件」为例,一个继承了根底格调的虚构组件用代码示意为:

$sc--form-control-font-size: $sc--font-size !default;
$sc--form-control-font-size-lg: $sc--font-size-lg !default;
$sc--form-control-font-size-sm: $sc--font-size-sm !default;

$sc--form-control-height: 36px !default;
$sc--form-control-height-lg: 40px !default;
$sc--form-control-height-sm: 32px !default;

$sc--form-control-color: $sc--text-regular !default;
$sc--form-control-placeholder-color: $sc--text-placeholder !default;
$sc--form-control-bg: #fff !default;
$sc--form-control-box-shadow: none !default;

$sc--form-control-border-width: $sc--border-width !default;
$sc--form-control-border-color: $sc--border-color !default;
$sc--form-control-border-radius: $sc--border-radius !default;
$sc--form-control-border-radius-lg: $sc--border-radius-lg !default;
$sc--form-control-border-radius-sm: $sc--border-radius-sm !default;

相应地,输入框组件的格调组件局部大抵为:

$sc--input-font-size: $sc--form-control-font-size !default;
$sc--input-font-size-lg: $sc--form-control-font-size-lg !default;
$sc--input-font-size-sm: $sc--form-control-font-size-sm !default;

$sc--input-height: $sc--form-control-height !default;
$sc--input-height-lg: $sc--form-control-height-lg !default;
$sc--input-height-sm: $sc--form-control-height-sm !default;

$sc--input-color: $sc--form-control-color !default;
$sc--input-placeholder-color: $sc--form-control-placeholder-color !default;
$sc--input-bg: $sc--form-control-bg !default;
$sc--input-box-shadow: $sc--form-control-box-shadow !default;

$sc--input-border-width: $sc--form-control-border-width !default;
$sc--input-border-color: $sc--form-control-border-color !default;
$sc--input-border-radius: $sc--form-control-border-radius !default;
$sc--input-border-radius-lg: $sc--form-control-border-radius-lg !default;
$sc--input-border-radius-sm: $sc--form-control-border-radius-sm !default;

视觉组件

尽管 UI 组件的最终出现须要内容构造作为骨架去撑持,但若仅仅是为了勾画出 UI 组件视觉构造的轮廓,只用 CSS 就能够了。一系列模块化、可复用、可组合的 CSS 规定形成了「视觉组件」,也可叫做「CSS 组件」。

在视觉组件中,得用 BEM 之类的命名法为 CSS 类选择器命名。举荐应用由 BEM 衍生进去的这种:

/* 组件 */
.ComponentName {}

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

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

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

一个残缺的视觉组件中曾经蕴含了格调组件。拿按钮组件来举例的话,它的视觉组件大体是这样:

$sc--button-font-size: $sc--form-control-font-size !default;
$sc--button-font-size-lg: $sc--form-control-font-size-lg !default;
$sc--button-font-size-sm: $sc--form-control-font-size-sm !default;

$sc--button-padding-y: 10px !default;
$sc--button-padding-y-lg: 12px !default;
$sc--button-padding-y-sm: 9px !default;

$sc--button-padding-x: 20px !default;
$sc--button-padding-x-lg: 20px !default;
$sc--button-padding-x-sm: 15px !default;

$sc--button-color: $sc--form-control-color !default;
$sc--button-bg: $sc--form-control-bg !default;
$sc--button-box-shadow: $sc--form-control-box-shadow !default;

$sc--button-border-width: $sc--form-control-border-width !default;
$sc--button-border-color: $sc--form-control-border-color !default;
$sc--button-border-radius: $sc--form-control-border-radius !default;
$sc--button-border-radius-lg: $sc--form-control-border-radius-lg !default;
$sc--button-border-radius-sm: $sc--form-control-border-radius-sm !default;

$sc--button-disabled-color: $sc--text-placeholder !default;
$sc--button-disabled-bg: #eee !default;

/* ----- 以上为格调组件局部 ----- */

.Button {
  padding: $sc--button-padding-y $sc--button-padding-x;
  font-size: $sc--button-font-size;
  color: $sc--button-color;
  background-color: $sc--button-bg;
  border: $sc--button-border-width solid $sc--button-border-color;
  border-radius: $sc--button-border-radius;
  box-shadow: $sc--button-box-shadow;

  &-icon,
  &-text {
    display: inline-block;
    vertical-align: middle;
  }

  &-icon + &-text {margin-left: 5px;}

  // 大按钮
  &--large {
    padding: $sc--button-padding-y-lg $sc--button-padding-x-lg;
    font-size: $sc--button-font-size-lg;
    border-radius: $sc--button-border-radius-lg;
  }

  // 小按钮
  &--small {
    padding: $sc--button-padding-y-sm $sc--button-padding-x-sm;
    font-size: $sc--button-font-size-sm;
    border-radius: $sc--button-border-radius-sm;
  }

  // 按钮生效 / 禁用状态
  &.is-disabled {
    color: $sc--button-disabled-color;
    background-color: $sc--button-disabled-bg;
  }
}

从下面的代码示例中能够看出,按钮组件中蕴含了「图标」和「文本」这两个横向排列且垂直居中的「后辈」,并有「惯例」、「大」和「小」三种「规格」,以及有「失常」和「生效 / 禁用」两种「状态」——通过 CSS 描绘出了 UI 组件视觉上的根本构造与个性。

无头组件

「无头」这个词译自「headless」,在计算机领域中代表硬件或软件在应用或运行时不须要依赖 GUI 相干的设施或库。在这里,「无头组件」是指 UI 组件的交互逻辑,以及与之相交融的业务逻辑。

无头组件的职责是负责监听并接管事件零碎的告诉,提供解决 UI 组件本身状态、数据转换逻辑的函数或办法,它不应该关注和解决除了交互逻辑之外的事件。

在无头组件中,所监听并接管的并非是运行环境提供的实在事件,而是自定义的「代理事件」,它是实在事件的占位符。这么做的次要起因是,同一个行为尽管可能是由不同的实在事件触发的,但对 UI 组件而言其语义是雷同的——通过代理事件来表白对 UI 组件有意义的实在语义。

就拿下拉菜单组件来说,它的弹出层的显示能够通过其所蕴含的按钮的 clickmouseover 这两个实在事件来触发,但对 UI 组件的实在语义是「弹出」而非「点击」或「悬停」,因此应用代理事件 pop-up 来代替。

上文说到无头组件是「UI 组件的交互逻辑及与之相交融的业务逻辑」,又说「不应该关注和解决除了交互逻辑之外的事件」,这两点乍看之下互相矛盾,然而并不——

业务逻辑对于一个网站、利用来说是十分必要且重要的,但对 UI 组件来说,它就没那么必要了,更谈不上重要。在前端的 GUI 层面,业务逻辑理当是交互逻辑的延长。

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

在 UI 组件中,业务逻辑与事件是非亲非故的,不仅仅是 UI 事件,如点击按钮后收回 HTTP 申请;还有数据事件,如业务数据变动后更新显示的文本。因而,业务逻辑是交互逻辑的延长。这就须要无头组件在解决交互逻辑时提供扩大点,比方「事件映射」,以使业务逻辑作为无头组件的扩大存在,而不是集成进去。

无头组件会依据代理事件去调用事件处理函数。在未指定的状况下,代理事件会默认指向一个实在事件,事件处理函数会执行一段默认的解决逻辑;事件映射的作用就是更改代理事件所指向的实在事件以及事件处理函数的逻辑。

无头组件的接口定义大略长这样:

// 代理事件
type EventBroker = string;
// 实在事件
type EventName = string;
// 事件处理函数
type EventHandler = (params: any) => void;
// 事件对象
type EventObject = {name: EventName; handler: EventHandler};
// 事件映射
type EventMap = {[key: string]: EventName | EventHandler | EventObject };

interface IHeadlessComponent {
  // 事件映射
  setEventMap(map: EventMap): void;
  // 获取实在事件
  getEventName(broker: EventBroker): EventName;
  // 获取事件处理函数
  getEventHandler(broker: EventBroker): EventHandler;
}

结构组件

顾名思义,「结构组件」是用来生成 UI 组件内容构造的,但它的作用不仅如此,还会负责对接视觉组件与无头组件。

如果单纯从最终的 HTML 构造上来看,它也算是不易变的,但在古代前端开发中,HTML 的构造根本是动静生成的,并且强依赖于 React、Vue 这类没有统一标准的 JS 库 / 框架。另外,还存在着像 WXML、AXML 这类平台限定的视图构造描述语言。因为写法不统一,这就使页面内容构造变得不那么稳固。

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

如上所述,UI 组件的内容构造依赖于视图构造形容语法,视图构造形容语法又取决于平台或运行环境,这就导致了结构组件无奈像格调组件、视觉组件和无头组件一样将变动隔离在内部,因此结构组件是形成 UI 组件的几个组件中易变性最强的,最容易被替换的。

用 Vue 2.x 版本的类组件写法来举例,下拉菜单组件的结构组件大抵为:

<template>
  <div :class="$style.Dropdown">
    <button :class="$style['Dropdown-trigger']" @[popUpEventName]="handlePopUp"> 显示弹出层 </button>
    <div :class="[$style['Dropdown-popup'], {[$style['is-shown']]: isPopUpShown }]"> 我是弹出层 </div>
  </div>
</template>

<!-- 视觉组件 -->
<style lang="scss" src="./style.scss" module></style>

<script lang="ts">
import {Vue, Component, Prop} from 'vue-property-decorator';

// 无头组件
import DropdownHeadlessComponent from './headless';

@Component
export default class DropdownComponent extends Vue {
  // 事件映射配置外置
  @Prop({type: Object, default: () => ({}) })
  private readonly eventMap!: {[key: string]: any };

  private popUpEventName: string = '';

  private isPopUpShown: boolean = false;

  private handlePopUp(): void {this.isPopUpShown = true;}

  // 初始化无头组件实例及与其相干的
  private initHeadless(): void {const hc = new DropdownHeadlessComponent();

    hc.setEventMap(this.eventMap);

    this.popUpEventName = hc.getEventName('pop-up');
  }

  public created(): void {this.initHeadless();
  }
}
</script>

假使要反对多技术栈、多平台,以后风行的次要有两种策略:在各技术栈、各平台下别离实现结构组件;利用 Taro、uni-app 这类工具进行转译。

可定制性

上文论述的组件架构将一个本来很容易实现得泥沙俱下的 UI 组件依据关注点拆分成了格调组件、视觉组件、无头组件和结构组件,这种架构会使各局部的可复用性失去很大的晋升。除了易变性较强的结构组件之外,其余组件在达到肯定成熟度之后就根本不会变动。如果须要更换技术栈或新反对一个平台,只需实现一遍结构组件即可,其余组件能够拿来就用。

不单是可复用性有所改善,可定制性也有所增强。依据定制代码 / 配置与程序联合时所处的程序生命周期阶段,将可定制性整顿为下表:

可定制点 编辑时 / 编译时 运行时
主题格调
视觉构造
触发事件
业务逻辑
内容构造

如果格调组件的代码是像示例代码中写的那样,是不反对运行时定制的,得略微革新一下:

// 未经革新
$sc--font-size: 14px !default;

// 利用 CSS 自定义属性革新后
$sc--font-size: var(--sc-font-size, 14px) !default;

组件标准

每个 UI 组件都该当被视作是独立的软件包、模块,所以它的各方面应该是齐备的——除了实现 UI 组件的代码,还应有详尽的应用阐明文档、可交互的在线 demo、欠缺的测试代码以及用来做一些自动化解决的元数据等。

还是以下拉菜单组件为例,上述资料相干文件的目录构造大体如下:

dropdown
   ├── demo
   │   └── ...
   ├── test
   │   └── ...
   ├── changelog.md
   ├── headless.ts
   ├── index.ts
   ├── metadata.yml
   ├── package.json
   ├── readme.md
   ├── structure.vue
   └── style.scss

代码编写方面能够参考我总结并整顿的代码格调指南:https://ntks.ourai.ws/guides/coding-style/。

另外,在结构组件中对接视觉组件时,要用 CSS Modules,以防止内部的款式代码所引起的非预期成果。

总结

本文基于本系列的上篇文章中得出的特色模型提出了一个以「结构可替换的 UI 组件」为指标的组件架构,次要由格调组件、视觉组件、无头组件和结构组件所形成,除了结构组件之外的组件可复用性都很高。

当一个 UI 组件是可替换的时,就能够围绕它做一些比拟乏味且有价值的事件了。

最初的最初,文中的示例代码是为了帮忙了解而写,仅供参考。;-)


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

正文完
 0