[译]创建 React 组件的10条准则

455 阅读14分钟
原文地址:dev.to/selbekk/the…

要创建供多人使用的组件是很难的,组件包含属性(props),如果这些属性要作为公开 API 的一部分,那就必须非常仔细地考虑组件应该接受哪些属性。

本文会简要介绍 API 设计中的一些最佳实践,以及帮助你开发出优秀组件的 10 条准则。

什么是 API ?

API (Application Programing Interface,应用编程接口)是两段代码交互的地方。它是代码与世界沟通的桥梁,我们将这个桥梁称为接口。可以通过 API 进行数据与功能的交互。

后端和前端之间的接口是一个 API。 可以通过与这个 API 进行交互来访问一组数据和功能。

一个类和调用这个类的代码之间的接口同样也是一个 API。你可以调用类里的方法,来检索数据或触发封装在里面的功能。

同理,组件中要接收的 props 同样也是 API 。 这是调用者与组件交互的方式,当你想要对外暴露什么的时候,会应用很多相同的规则和注意事项。

API 设计的一些最佳实践

那么,在设计 API 的时候,需要注意哪些规则和事项呢?我们在这方面做了一些研究,最后找到了许多非常优秀的资源。我们选取了其中的 2 篇:Josh Tauberer 的《What Makes a Good API ?》以及 Ron Kurir 的同名文章;并且我们也提出了4 个可遵循的最佳实践。

版本稳定

当创建一个 API 的时候,需要考虑的最重要的一件事是尽可能地保持 API 的稳定。这意味着需要最大限度地减少重大变化的数量。如果 API 真的有较大的变化,也请确保撰写了详细的升级指南,并尽可能提供一份代码模块,可以让用户自动完成升级过程。

如果正在发布 API,请确保遵循了语义版本规范。这可以让用户轻松地决定所需的版本。

提供错误描述信息

每当调用 API 发生错误时,你需要尽可能地去解释发生了什么问题,以及如何去修复错误。在没有任何提醒或信息的情况下,直接抛出一个“错误使用”来羞辱调用者貌似不是一种良好的用户体验。

相反,撰写描述性错误信息可以帮助调用者来修改他们调用 API 的方式。

别让程序猿犯嘀咕

程序猿是脆弱的,而且你也不希望他们在使用 API 的时候吓到他们。也就是说,API 应该尽可能直观。可以通过遵循最佳实践和现有的命名习惯来实现这一点。

另外还需要注意一点:保持代码风格的连贯。如果在布尔属性的名称前加上了 is 或者 has 作为前缀,但是接下来却又不这么做了,这就会让人感到费解。

精简 API 结构

当我们在讨论做减法的时候,同样也包括减少 API 。功能多了固然很好,但是 API 的结构越简单,调用者的学习成本就越小。反过来讲,这会被认为是一个简单易用的 API 。

总有办法来控制 API 的大小,其中的一个办法是,从旧的 API 中重构出一个新的API。

创建组件的 10 条准则

上面的 4 条黄金法则在编写 REST API 以及古老的 Pascal 程序时很管用,那应该如何转化才能适用于现代世界的 React 呢?

正如我们前面所提到的,组件有自己的 API。我们称其为 属性(props),这是我们提供给组件数据、回调函数以及其他功能的方式。我们应该以何种方式构建props对象,才能够不违反上述任何一条规范呢?我们同样应该以何种方式编写组件,才能让下一个开发者轻松地测试它们呢?

我们列出了创建组件时需要遵循的 10 条准则,并且我们希望你能发现它们是行之有效的。

1. 写文档

如果没有文档来记录组件是如何使用的,好吧,虽然大多数开发者会随时查看你的代码,但这不能说是一种良好的用户体验。

写文档有很多工具,我们推荐以下 3 个:

前两个会在开发组件的时候提供一个演练场,第三个则会让你使用 MDX 编写更多自由格式的文档。

无论选择哪个,都请确保在文档中记录了 API 的用法 ,以及组件的用法和使用时机。 后者在共享组件库中尤为重要。

2. 允许上下文语义

HTML 是一种通过语义化的方式来组织信息的语言。大多数组件是使用 <div /> 标签来构建的。这在某种程度上是有道理的——因为通用组件不清楚它到底应该是一个 <article /> 还是 <section /> 或者是 <aside /> ,尽管如此,但只用<div/>来构建也并不完美。

相反,我们建议允许组件接受一个 as 属性,它将始终覆盖正在呈现的DOM 元素。下面是一个实现它的例子:

function Grid({ as: Element, ...props }) {
  return <Element className="grid" {...props} />
}
Grid.defaultProps = {
  as: 'div',
};

