十条戒律

2025-05-26

十条戒律

与Caroline Odden合作撰写。基于 2019 年 6 月在 ReactJS Oslo Meetup 上举行的同名、同人员的演讲。

创建大量用户使用的组件并非易事。如果组件的 props 需要作为公共 API 的一部分,那么你必须仔细考虑应该接受哪些 props。

本文将向您简要介绍 API 设计中的一些最佳实践,以及可用于创建您的其他开发人员喜欢使用的组件的 10 条实用戒律的明确列表。

一个人思考 API 的插图

什么是 API?

API(应用程序编程接口)本质上是两段代码的交汇点。它是代码与外界交互的接触面。我们称这个接触面为接口。它是一组定义好的、可供您交互的操作或数据点。

后端和前端之间的接口是 API。您可以通过与此 API 交互来访问给定的数据集和功能。

类与调用该类的代码之间的接口也是一种 API。您可以调用类上的方法来检索数据或触发封装在其中的功能。

按照同样的思路,组件接受的 props 也是它的 API。它是用户与组件交互的方式,在决定暴露什么内容时,许多相同的规则和注意事项也适用。

API设计中的一些最佳实践

那么,设计 API 时应该遵循哪些规则和注意事项呢?我们对此进行了一些研究,发现有很多很棒的资源。我们挑选了两篇——Josh Tauberer《什么是好的 API?》Ron Kurir 的同名文章——并总结了 4 条最佳实践供大家参考。

稳定版本

创建 API 时,最重要的考虑因素之一是尽可能保持其稳定性。这意味着要尽量减少随着时间的推移而发生的重大变更。如果确实存在重大变更,请务必编写详尽的升级指南,并在可能的情况下,提供一个代码模块,以便为用户自动化升级流程。

如果您要发布 API,请务必遵循语义化版本控制。这样,用户可以轻松决定需要哪个版本。

描述性错误消息

每当调用 API 时出现错误时,您都应该尽力解释错误的原因以及如何修复。在没有其他任何上下文的情况下,仅仅用“使用错误”的响应来羞辱用户,似乎并不是一个好的用户体验。

相反,编写描述性错误来帮助用户修复他们调用 API 的方式。

尽量减少开发人员的意外

开发人员很脆弱,您肯定不希望他们在使用 API 时感到惊慌。换句话说,让您的 API 尽可能直观。您可以通过遵循最佳实践和现有的命名约定来实现这一点。

另一件需要注意的事情是保持代码的一致性。如果你在一个地方添加了布尔属性名ishas然后在下一个地方省略了它,这会让读者感到困惑。

最小化 API 接口

既然我们说的是精简功能,那么 API 也同样要精简。功能丰富固然好,但 API 的功能越少,用户学习起来就越轻松。这样,用户就会觉得 API 用起来更简单!

总有办法控制 API 的大小 - 一种是从旧 API 中重构出新的 API。

十条戒律

JSX 的插图""/>

因此,这 4 条黄金法则对于 REST API 和 Pascal 中的旧程序内容非常有效 - 但它们如何转化为现代的 React 世界呢?

嗯,正如我们之前提到的,组件有它们自己的 API。我们称之为props,这就是我们向组件提供数据、回调和其他功能的方式。我们如何构建这个props对象才能不违反上述任何规则?我们如何编写组件,以便让下一个开发人员测试它们时能够轻松使用?

我们列出了创建组件时需要遵循的 10 条良好规则,希望它们对您有所帮助。

1.记录使用情况

如果你没有文档说明组件应该如何使用,那么它注定是无用的。好吧,差不多——用户总是可以查看具体实现,但这很少能带来最佳的用户体验。

记录组件的方法有很多种,但我们认为有 3 种方法值得推荐:

前两个为您提供了开发组件时可以使用的场地,而第三个让您可以使用MDX编写更多自由格式的文档。

无论你选择什么,务必记录 API 以及组件的使用方式和时间。最后一部分在共享组件库中至关重要——这样人们才能在特定的上下文中使用正确的按钮或布局网格。

2. 允许上下文语义

HTML 是一种以语义方式构建信息的语言。然而,我们的大多数组件都是由标签构成的<div />。这在某种程度上是有道理的,因为通用组件无法真正判断它应该是“<article />或”<section />还是“ <aside />-”,但这并不理想。

相反,我们建议你允许组件接受一个asprop,这样就能始终覆盖正在渲染的 DOM 元素。以下是实现方法的示例:

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

我们将asprop 重命名为局部变量Element,并在 JSX 中使用它。当您没有更语义化的 HTML 标签需要传递时,我们会提供一个通用的默认值。

当需要使用此<Grid />组件时,您只需传递正确的标签即可:

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

请注意,这同样适用于 React 组件。一个很好的例子是,如果你想让<Button />组件渲染 React Router <Link />

<Button as={Link} to="/profile">
  Go to Profile
</Button>
Enter fullscreen mode Exit fullscreen mode

3. 避免使用布尔值 props

布尔值 props 听起来是个好主意。你可以不指定值,所以看起来非常优雅:

<Button large>BUY NOW!</Button>
Enter fullscreen mode Exit fullscreen mode

但即使它们看起来很漂亮,布尔属性也只允许两种可能性。开或关。可见或隐藏。1 或 0。

每当您开始引入诸如大小、变体、颜色或任何可能不是二进制选择的东西的布尔属性时,您就会遇到麻烦。

<Button large small primary disabled secondary>
  WHAT AM I??
</Button>
Enter fullscreen mode Exit fullscreen mode

换句话说,布尔属性通常无法随着需求的变化而扩展。相反,尝试使用枚举值(例如字符串)来表示那些可能成为二进制选择以外的任何值。

<Button variant="primary" size="large">
  I am primarily a large button
</Button>
Enter fullscreen mode Exit fullscreen mode

这并不是说布尔属性没有用武之地。它们当然有用武之地!disabled我上面列出的 prop 仍然应该是布尔值——因为在启用和禁用之间没有中间状态。把它们留给真正的二元选择吧。

4.使用props.children

React 有一些特殊属性,它们的处理方式与其他属性不同。其中一个是key,用于跟踪列表项的顺序;另一个是children

任何在组件开始和结束标签之间放置的内容都会被放置在props.childrenprop 中。你应该尽可能多地使用它。

原因是它比使用content道具或其他通常只接受简单值(如文本)的东西更容易使用。

<TableCell content="Some text" />

// vs

<TableCell>Some text</TableCell>
Enter fullscreen mode Exit fullscreen mode

使用 有几个好处props.children。首先,它类似于常规 HTML 的工作方式。其次,你可以自由地传入任何你想要的内容!无需将leftIconrightIcon属性添加到组件中 - 只需将它们作为 属性的一部分传入即可props.children

<TableCell>
  <ImportantIcon /> Some text
</TableCell>
Enter fullscreen mode Exit fullscreen mode

您可能会争辩说,您的组件应该只允许渲染常规文本,在某些情况下确实如此。至少目前是这样。通过使用props.children,您可以让您的 API 适应这些不断变化的需求。

5. 让父母参与内部逻辑

有时我们会创建具有大量内部逻辑和状态的组件 - 例如自动完成下拉菜单或交互式图表。

这些类型的组件最常受到冗长的 API 的影响,原因之一是随着时间的推移,您通常必须支持大量的覆盖和特殊用法。

如果我们只提供一个单一的、标准化的道具,让消费者控制、反应或完全覆盖组件的默认行为,那会怎样?

Kent C. Dodds 写了一篇很棒的文章,探讨了“状态 reducer”这个概念。另外还有一篇关于这个概念本身的文章,以及一篇关于如何在 React Hooks 中实现它的文章

简单概括一下,这种将“状态 reducer”函数传递给组件的模式,可以让使用者访问组件内部分发的所有操作。你可以更改状态,甚至触发副作用。这是一种无需所有 props 即可实现高级自定义的好方法。

它看起来可能是这样的:

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>
    </>
}
Enter fullscreen mode Exit fullscreen mode

顺便说一句,你当然可以创建更简单的事件响应方式。onClose在上例中提供一个 prop 可能会带来更好的用户体验。保存状态 Reducer 模式,以便在需要时使用。

6. 散布剩余的道具

每当您创建新组件时,请确保将剩余的道具分散到有意义的元素上。

你无需不断地向组件添加 props,这些 props 只会传递给底层组件或元素。这将使你的 API 更加稳定,无需为了下一个开发者需要新的事件监听器或 aria-tag 而进行大量的小版本升级。

你可以这样做:

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

每当你的组件在实现中传递 props(例如类名或onClick处理程序)时,请确保外部使用者也能执行相同的操作。对于类,你可以简单地使用classnamesnpm 包(或简单的字符串连接)附加 class props:

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

