Skip to content

Instantly share code, notes, and snippets.

@iahu
Created February 8, 2021 06:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iahu/f8d979606e364aa047386533f871bbcd to your computer and use it in GitHub Desktop.
Save iahu/f8d979606e364aa047386533f871bbcd to your computer and use it in GitHub Desktop.

怎么编写一个高可用的前端组件

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 元素的 styleclassName 属性,或者 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
}

假如我们可以用 DropdownMenu 来组合成一个 Selector 组件,你应该把 DropdownMenu 的 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

如果用户发现我们的组件不能很好满足他的需求,他在可以拿到独立实现的 MenuItemuserKeyboarNav 的前提下,可以很轻松地定制出自己的 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 设计通常会给人精致的感觉。同样地,高对比的色彩搭配通常让人观感舒适。

在交互上尽量让行为统一,不要设置过多的例外情况,好的交互逻辑就和好的数学公式一样,是最简单的。

一生二,二生三,三生万物

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment