赞
踩
诚然,想要创建一个供多人使用的组件绝非易事,组件包含属性(props),如果这些属性要作为公开 API 的一部分,那就必须非常仔细地考虑组件应该接受哪些属性。
本文会简要介绍 API 设计中的一些最佳实践,同时还总结了开发优秀组件的10条准则。希望这些准则能够对你有所帮助。
API (Application Programing Interface,应用编程接口)是两段代码交互的地方。它是代码与世界沟通的桥梁,我们将这个桥梁称为接口。可以通过 API 进行数据与功能的交互。
后端和前端之间的接口是一个 API。 可以通过与这个 API 进行交互来访问一组数据和功能。
一个类和调用这个类的代码之间的接口同样也是一个 API。你可以调用类里的方法,来检索数据或触发封装在里面的功能。
同理,组件中要接收的 props 同样也是 API 。这是调用者与组件交互的方式,当你想要对外暴露什么的时候,会应用很多相同的规则和注意事项。
那么,在设计 API 的时候,需要注意哪些规则和事项呢?我们在这方面做了一些研究,最后找到了许多非常优秀的资源。我们选取了其中的 2 篇:Josh Tauberer 的《What Makes a Good API ?》以及 Ron Kurir 的同名文章;并且也提出了4 个可遵循的最佳实践。
当创建一个 API 的时候,需要考虑的最重要的一件事是尽可能地保持 API 的稳定。这意味着需要最大限度地减少重大变化的数量。如果 API 真的有较大的变化,也请确保撰写了详细的升级指南,并尽可能提供一份代码模块,可以让用户自动完成升级过程。
如果正在发布 API,请确保遵循了语义版本规范,可以让用户轻松地决定所需的版本。
每当调用 API 发生错误时,你需要尽可能地去解释发生了什么问题,以及如何去修复错误。在没有任何提醒或信息的情况下,直接抛出一个“错误使用”来羞辱调用者貌似不是一种良好的用户体验。
相反,撰写描述性错误信息可以帮助调用者来修改他们调用 API 的方式。
程序猿是脆弱的,而且你也不希望他们在使用 API 的时候来个 surprise,然后把人家吓出个好歹来。也就是说,API 应该尽可能直观。可以通过遵循最佳实践和现有的命名习惯来实现这一点。
另外还需要注意一点:保持代码风格的连贯。如果在布尔属性的名称前加上了 is 或者 has 作为前缀,接下来却又不这么做了,就会让人感到费解。
当我们在讨论做减法的时候,同样也包括减少 API 。功能多了固然很好,但是 API 的结构越简单,调用者的学习成本就越小。反过来讲,这会被认为是一个简单易用的 API 。
总有办法来控制 API 的大小,其中的一个办法是,从旧的 API 中重构出一个新的 API。
上面的 4 条黄金法则在编写 REST API 以及古老的 Pascal 程序时很管用,那应该如何转化才能适用于现代世界的 React 呢?
正如我们前面所提到的,组件有自己的 API。我们称其为 属性(props),这是我们提供给组件数据、回调函数以及其他功能的方式。我们应该以何种方式构建props对象,才能够不违反上述任何一条规范呢?我们同样应该以何种方式编写组件,才能让下一个开发者轻松地测试它们呢?
以下列出了创建组件时需要遵循的 10 条准则,并且希望你能发现它们是行之有效的。
如果没有文档来记录组件是如何使用的,好吧,虽然大多数开发者会随时查看你的代码,但这不能说是一种良好的用户体验。
写文档有很多工具,我们推荐以下 3 个:
前两个会在开发组件的时候提供一个演练场,第三个则会让你使用 MDX 编写更多自由格式的文档。
无论选择哪个,都请确保在文档中记录了 API 的用法 ,以及组件的用法和使用时机。 后者在共享组件库中尤为重要。
HTML 是一种通过语义化的方式来组织信息的语言。大多数组件是使用
标签来构建的。这在某种程度上是有道理的——因为通用组件不清楚它到底应该是一个 还是 或者是 ,尽管如此,但只用来构建也并不完美。
相反,我们建议允许组件接受一个 as 属性,将始终覆盖正在呈现的DOM 元素。下面是一个实现用例:
- function Grid({ as: Element, ...props }) {
- return <Element className="grid" {...props} />
- }
- Grid.defaultProps = {
- as: 'div',
- };
我们将 as 属性重命名为局部变量 Element,并在 JSX 中使用。当不需要更多语义化的 HTML 标签时,我们也提供了普通的默认值来传给组件。
当使用 组件的时候,你可以传入合适的标签:
- function App() {
- return (
- <Grid as="main">
- <MoreContent />
- </Grid>
- );
- }
请注意上面的代码在 React 组件中同样有效。下面是一个很好的例子,展示了如果想让一个组件呈现一个 React Router 。
- <Button as={Link} to="/profile">
- Go to Profile
- </Button>
布尔属性听起来不错,你不需要给它赋值就可以指定一个布尔属性,所以看起来非常优雅:
<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 属性应当依旧是布尔类型——因为在“可用”与“不可用”的状态之间,不存在中间状态,所以这里用布尔类型是恰当的。
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>
有时,我们会创建一些内部逻辑复杂的组件,比如自动补全的下拉菜单或者可交互图表。
这种类型的组件通常会有冗长复杂的 API ,其中原因是,需要覆盖的功能以及需要支持的特殊用法,这两者的数量都会随着时间的推移而不断增加。
如果我们想提供一个简单且标准化的属性,来让调用者去控制或者覆盖组件的默认行为,我们应该怎么做呢?
Kent C. Dodds 为此写过一篇很棒的文章,在文中他将这个问题的解决方案称为:state reducers。请参阅这两篇文章:Post about the concept itself 和 How 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 属性会产生更好的用户体验。
每当创建一个新的组件时,请确保将剩余属性也扩散到有意义的元素上。
如果有某些属性仅需传递给子组件或子元素(而组件自身并不需要这个属性),那就不必添加到你的组件中,这么做可以让组件 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)}
- />
- );
- }
-
请确保为属性提供了充分的默认值,这样做可以最大限度地减少必传值的数量,而且也大大简化了代码实现。
以 onClick 处理函数为例,如果它不是必需的,就可以提供一个空函数来作为默认值。这样,你就可以在代码中随时调用它,就好像组件总是被提供了回调函数一样。
另一个例子是自定义输入。 除非明确提供,否则假设输入的字符串是空字符串。 这将使你确保始终处理字符串对象,而不是 undefined 或 null 。
HTML 作为一门语言拥有自己的属性,它本身就是 HTML 元素的 API,为啥不继续使用这些 API?
正如前面所提到的,精简 API 数量并使其具有一定的直观性是改进组件API的两种很好的方法。所以与其创建自己的自定义标签属性,为什么不直接使用现成的原生标签 API 呢?
因此,不要重命名任何现有 HTML 属性。甚至没有用新的 API 替换现有的 API,你只是在上面添加了自己的 API。其他人仍然可以将原生标签与你自定义标签的属性一起传递,那么最终的值应该是什么呢?
另外,也请确保在组件中没有将 HTML 属性进行覆盖。一个很好的例子就是
元素的 type 属性。它的值可以是 submit (默认值)、button 和 reset。但是,许多开发者却倾向于将这个属性名用于表示按钮的可视类型(primary,cta 等等)。
通过改变这个属性的用途,你不得不添加其他属性来覆盖,设置实际的 type 属性值,这只会给调用者带来困惑。
没有什么文档比代码中的文档更好了,React 提供了 prop-types 包来声明组件 API 的类型,一定要用它。
你可以为所有的属性指定明确的类型,也可以规定属性是否必传,甚至可以使用 JSDoc 注释来做进一步改进。
如果忽略了必传属性,或者传递了无效值和意外值,运行时会在控制台打印警告。这样的开发体验很棒,并且可以从生产构建中剥离出来。
如果使用 TypeScript 或者 Flow 来编写 React 应用,就可以将此类API文档作为语言功能。这会带来更好的工具支持以及更棒的开发体验。
如果你自己没有使用类型化的 JavaScript,仍然应该考虑为那些使用类型化的 JavaScript 调用者提供类型定义。通过这种方式,他们能够更轻松地使用你的组件。
最后,需要遵循的最重要一条规则就是:保证 API 以及“组件体验”已经针对它的调用者进行了优化。
提升开发者体验的一个办法是在错误调用时提供详细的错误信息,以及在开发过程中的警告信息。
当编写错误和警告时,记得使用链接引用文档或提供简单的代码示例。这可以帮助开发者更快地发现错误并修改,提供更好的开发体验。
不必担心过于冗长的错误信息会占用太大空间,况且构建生产环境的时候也不会把这些信息打包进去。
React 本身就是一个非常优秀的类库,当你忘记使用 key 或者拼错了生命周期的名字等等,都会在控制台收到大量详细的错误警告信息。
因此,要为你未来的用户设计,在 5 周内为自己设计,为那些在你离开后必须维护你代码的可怜兄 dei 设计,为开发者设计!
在经典的 API 设计中我们还可以学到很多优秀的东西,通过遵循本文中提到的提示和准则,你应该可以创建出易于使用、便于维护、使用直观,以及出现问题时可以进行快速修复的组件。 你还有哪些关于创建组件的点子?
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。