对于点击处理程序和其他回调,你可以使用一个小工具将它们组合成一个函数。以下是其中一种做法:

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

这里,我们创建一个函数,它接受要组合的函数列表。它返回一个新的回调函数,该回调函数使用相同的参数依次调用所有函数。

你可以像这样使用它:

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)}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

7. 提供足够的默认值

尽可能为你的 props 提供足够的默认值。这样,你可以最大限度地减少需要传递的 props 数量,从而大大简化你的实现。

以处理程序为例onClick。如果您的代码中不需要它,请提供一个 noop-function 作为默认属性。这样,您就可以在代码中调用它,就像它始终存在一样。

另一个例子是自定义输入。除非明确指定,否则假设输入字符串为空字符串。这样可以确保始终处理的是字符串对象,而不是未定义或空的对象。

8. 不要重命名 HTML 属性

HTML 作为一种语言,自带 props(属性),它本身就是 HTML 元素的 API。为什么不继续使用这个 API 呢?

正如我们之前提到的,最小化 API 接口并使其更加直观是改进组件 API 的两个好方法。所以,与其创建自己的screenReaderLabelprop,为什么不直接使用aria-label组件提供的 API 呢?

所以,不要为了“方便使用”而重命名任何现有的 HTML 属性。你甚至没有用新的 API 替换现有的 API,而是在 API 之上添加了你自己的 API。人们仍然可以将aria-label你的screenReaderLabelprop 传递给它——那么最终的值应该是什么呢?

另外,请确保永远不要覆盖组件中的 HTML 属性。<button />元素的type属性就是一个很好的例子。它可以是submit(默认值),button或者reset。然而,很多开发者倾向于重新使用这个 prop 名称来表示按钮的视觉类型(primarycta等等)。

通过重新利用这个道具,您必须添加另一个覆盖来设置实际type属性,这只会导致混乱、怀疑和用户的不满。

相信我——我已经一次又一次地犯下这个错误——这是一个真正糟糕的决定。

9. 编写 prop 类型(或多个类型)

没有什么文档比代码本身的文档更好。React 包提供了一种非常棒的方式来声明你的组件 API prop-types。现在就去使用它吧。

您可以对必需和可选道具的形状和形式指定任何类型的要求,甚至可以使用JSDoc 注释进一步改进它。

如果您跳过必需的属性,或者传递了无效或意外的值,控制台中会显示运行时警告。这对于开发环境非常有用,并且可以从生产版本中移除。

如果您使用 TypeScript 或 Flow 编写 React 应用,则可以将此类 API 文档作为语言功能获得。这将带来更好的工具支持和出色的用户体验。

即使你自己不使用类型化 JavaScript,你仍然应该考虑为使用 JavaScript 的使用者提供类型定义。这样,他们就能更轻松地使用你的组件。

10. 为开发人员设计

最后,最重要的规则是:确保你的 API 和“组件体验”针对实际使用者(也就是你的开发者同事)进行了优化。

改善开发人员体验的一种方法是针对无效使用提供充足的错误消息,以及在有更好的方法使用组件时提供仅针对开发的警告。

编写错误和警告时,请务必提供文档链接或简单的代码示例。用户越快发现问题所在并知道如何修复,您的组件使用体验就越好。

事实证明,所有这些冗长的错误警告根本不会影响最终的打包大小。得益于死代码消除的神奇功能,所有这些文本和错误代码都可以在生产构建时删除。

React 本身就是一个在这方面做得非常好的库。每当你忘记为列表项指定键,或者拼写错误生命周期方法,忘记扩展正确的基类,或者以不确定的方式调用钩子时,你都会在控制台中看到粗体的错误信息。你的组件用户为什么要期待更少呢?

所以,为未来的用户设计吧!为五周后的自己设计吧!为那些在你离开后还要维护你代码的可怜虫设计吧!为开发者设计吧。

回顾

我们可以从经典的 API 设计中学习到很多宝贵的东西。通过遵循本文中的技巧、窍门、规则和戒律,你应该能够创建易于使用、易于维护、直观易用且在需要时极其灵活的组件。

您最喜欢的创建酷炫组件的技巧有哪些?

文章来源:https://dev.to/selbekk/the-10-component-commandments-2a7f
PREV
更高效使用 VSCode 的技巧
NEXT
27 行代码实现 Redux