front-end-interview-handbook/packages/front-end-interview-guidebook/contents/user-interface-components-a.../zh-CN.mdx

372 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 用户界面组件API设计原则
description: 设计开发者界面组件API的最佳实践对UI组件编码和系统设计面试很有用
---
像[Bootstrap](https://getbootstrap.com/)和[Material UI](https://mui.com/)这样的用户界面组件库,通过提供常用的组件,如按钮、标签、模版等,帮助开发者更快地构建用户界面,从而使开发者在开始一个新项目时不必重新发明轮子,从头构建这些组件。
通常在前端面试中你会被要求建立UI组件并设计一个API来初始化它们。 设计好的组件API是前端工程师的面包和黄油。 本页涵盖了设计UI组件API的一些顶级技巧和最佳实践。 其中一些提示可能是针对框架的但也可以推广到其他基于组件的UI框架。
## 初始化
### jQuery 风格
在[React](https://reactjs.org/)、[Angular](https://angular.io/)和[Vue](https://vuejs.org/)等现代JavaScript UI库/框架出现之前,[jQuery](https://jquery.com/)(和[jQuery UI](https://jqueryui.com/)是构建UI的最流行方式。jQuery UI推广了通过涉及两个参数的 "构造函数 "初始化UI组件的想法
1. **根元素**一个根DOM元素用来渲染内容。
2. **定制选项**可选的、额外的、自定义的选项通常以普通JavaScript对象的形式出现。
使用jQuery UI人们可以用一行代码将一个DOM元素变成一个[slider](https://api.jqueryui.com/slider/)以及许多其他UI组件
```html
<div id="gfe-slider"></div>
<script>
$('#gfe-slider').slider();
</script>
```
**jQuery refresher**jQuery UI的`slider()`方法构造函数接收了一个JavaScript对象作为定制选项。 执行`$('#slider')`选择`<div id="slider">`元素并返回一个jQuery对象其中包含方便的方法来对该元素 "做一些事情",如`addClass``removeClass`等和其他DOM操作方法。 在jQuery方法中选定的元素可以通过`this`关键字访问。 jQuery的API是围绕这个 "选择一个元素并对其进行处理 "的方法而建立的,因此`slider()`方法不需要一个根DOM元素的参数。
slider 可以通过传入一个普通的JavaScript对象的选项来进行定制
```html
<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编码问题](/questions/vanilla)的解决方案你会发现我们推荐的API与jQuery的类似构造函数接收一个根元素和选项
```js
function slider(rootEl, options) {
// Do something with rootEl and options.
}
```
### React
React迫使你把UI写成包含其自身逻辑和外观的组件。 React组件是返回标记的JavaScript函数关于如何呈现自身的描述。 React组件可以接受 "props",它本质上是对组件选项的定制。
```js
function Slider({ min, max }) {
// Use the props to render a customized component.
return <div>...</div>;
}
<Slider max={50} min={10} />;
```
组件不会在 root 元素中。 为了将该元素渲染到页面中需要使用一个单独的API。
```jsx
import { createRoot } from 'react-dom/client';
import Slider from './Slider';
const domNode = document.getElementById('#gfe-slider');
// React will manage the DOM within this element.
const root = createRoot(domNode);
// Display the Slider component within the element.
root.render(<Slider max={50} min={10} />);
```
如果整个页面是React应用你通常不需要自己调用`createRoot()`,因为根/页级组件只有一个`createRoot`调用。
## 定制外观
尽管UI库中的UI组件提供了默认的样式但开发者通常希望用他们公司/产品的品牌和主题颜色来定制它们。 因此,所有的用户界面组件将允许通过一些方法来定制外观:
### 类注入
这里的想法很简单组件接受一个prop/option允许开发者提供他们自己的类这些类被添加到实际的DOM元素中。 这种方法不是很稳健,因为如果组件也通过类来添加自己的样式,那么在组件的类和开发者提供的类中可能会有冲突的属性。
#### React
```jsx
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} />;
```
```css
/* UI library default stylesheet */
.gfe-slider {
height: 12px;
}
```
```css
/* Developer's custom stylesheet */
.my-custom-slider {
color: red;
}
```
通过类注入,开发者可以将组件的文本`color`改为`red`。
如果组件内有许多DOM元素一个`className` prop 是不够的,你也可以为不同元素的`className` 设置多个不同名称的 prop
```jsx
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中类也可以作为选项的一个字段传递。
```js
$('#gfe-slider').slider({
// 在现实中jQuery UI 在 'class' 字段中使用
// 因为有多个元素。
class: 'my-custom-slider',
});
```
在现实中所有jQuery UI的组件初始化器都接受`classes`字段,以允许添加额外的类到单个元素。 下面的例子取自[jQuery UI Slider](https://api.jqueryui.com/slider/#option-classes)
```js
$('#gfe-slider').slider({
classes: {
'ui-slider': 'highlight',
'ui-slider-handle': 'ui-corner-all',
'ui-slider-range': 'ui-corner-all ui-widget-header',
},
});
```
#### 非确定性风格
类注入有一个不明显的缺点--最终的视觉结果是不确定的,可能不是预期的那样。 以下面的代码为例:
```jsx
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} />;
```
```css
/* UI library 默认样式表 */
.gfe-slider {
height: 12px;
color: black;
}
```
```css
/* 开发者的自定义样式表 */
.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的要求进行版本升级。
然后,这是一种可接受的做法,开发者可以通过在他们的样式表中使用这些选择器来 "钩住 "它们(针对它们)。
钩住一个组件的选择器的一个例子:
```jsx
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>
);
}
```
```css
/* UI library 默认样式表 */
.gfe-slider {
font-size: 12px;
}
/* 在这个样式表中没有定义其他的类gfe-slider-label和gfe-slider-range被添加到组件中只是为了让开发者获得对底层元素的访问。 */
```
```css
/* 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](https://reach.tech/styling)是React的一个无头UI组件库使用元素选择器。 每个组件在底层DOM元素上都有一个`data-reach-*`属性。
```css
[data-reach-menu-item] {
color: blue;
}
```
然而,这种方法仍然受到 "类注入 "的非确定性风格问题的影响,并且不容易允许按实例进行样式设计。 如果需要每个实例的样式,这种方法可以与类的注入方法相结合。
### 主题对象
该组件不是接收类,而是接收一个用于样式的键/值的对象。 如果只有一个严格的属性子集需要定制,或者你想把样式限制在几个属性上,这就很有用。
```jsx
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={{
fontSize: 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预处理器编写如[Sass](https://sass-lang.com/)和[Less](https://lesscss.org/)。 [Bootstrap](https://getbootstrap.com/)是用Sass编写的他们提供了一种方法来[自定义所使用的Sass变量](https://getbootstrap.com/docs/5.3/customize/sass/)以便开发者可以生成一个自定义的UI库样式表。
这种方法很好因为它不依赖覆盖CSS选择器来实现定制。 此外产生的CSS数量也较少没有多余的重写样式。 缺点是需要一个编译的步骤。
### CSS 变量 / 自定义属性
[CSS变量](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties)或更正式地称为CSS自定义属性是由CSS作者定义的实体它包含特定的值以便在整个文档中重复使用。 `var()`函数,如果给定的变量未被设置,它接受回退值。
```jsx
function Slider({ value, label }) {
return (
<div className="gfe-slider">
<label for={id}>{label}</label>
<input id={id} type="range" value={value} />
</div>
);
}
```
```css
/* UI library default stylesheet */
.gfe-slider {
/* 如果没有设置则回调为12px。 */
font-size: var(--gfe-slider-font-size, 12px);
}
```
```css
/* 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库如[Radix](https://www.radix-ui.com/)、[Headless UI](https://headlessui.com/)和[Reach UI](https://reach.tech/menu-button)大量使用了render props。
{/* TODO: Section on manipulation of component after initialization */}
## 国际化 (i18n)
你的用户界面是否适用于多种语言? 增加对更多语言的支持有多容易?
### 避免用某种语言对标签进行硬编码
一些UI组件内有标签字符串例如图像轮播有上一个/下一个按钮的标签)。 如果能让这些标签字符串成为组件props/options的一部分允许自定义这些标签字符串就更好了。
### 从右到左的语言
有些语言(如阿拉伯语、希伯来语)是从右向左阅读的,用户界面必须水平翻转。 该组件可以接受一个 "方向 "props/options并改变元素的渲染顺序。 例如在RTL语言中上一页和下一页的按钮将分别在右边和左边。
使用[CSS逻辑属性](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties)来为你的风格做准备,让你的布局适用于不同的[书写模式](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Writing_Modes)。
{/* TODO: Give examples of how to implement RTL. */}