怎么编写一个高可用的前端组件
React 流行以后,编写前端组件的成本变得非常低廉,开发一个通用组件是件非常容易的事,日常业务开发也早已变成搭积木式的堆组件了。在这种开发环境下组件的可用性就变得至关重要,谁都希望拿在手的组件可以像橡皮泥一样”任人摆布“,“放之四海皆准”,而不是动不动就要去改源码。
高可用包含两层意思,
一是开发时的高可用,一处编码,处处可用
二是用户群体的高可用,正常用户行为可用,特殊用户行为也可用。
那,如何做到高可用?
葛大爷说得好:
恶心自己,成全别人
API 数量可以有很多,但必选参数要尽量少,最好是没有
使用组件最好的情况就是不要任何参数,就像我们使用原生的 HTML 组件那样。比如:
<audio />
这个组件,我们可以不加任何参数去使用它而不报错,也可以加上任意的参数
<audio src="./music.mp3" controls loop />
编写组件的时候可以多用 ES6 的默认赋值功能
const SearchBar = props => {
const { placeholder = '搜索', ...others } = props
return <input placeholder={placeholder} {...others} />
}
export default SearchBar
关于组件状态
const SearchBar = props => {
const { value: _value, onChange, ...others } = props
const [value, setValue] = useState(_value)
const handleChange = (e) => {
setValue(e.target.value)
onChange?.(e) // optional chain
}
useEffect(() => {
setValue(_value)
}, [_value])
return <input {...others} onChange={handleChange} />
}
export default SearchBar
value
可以由组件内部维护也可以由外部控制
const App = () => {
const [value, setValue] = useState('')
const handleChange = (e) => setValue(e.target.value)
return <div>
<SearchBar /> // 完全由内部控制
<SearchBar value="123" /> // value 相当于 defaultValue 的作用
<SearchBar value={value} onChange={handleChange} />// 由外部控制
</div>
}
还有个典型的例子是往组件里传类型 setState
dispatch
之类的方法,为了避免这种情况可以把内部的事件和状态暴露出去。组件不要去约定外部依赖,只交流数据,不交流 API。
处下居后,利万物而不争 ——《老子》
不要认为自己在“封”装,而是把各个子组件捧出去,更不能“封”杀子组件,让它们都走到台前,面向观众。
API/功能组合的形式最好是 “V” 型或 “||” 型,但不要是“/\”型
解释一下这句话的意思就是说,从底层到上层的封装过程不要“偷吃”下层的API/功能,多用继承,让上层参数漏到底层去。
你把组件 A B C 组合成 D ,就尽量做到 D ≥ A + B + C。比如非常常见的就是偷吃了 HTML 元素的 style
和 className
属性,或者 onClick
之类的事件。为避免这样的问题我们可以活用 rest parameter 功能
const Input = props => {
const { customProp, ...others } = props
return <input customProp={customProp} {...others} />
}
export default Input
如果是 TypeScript 代码,我们可以继承 React.HTMLAttributes
export interface Props extends React.HTMLAttributes {
customProp?: string
}
假如我们可以用 Dropdown
和 Menu
来组合成一个 Selector
组件,你应该把 Dropdown
和 Menu
的 API 尽量都暴露出去
export interface Props extends DropdownProps, MenuProps {
someProp?: string
}
把“封装”改成“拼装”。
API/功能尽可能公开、独立
把组件按功能拆分成独立的小组件,都 exports
出去,让使用组件的人可以获取到原始组件,在万不得已的情况可以让用户自己去组合。同时更重要的是可以降低组件本身的开发和维护难度。把难的事情折成很多小的事件,一个个去完成这些小的事件一般会更轻松。
假如我们有一个 Menu 组件
const Menu = props => {
const { dataSource } = props
return <div>
{
dataSource.map(d => <MenuItem key={d.id} dataSource={d}>{d.label}</MenuItem>)
}
</div>
}
export default Menu
想加一个键盘导航功能可以实现一个 uesKeyboardNav
的 React Hook。
const Menu = props => {
const { dataSource } = props
const ref = useRef()
useKeyboardNav(ref) // 键盘导航功能
return <div ref={ref}>
{
dataSource.map(d => <MenuItem key={d.id} dataSource={d}>{d.label}</MenuItem>)
}
</div>
}
export default Menu
如果用户发现我们的组件不能很好满足他的需求,他在可以拿到独立实现的 MenuItem
和 userKeyboarNav
的前提下,可以很轻松地定制出自己的 Menu。
相反的例子是,为了满足用户的各种需求,在组件里加入各种分支条件和参数,最后组件变得臃肿难以维护和使用
做组件并不一定要处处“亲力亲为”,让别人实现你的想法,按你的想法去用组件也在输出分享你的组件,正所谓
大象无形,道隐无名
另一个问题是用户群体的高可用
除了鼠标,也有人喜欢/被迫用键盘,也有人使用触摸设备。在用键盘的时候我们一般都希望有当前激活元素的高亮显示效果,而触摸设备一般不需要,现在我们可以通过 CSS 的 :focus-visible
伪类来实现
.menu-item:not(:focus-visible) {
outline: none;
}
我们也可以通过手动设置 autoIndex
来让普通元素可以获取焦点
const MenuItem = props => {
const { label } = props
return <div role="menuitem">
<div role="button" tabIndex={0}>{label}</div>
</div>
}
export default MenuItem
设置 tabIndex={-1}
则可以让元素自动获得焦点,比如在 Dropdown
组件上让 overlayer 显示后自动获取焦点。
为了让使用读屏软件的用户也能使用我们的网页,应该加上正确的 aria
属性
const MenuItem = props => {
const { label, value } = props
const checked = useChcecked(value) // 假设是这样实现的,下同
const [disabled, setDisabled] = useState()
return <div className="menu-item" role="menuitem" aria-expanded={expanded}>
<div
className="menu-item-btn"
role="button"
tabIndex={0}
aria-pressed={checked}
aria-disabled={disabled}
>
{label}
</div>
</div>
}
export default MenuItem
当然我们还应该配置效果良好的色彩来提示用户
.menu-item {
&[aria-expanded="true"] {
background: #999;
}
&-btn {
&[aria-pressed="true"] {
background: #eee;
}
&[aria-disabled="true"] {
color: #aaa;
cursor: default;
}
}
}
颜色明暗对比应该强一些,并不是每个人和每个屏幕都对所有颜色敏感,而在灰阶上区分不同元素角色地位的强度通常是比较有效的。所以不要仅用不同的色相色调来区分元素角色及其重要度,考虑加上明度、纯度的变化。
编写 HTML 结构的时候甚至应该考虑 CSS 未能加载的情况,所以在标签选择上,尽量使用原生带语义化的标签。
如果是个可点击的操作区,可以用 button
,而非在 div
上绑定一个 onClick
事件,链接尽量用 a
,这些元素原生支持键盘操作。而列表可以用 ol
ul
dl
,原生具有列表结构样式。
在使用 CSS 绘制组件的时候有个问题很容易被忽视,那就是“尺寸”。用户的屏幕是有限的,也可能是可变化的,克制自己不要占用太多空间,高效地利用空间,尽量不用让用户必须在 800 x 1000 的浏览器窗口下才能用你的组件,那很痛苦。
保持紧致就是保持精致,紧凑的 UI 设计通常会给人精致的感觉。同样地,高对比的色彩搭配通常让人观感舒适。
在交互上尽量让行为统一,不要设置过多的例外情况,好的交互逻辑就和好的数学公式一样,是最简单的。
一生二,二生三,三生万物