用户界面组件API设计原则

设计开发者界面组件API的最佳实践,对UI组件编码和系统设计面试很有用

BootstrapMaterial UI这样的用户界面组件库,通过提供常用的组件,如按钮、标签、模版等,帮助开发者更快地构建用户界面,从而使开发者在开始一个新项目时不必重新发明轮子,从头构建这些组件。

通常在前端面试中,你会被要求建立 UI 组件并设计一个 API 来初始化它们。 设计好的组件 API 是前端工程师的面包和黄油。 本页涵盖了设计 UI 组件 API 的一些顶级技巧和最佳实践。 其中一些提示可能是针对框架的,但也可以推广到其他基于组件的 UI 框架。

初始化

jQuery 风格

ReactAngularVue等现代 JavaScript UI 库/框架出现之前,jQuery(和jQuery UI)是构建 UI 的最流行方式。jQuery UI 推广了通过涉及两个参数的 "构造函数 "初始化 UI 组件的想法:

  1. 根元素:一个根 DOM 元素,用来渲染内容。
  2. 定制选项:可选的、额外的、自定义的选项,通常以普通 JavaScript 对象的形式出现。

使用 jQuery UI,人们可以用一行代码将一个 DOM 元素变成一个slider(以及许多其他 UI 组件):

<div id="gfe-slider"></div>
<script>
$('#gfe-slider').slider();
</script>

jQuery refresher:jQuery UI 的slider()方法(构造函数)接收了一个 JavaScript 对象,作为定制选项。 执行$('#slider')选择<div id="slider">元素并返回一个 jQuery 对象,其中包含方便的方法来对该元素 "做一些事情",如addClassremoveClass等和其他 DOM 操作方法。 在 jQuery 方法中,选定的元素可以通过this关键字访问。 jQuery 的 API 是围绕这个 "选择一个元素并对其进行处理 "的方法而建立的,因此slider()方法不需要一个根 DOM 元素的参数。

slider 可以通过传入一个普通的 JavaScript 对象的选项来进行定制:

<div id="gfe-slider"></div>
<script>
$('#gfe-slider').slider({
animate: true,
max: 50,
min: 10,
// See other options here: https://api.jqueryui.com/slider/
});
</script>

Vanilla JavaScript 风格

在初始化组件方面没有 vanilla JavaScript 风格,因为 vanilla JavaScript 不是一个标准或框架。 但如果你读够了 GreatFrontEnd 对我们的 vanilla JavaScript UI 编码问题的解决方案,你会发现我们推荐的 API 与 jQuery 的类似,构造函数接收一个根元素和选项:

function slider(rootEl, options) {
// Do something with rootEl and options.
}

React

React 迫使你把 UI 写成包含其自身逻辑和外观的组件。 React 组件是返回标记的 JavaScript 函数(关于如何呈现自身的描述)。 React 组件可以接受 "props",它本质上是对组件选项的定制。

function Slider({ min, max }) {
// 使用 props 来呈现自定义组件。
return <div>...</div>;
}
<Slider max={50} min={10} />;

组件不会在 root 元素中。 为了将该元素渲染到页面中,需要使用一个单独的 API。

import { createRoot } from 'react-dom/client';
import Slider from './Slider';
const domNode = document.getElementById('#gfe-slider');
// React 将管理此元素内的 DOM。
const root = createRoot(domNode);
// 在元素内显示 Slider 组件。
root.render(<Slider max={50} min={10} />);

如果整个页面是 React 应用,你通常不需要自己调用createRoot(),因为根/页级组件只有一个createRoot调用。

定制外观

尽管 UI 库中的 UI 组件提供了默认的样式,但开发者通常希望用他们公司/产品的品牌和主题颜色来定制它们。 因此,所有的用户界面组件将允许通过一些方法来定制外观:

类注入

这里的想法很简单,组件接受一个 prop/option,允许开发者提供他们自己的类,这些类被添加到实际的 DOM 元素中。 这种方法不是很稳健,因为如果组件也通过类来添加自己的样式,那么在组件的类和开发者提供的类中可能会有冲突的属性。

React

