十条戒律
与Caroline Odden合作撰写。基于 2019 年 6 月在 ReactJS Oslo Meetup 上举行的同名、同人员的演讲。
创建大量用户使用的组件并非易事。如果组件的 props 需要作为公共 API 的一部分,那么你必须仔细考虑应该接受哪些 props。
本文将向您简要介绍 API 设计中的一些最佳实践,以及可用于创建您的其他开发人员喜欢使用的组件的 10 条实用戒律的明确列表。
什么是 API?
API(应用程序编程接口)本质上是两段代码的交汇点。它是代码与外界交互的接触面。我们称这个接触面为接口。它是一组定义好的、可供您交互的操作或数据点。
后端和前端之间的接口是 API。您可以通过与此 API 交互来访问给定的数据集和功能。
类与调用该类的代码之间的接口也是一种 API。您可以调用类上的方法来检索数据或触发封装在其中的功能。
按照同样的思路,组件接受的 props 也是它的 API。它是用户与组件交互的方式,在决定暴露什么内容时,许多相同的规则和注意事项也适用。
API设计中的一些最佳实践
那么,设计 API 时应该遵循哪些规则和注意事项呢?我们对此进行了一些研究,发现有很多很棒的资源。我们挑选了两篇——Josh Tauberer的《什么是好的 API?》和Ron Kurir 的同名文章——并总结了 4 条最佳实践供大家参考。
稳定版本
创建 API 时,最重要的考虑因素之一是尽可能保持其稳定性。这意味着要尽量减少随着时间的推移而发生的重大变更。如果确实存在重大变更,请务必编写详尽的升级指南,并在可能的情况下,提供一个代码模块,以便为用户自动化升级流程。
如果您要发布 API,请务必遵循语义化版本控制。这样,用户可以轻松决定需要哪个版本。
描述性错误消息
每当调用 API 时出现错误时,您都应该尽力解释错误的原因以及如何修复。在没有其他任何上下文的情况下,仅仅用“使用错误”的响应来羞辱用户,似乎并不是一个好的用户体验。
相反,编写描述性错误来帮助用户修复他们调用 API 的方式。
尽量减少开发人员的意外
开发人员很脆弱,您肯定不希望他们在使用 API 时感到惊慌。换句话说,让您的 API 尽可能直观。您可以通过遵循最佳实践和现有的命名约定来实现这一点。
另一件需要注意的事情是保持代码的一致性。如果你在一个地方添加了布尔属性名is
,has
然后在下一个地方省略了它,这会让读者感到困惑。
最小化 API 接口
既然我们说的是精简功能,那么 API 也同样要精简。功能丰富固然好,但 API 的功能越少,用户学习起来就越轻松。这样,用户就会觉得 API 用起来更简单!
总有办法控制 API 的大小 - 一种是从旧 API 中重构出新的 API。
十条戒律
因此,这 4 条黄金法则对于 REST API 和 Pascal 中的旧程序内容非常有效 - 但它们如何转化为现代的 React 世界呢?
嗯,正如我们之前提到的,组件有它们自己的 API。我们称之为props
,这就是我们向组件提供数据、回调和其他功能的方式。我们如何构建这个props
对象才能不违反上述任何规则?我们如何编写组件,以便让下一个开发人员测试它们时能够轻松使用?
我们列出了创建组件时需要遵循的 10 条良好规则,希望它们对您有所帮助。
1.记录使用情况
如果你没有文档说明组件应该如何使用,那么它注定是无用的。好吧,差不多——用户总是可以查看具体实现,但这很少能带来最佳的用户体验。
记录组件的方法有很多种,但我们认为有 3 种方法值得推荐:
前两个为您提供了开发组件时可以使用的场地,而第三个让您可以使用MDX编写更多自由格式的文档。
无论你选择什么,务必记录 API 以及组件的使用方式和时间。最后一部分在共享组件库中至关重要——这样人们才能在特定的上下文中使用正确的按钮或布局网格。
2. 允许上下文语义
HTML 是一种以语义方式构建信息的语言。然而,我们的大多数组件都是由标签构成的<div />
。这在某种程度上是有道理的,因为通用组件无法真正判断它应该是“<article />
或”<section />
还是“ <aside />
-”,但这并不理想。
相反,我们建议你允许组件接受一个as
prop,这样就能始终覆盖正在渲染的 DOM 元素。以下是实现方法的示例:
function Grid({ as: Element, ...props }) {
return <Element className="grid" {...props} />
}
Grid.defaultProps = {
as: 'div',
};
我们将as
prop 重命名为局部变量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. 避免使用布尔值 props
布尔值 props 听起来是个好主意。你可以不指定值,所以看起来非常优雅:
<Button large>BUY NOW!</Button>
但即使它们看起来很漂亮,布尔属性也只允许两种可能性。开或关。可见或隐藏。1 或 0。
每当您开始引入诸如大小、变体、颜色或任何可能不是二进制选择的东西的布尔属性时,您就会遇到麻烦。
<Button large small primary disabled secondary>
WHAT AM I??
</Button>
换句话说,布尔属性通常无法随着需求的变化而扩展。相反,尝试使用枚举值(例如字符串)来表示那些可能成为二进制选择以外的任何值。
<Button variant="primary" size="large">
I am primarily a large button
</Button>
这并不是说布尔属性没有用武之地。它们当然有用武之地!disabled
我上面列出的 prop 仍然应该是布尔值——因为在启用和禁用之间没有中间状态。把它们留给真正的二元选择吧。
4.使用props.children
React 有一些特殊属性,它们的处理方式与其他属性不同。其中一个是key
,用于跟踪列表项的顺序;另一个是children
。
任何在组件开始和结束标签之间放置的内容都会被放置在props.children
prop 中。你应该尽可能多地使用它。
原因是它比使用content
道具或其他通常只接受简单值(如文本)的东西更容易使用。
<TableCell content="Some text" />
// vs
<TableCell>Some text</TableCell>
使用 有几个好处props.children
。首先,它类似于常规 HTML 的工作方式。其次,你可以自由地传入任何你想要的内容!无需将leftIcon
和rightIcon
属性添加到组件中 - 只需将它们作为 属性的一部分传入即可props.children
:
<TableCell>
<ImportantIcon /> Some text
</TableCell>
您可能会争辩说,您的组件应该只允许渲染常规文本,在某些情况下确实如此。至少目前是这样。通过使用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>
</>
}
顺便说一句,你当然可以创建更简单的事件响应方式。onClose
在上例中提供一个 prop 可能会带来更好的用户体验。保存状态 Reducer 模式,以便在需要时使用。
6. 散布剩余的道具
每当您创建新组件时,请确保将剩余的道具分散到有意义的元素上。
你无需不断地向组件添加 props,这些 props 只会传递给底层组件或元素。这将使你的 API 更加稳定,无需为了下一个开发者需要新的事件监听器或 aria-tag 而进行大量的小版本升级。
你可以这样做:
function ToolTip({ isVisible, ...rest }) {
return isVisible ? <span role="tooltip" {...rest} /> : null;
}
每当你的组件在实现中传递 props(例如类名或onClick
处理程序)时,请确保外部使用者也能执行相同的操作。对于类,你可以简单地使用classnames
npm 包(或简单的字符串连接)附加 class props:
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. 提供足够的默认值
尽可能为你的 props 提供足够的默认值。这样,你可以最大限度地减少需要传递的 props 数量,从而大大简化你的实现。
以处理程序为例onClick
。如果您的代码中不需要它,请提供一个 noop-function 作为默认属性。这样,您就可以在代码中调用它,就像它始终存在一样。
另一个例子是自定义输入。除非明确指定,否则假设输入字符串为空字符串。这样可以确保始终处理的是字符串对象,而不是未定义或空的对象。
8. 不要重命名 HTML 属性
HTML 作为一种语言,自带 props(属性),它本身就是 HTML 元素的 API。为什么不继续使用这个 API 呢?
正如我们之前提到的,最小化 API 接口并使其更加直观是改进组件 API 的两个好方法。所以,与其创建自己的screenReaderLabel
prop,为什么不直接使用aria-label
组件提供的 API 呢?
所以,不要为了“方便使用”而重命名任何现有的 HTML 属性。你甚至没有用新的 API 替换现有的 API,而是在 API 之上添加了你自己的 API。人们仍然可以将aria-label
你的screenReaderLabel
prop 传递给它——那么最终的值应该是什么呢?
另外,请确保永远不要覆盖组件中的 HTML 属性。<button />
元素的type
属性就是一个很好的例子。它可以是submit
(默认值),button
或者reset
。然而,很多开发者倾向于重新使用这个 prop 名称来表示按钮的视觉类型(primary
,cta
等等)。
通过重新利用这个道具,您必须添加另一个覆盖来设置实际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