我们将 as 属性重命名为局部变量 Element,并在 JSX 中使用它。当不需要更多语义化的 HTML 标签时,我们也提供了普通的默认值来传给组件。

当使用 <Grid /> 组件的时候,你可以传入合适的标签:

function App() {
  return (
    <Grid as="main">
      <MoreContent />
    </Grid>
  );
}

请注意上面的代码在 React 组件中同样有效。下面是一个很好的例子,展示了如果想让一个<Button />组件呈现一个 React Router <Link />。

<Button as={Link} to="/profile">
  Go to Profile
</Button>

3.避免布尔属性

布尔属性听起来不错,你不需要给它赋值就可以指定一个布尔属性,所以这让它们看起来非常优雅:

<Button large>BUY NOW!</Button>

尽管看起来很好,但是布尔属性却只允许有 2 个可选值:打开或者关闭,展示或者隐藏,1 或者 0。

每当你开始想为布尔属性引入些其他的东西的时候,比如尺寸、变体、颜色,或者其他可能除了二元选择之外的任何东西,就有些麻烦了。

<Button large small primary disabled secondary> WHAT AM I?? </Button>

换句话说,布尔属性常常不能随着需求的改变而进行扩展。相反,尝试使用类似字符串类型的可枚举类型来作为属性值,可以获得二元选择之外更多的选择。

<Button variant="primary" size="large"> I am primarily a large button </Button>

这并不代表布尔属性就完全没有一席之地了,其实布尔属性是有用的!上面列出的 disable 属性应当依旧是布尔类型——因为在“可用”与“不可用”的状态之间,不存在中间状态,所以这里用布尔类型是恰当的。

4.使用 props.children

React 中有几个特殊的属性,他们的处理方式与其他属性不太一样。其中一个就是key,用来在有序列表中追踪列表项的,另一个就是children。

在一个开始标签和结束标签之间的任何东西都被放置在props.children属性中,推荐尽量多使用这个属性。

推荐的原因是使用props.children属性比起使用content属性,或者其他只接受类似文本的简单值的属性来说,要简便的多。

<TableCell content="Some text" /> // vs <TableCell>Some text</TableCell>

使用 props.children还有几个好处。首先,它的写法和普通的 HTML 是一样的。第二,你可以向组件传递任何想要的东西,而不是向组件中添加 leftIcon 和 rightIcon 属性,把他们作为 props.children 的一部分传递给组件即可。

<TableCell> <ImportantIcon /> Some text </TableCell>

你可能会说,我的组件只会渲染普通的文本,不需要渲染其他东西。在某些情况下可能是正确的,至少现在是没问题的。而在未来需求变化的时候,你就会发现使用 props.children 的好处。

5.让父组件的钩子函数进入内部逻辑

有时,我们会创建一些内部逻辑复杂的组件,比如自动补全的下拉菜单或者可交互图表。

这种类型的组件通常会有冗长复杂的 API ,其中一个原因是,需要覆盖的功能,以及需要支持的特殊用法,这两者的数量都会随着时间的推移而断增加。

如果我们想提供一个简单且标准化的属性,来让调用者去控制或者覆盖组件的默认行为,我们应该怎么做呢?

Kent C. Dodds 为此写过一篇很棒的文章,在文中他将这个问题的解决方案称为:state reducers。请参阅这两篇文章:Post about the concept itselfHow to implement it for React Hooks

简单总结来说,这种通过传递 state reducer 函数到组件中的模式,允许调用者访问组件内部分派的所有操作。你可以修改 state ,或者触发内部事件。这是一种创建无需 prop 的高度自定义组件很好的方式。

示例代码:

function MyCustomDropdown(props) {
  const stateReducer = (state, action) => {
    if (action.type === Dropdown.actions.CLOSE) {
      buttonRef.current.focus();
    }
  };
  return (
    <>
      <Dropdown stateReducer={stateReducer} {...props} />
      <Button ref={buttonRef}>Open</Button>
    </>
}

顺便提一下,你当然可以创建更简单的方式来响应事件。在上面的例子中,在组件中提供一个 onClose 属性会产生更好的用户体验。

6. 扩散剩余属性

每当创建一个新的组件的时候,请确保将剩余的属性也扩散到有意义的元素上。

如果有某些属性仅需传递给子组件或子元素(而组件自身并不需要这个属性),那就不必添加到你的组件中,这么做可以让组件 API 更加稳定,即使当下一个开发者需要新事件监听器的时候,也无需发布新版本的组件。

例如:

function ToolTip({ isVisible, ...rest }) {
  return isVisible ? <span role="tooltip" {...rest} /> : null;
}

你的组件可以向底层组件或元素传递属性,比如 className 或者 ·onClick 的监听函数,一定要确保外部的调用者一样可以这样做。比如在 class 这种情况中,你可以使用 npm 上的 classname 包来方便地添加 class 属性(或者干脆直接用简单的 string 字符串)。

import classNames from 'classnames';
function ToolTip(props) {
  return (
    <span 
      {...props} 
      className={classNames('tooltip', props.tooltip)} 
    />
}

在事件监听回调的情况下,可以用一个小工具函数将它们合并成单个函数。 例如:

function combine(...functions) {
  return (...args) =>
    functions
      .filter(func => typeof func === 'function')
      .forEach(func => func(...args));
}

现在,我们创建了一个以函数数组为参数的函数,它返回一个新的回调函数,该回调函数会向各个函数传入相同的参数,并依次调用各个函数。

示例代码:

function ToolTip(props) {
  const [isVisible, setVisible] = React.useState(false);
  return (
    <span 
      {...props}
      className={classNames('tooltip', props.className)}
      onMouseIn={combine(() => setVisible(true), props.onMouseIn)}
      onMouseOut={combine(() => setVisible(false), props.onMouseOut)}
    />
  );
}

7. 充分提供默认值

请确保为属性提供了充分的默认值,这样做可以最大限度地减少必传值的数量,而且也大大简化了代码实现。

以 onClick 处理函数为例,如果它不是必需的,就可以提供一个空函数来作为默认值。这样,你就可以在代码中随时调用它,就好像组件总是被提供了回调函数一样。

另一个例子是自定义输入。 除非明确提供,否则假设输入的字符串是空字符串。 这将使你确保始终处理字符串对象,而不是 undefined 或 null 。

8.不要重命名 HTML 属性

HTML 作为一门语言拥有自己的属性,它本身就是 HTML 元素的 API,为啥不继续使用这些 API?

正如前面所提到的,精简 API 数量并使其具有一定的直观性是改进组件API的两种很好的方法。所以与其创建自己的自定义标签属性,为什么不直接使用现成的原生标签 API 呢?

因此,不要重命名任何现有 HTML 属性。你甚至没有用新的 API 替换现有的 API,你只是在上面添加了自己的 API。其他人仍然可以将原生标签与你自定义标签的属性一起传递,那么最终的值应该是什么呢?

另外,也请确保在组件中没有将 HTML 属性进行覆盖。一个很好的例子就是 <button /> 元素的 type 属性。它的值可以是 submit (默认值)、button 和 reset。但是,许多开发者却倾向于将这个属性名用于表示按钮的可视类型(primary,cta 等等)。

通过改变这个属性的用途,你不得不添加其他属性来覆盖,设置实际的 type 属性值,这只会给调用者带来困惑。

9. 指定属性的类型

没有什么文档比代码中的文档更好了,React 提供了 prop-types 包来声明组件 API 的类型,一定要用它。

你可以为所有的属性指定明确的类型,也可以规定属性是否必传,甚至可以使用 JSDoc 注释来做进一步改进。

如果忽略了必传属性,或者传递了无效值和意外值,运行时会在控制台打印警告。这样的开发体验很棒,并且可以从生产构建中剥离出来。

如果使用 TypeScript 或者 Flow 来编写 React 应用,你就可以将此类API文档作为语言功能。这会带来更好的工具支持,以及更棒的开发体验。

如果你自己没有使用类型化的 JavaScript,你仍然应该考虑为那些使用类型化的 JavaScript调用者提供类型定义。通过这种方式,他们能够更轻松地使用你的组件。

10. 为开发者而设计

最后,需要遵循的最重要一条规则就是:保证 API 以及“组件体验”已经针对它的调用者进行了优化。

提升开发者体验的一个办法是在错误调用时提供详细的错误信息,以及在开发过程中的警告信息。

当编写错误和警告时,记得使用链接引用文档或提供简单的代码示例。这可以帮助开发者更快地发现错误并修改,提供更好的开发体验。

不必担心过于冗长的错误信息会占用太大空间,况且构建生产环境的时候也不会把这些信息打包进去。

React 本身就是一个非常优秀的类库,当你忘记使用 key 或者拼错了生命周期的名字等等,都会在控制台收到大量详细的错误警告信息。

因此,要为你未来的用户设计,在 5 周内为自己设计,为那些在你离开后必须维护你代码的可怜兄 dei 设计,为开发者设计!

总结

在经典的 API 设计中我们还可以学到很多优秀的东西,通过遵循本文中提到的提示和准则,你应该可以创建出易于使用、便于维护、使用直观,以及出现问题时可以进行快速修复的组件。 你还有哪些关于创建组件的点子?