import clsx from 'clsx';
function Slider({ className, value }) {
return (
<div className={clsx('gfe-slider', className)}>
<input type="range" value={value} />
</div>
);
}
<Slider className="my-custom-slider" value={50} />;
/* UI library default stylesheet */
.gfe-slider {
height: 12px;
}
/* Developer's custom stylesheet */
.my-custom-slider {
color: red;
}

通过类注入,开发者可以将组件的文本color改为red

如果组件内有许多 DOM 元素,一个className prop 是不够的,你也可以为不同元素的className 设置多个不同名称的 prop:

import { useId } from 'react';
import clsx from 'clsx';
function Slider({ label, value, className, classNameLabel, classNameTrack }) {
const id = useId();
return (
<div className={clsx('gfe-slider', className)}>
<label className={clsx('gfe-slider-label', classNameLabel)} for={id}>
{label}
</label>
<input
className={clsx('gfe-slider-range', classNameRange)}
id={id}
type="range"
value={value}
/>
</div>
);
}

jQuery

在 jQuery 中,类也可以作为选项的一个字段传递。

$('#gfe-slider').slider({
// 在现实中,jQuery UI 在 'class' 字段中使用
// 因为有多个元素。
class: 'my-custom-slider',
});

在现实中,所有 jQuery UI 的组件初始化器都接受classes字段,以允许添加额外的类到单个元素。 下面的例子取自jQuery UI Slider

$('#gfe-slider').slider({
classes: {
'ui-slider': 'highlight',
'ui-slider-handle': 'ui-corner-all',
'ui-slider-range': 'ui-corner-all ui-widget-header',
},
});

非确定性风格

类注入有一个不明显的缺点--最终的视觉结果是不确定的,可能不是预期的那样。 以下面的代码为例:

import clsx from 'clsx';
function Slider({ className, value }) {
return (
<div className={clsx('gfe-slider', className)}>
<input type="range" value={value} />
</div>
);
}
<Slider className="my-custom-slider" value={50} />;
/* UI library 默认样式表 */
.gfe-slider {
height: 12px;
color: black;
}
/* 开发者的自定义样式表 */
.my-custom-slider {
color: red; /* .gfe-slider 也定义了一个颜色的值。*/
}

在上面的例子中,.gfe-slider.my-custom-slider类都指定了color,由于这两个选择器具有相同的特殊性,获胜的样式实际上是后来出现在 HTML 页面上的类。 如果样式表的加载顺序没有保证(例如,如果样式表是懒惰地加载的),那么视觉结果将不是确定的。 这时,开发者开始使用!important.my-custom-slider.my-custom-slider等黑客手段,让他们的选择器赢得特异性战争,CSS 代码开始变得不可维护。

在 jQuery UI 中,如果一个自定义类被添加,现有的默认值不会被使用。 这消除了 "获胜风格 "的模糊性,但用户现在必须重新实现原类中存在的所有必要风格。 这种方法也可以应用于 React 组件,以解决模糊不清的问题。

尽管它可能存在缺陷,但类注入仍然是一个非常受欢迎的选择。

CSS 选择器钩子

从技术上讲,如果开发者阅读组件的源代码,并通过使用相同的类来定义他们的自定义样式,就可以实现定制。 然而,这样做是很危险的,因为依赖组件的内部结构,而且不能保证类名在将来不会改变。

如果 UI 库的作者能够将这些类/属性作为他们的 API 的一部分,这就有了这些保证:

  1. 选择器列表已发布,供外部参考。
  2. 已发布的选择器将不会被更改。 如果它们被改变,这将是一个破坏性的变化,需要按照 semver 的要求进行版本升级。

然后,这是一种可接受的做法,开发者可以通过在他们的样式表中使用这些选择器来 "钩住 "它们(针对它们)。

钩住一个组件的选择器的一个例子:

import { useId } from 'react';
import clsx from 'clsx';
function Slider({ label, value }) {
const id = useId();
return (
<div className="gfe-slider">
<label className="gfe-slider-label" for={id}>
{label}
</label>
<input className="gfe-slider-range" id={id} type="range" value={value} />
</div>
);
}
/* UI library 默认样式表 */
.gfe-slider {
font-size: 12px;
}
/* 在这个样式表中没有定义其他的类,gfe-slider-label和gfe-slider-range被添加到组件中,只是为了让开发者获得对底层元素的访问。 */
/* Developer's 默认样式表 */
.gfe-slider {
font-size: 16px; /* 与默认的.gfe-slider冲突 */
padding: 10px 20px;
}
.gfe-slider-label {
color: red;
}
.gfe-slider-range {
height: 20px;
}

这种方法为开发者省去了在组件中传递类的麻烦,因为他们只需要编写 CSS 来定制样式。 Reach UI是 React 的一个无头 UI 组件库,使用元素选择器。 每个组件在底层 DOM 元素上都有一个data-reach-*属性。

[data-reach-menu-item] {
color: blue;
}

然而,这种方法仍然受到 "类注入 "的非确定性风格问题的影响,并且不容易允许按实例进行样式设计。 如果需要每个实例的样式,这种方法可以与类的注入方法相结合。

主题对象

该组件不是接收类,而是接收一个用于样式的键/值的对象。 如果只有一个严格的属性子集需要定制,或者你想把样式限制在几个属性上,这就很有用。

const defaultTheme = { color: 'black', height: 12 };
function Slider({ value, label, theme }) {
// Combine with default.
const finalTheme = { ...defaultTheme, ...theme };
return (
<div className="gfe-slider">
<label
for={id}
style={{
color: finalTheme.color,
}}>
{label}
</label>
<input
id={id}
type="range"
value={value}
style={{
height: finalTheme.height,
}}
/>
</div>
);
}
<Slider themeOptions={{ color: 'red', height: 24 }} {...props} />;

然而,由于没有使用有冲突的样式的类,而且内联样式的特异性比类高,所以没有特异性冲突,内联样式将获胜。 然而,需要支持的选项数量可能真的会迅速增长。 内联样式也存在于每个组件实例的 DOM 中,如果这个组件在一个页面中被渲染了数百/数千次,这可能对性能不利。

主题对象只是一种将造型限制在某些属性和可选的一组值上的方法,这些值不需要作为内联样式使用,而是可以与其他造型方法相结合。

CSS 预处理程序的编译

UI 库通常用 CSS 预处理器编写,如SassLessBootstrap是用 Sass 编写的,他们提供了一种方法来自定义所使用的 Sass 变量,以便开发者可以生成一个自定义的 UI 库样式表。

这种方法很好,因为它不依赖覆盖 CSS 选择器来实现定制。 此外,产生的 CSS 数量也较少,没有多余的重写样式。 缺点是需要一个编译的步骤。

CSS 变量 / 自定义属性

CSS 变量(或更正式地称为 CSS 自定义属性)是由 CSS 作者定义的实体,它包含特定的值以便在整个文档中重复使用。 var()函数,如果给定的变量未被设置,它接受回退值。

function Slider({ value, label }) {
return (
<div className="gfe-slider">
<label for={id}>{label}</label>
<input id={id} type="range" value={value} />
</div>
);
}
/* UI library default stylesheet */
.gfe-slider {
/* 如果没有设置,则回调为12px。 */
font-size: var(--gfe-slider-font-size, 12px);
}
/* Developer's custom stylesheet */
:root {
--gfe-slider-font-size: 15px;
}

开发者可以通过:root选择器为--gfe-slider-font-size全局定义一个值,并为.gfe-slider类设置字体大小为 15px。 这种方法的好处是它不需要 JavaScript,然而,每个组件的定制将更加麻烦(但仍有可能)。

Render Props

在 React 中,render props 是一个组件用来知道要渲染什么的函数 prop。 它对于将行为和表现形式分开是很有用的。 许多行为/无头 UI 库,如RadixHeadless UIReach UI大量使用了 render props。

国际化 (i18n)

你的用户界面是否适用于多种语言? 增加对更多语言的支持有多容易?

避免用某种语言对标签进行硬编码

一些 UI 组件内有标签字符串(例如,图片轮播有上一个/下一个按钮的标签)。 如果能让这些标签字符串成为组件 props/options 的一部分,允许自定义这些标签字符串就更好了。

从右到左的语言

有些语言(如阿拉伯语、希伯来语)是从右向左阅读的,用户界面必须水平翻转。 该组件可以接受一个 "方向 "props/options,并改变元素的渲染顺序。 例如,在 RTL 语言中,上一页和下一页的按钮将分别在右边和左边。

使用CSS 逻辑属性来为你的风格做准备,让你的布局适用于不同的书写模式

Mark complete