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

原文地址: dev.to/selbekk/the…

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

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

什么是 API ?

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

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

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

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

API 设计的一些最佳实践

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

版本稳定

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

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

提供错误描述信息

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

相反,撰写描述性错误信息能够帮助调用者来修改他们调用 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 设计中咱们还能够学到不少优秀的东西,经过遵循本文中提到的提示和准则,你应该能够建立出易于使用、便于维护、使用直观,以及出现问题时能够进行快速修复的组件。 你还有哪些关于建立组件的点子?

相关文章
相关标签/搜索