书格前端

React组件开发的十条最佳实践


React组件开发的十条最佳实践

原文地址: https://dev.to/selbekk/the-10-component-commandments-2a7f 译文地址: https://blog.bookcell.org/2020/03/22/the-10-react-component-best-practice/

译者备注: 这是一篇关于React组件开发最佳实践的文章,很值得一读,推荐有能力阅读英文的同学去读原文,翻译或多或少会丢失一些原意。

正文从这里开始:

创建被许多人使用的组件是困难的。尤其当组件的属性作为公开API的一部分,你不得不仔细考虑哪些属性应该接受。

这篇文章会快速介绍一些在API设计时的通用最佳实践,并且给出10条明确的最佳实践指导你创建让同事开发者喜欢使用的组件。

image

API是什么?

API或者应用程序接口(Application Programming Interface),主要是指两部分代码相遇的地方。这是你的代码和其他世界接触的地方。我们称这个接触表面为接口。这些是可交互的动作集合或者数据点。

介于前端和后端之间的接口就是一个API。你可以通过这个API访问一系列特定的数据和功能。

介于一个类和调用代码之间接口也是一个API。你可以在类上调用方法,来获取数据或者触发封装在其中的功能。

沿着这个思路,你的组件接受的属性也是其API。这是你的用户和组件交互的方式,因此当你决定暴露哪些属性时有很多类似的规则和考虑可以应用。

API设计的部分最佳实践

那么在设计一个API时有哪些规则和考虑可以应用?在这方面我们做了一些调查,并找到了许多极好的资源。我们挑选了两篇文章,Josh Tauberer“What Makes a Good API?”和Ron Kurir的同名文章,从中提取了4条最佳实践来遵循。

稳定的版本管理

当你在创建一个API时要考虑的最重要的一点是尽可能保持稳定。这意味着破坏性变更的次数很少。如果你有破坏性的变更,确保写一份详细的升级指南,并且如果可能,提供一个重构件(code-mod)来给用户自动化处理这些变更。

如果你发布了API,确保遵守语义化版本。这让用户更容易决定使用什么版本。

描述性的错误信息

任何调用API出错的时候,你都应该尽可能的解释什么地方出错,并告知如何修复。返回一个错误使用且没有任何其他提示的响应会让使用者感到羞愧,这不是一种好的用户体验。

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

少让开发者感到惊讶

开发者是脆弱的人类,因此当他们使用API时不应该让他们感到惊讶。换句话说,尽可能让API更直观。这可以通过遵守最佳实践和命名规范来达到。

另外需要放在心上是保持你的代码一致性。如果你在一处地方给布尔型属性名称前面添加了ishas,而在另外一处忽略了,这会让其他人感到困惑。

最小化API接口

当我们谈到最小化的时候-同样要最小化你的API。更多的特性当然是好的,但是API接口暴露得越少,使用者学习的成本也更低。从而让用户认为这是一个易用的API。

有很多方式可以控制API的规模,其中一个就是从旧的中重构一个新的来。

组件开发的十条最佳实践

image

这4条黄金法则在REST API和Pascal语言程序中使用得很好,那么如何将他们拿到React的现代世界中来呢?

像我们前面提到的,组件也有自己的API。我们称做props,这是给组件传递数据、回调和其他功能的方式。我们如何组织props对象才能不破坏上面的规则?我们如何开发组件才能让其他开发者在使用组件时更便捷?

我们创建了开发组件时的10条不错的规则清单,希望对你有帮助。

1. 组件使用文档

如果组件没有提供如何使用的文档,那么显然组件是无用的。好吧,大多时候,使用者总是可以通过查看实现来了解如何使用,但那很少是最好的用户体验。

有很多方式可以给组件编写文档,从我们的角度想推荐3个选项:

前两个在你开发组件时可以提供一个playground用来试验,第3个提供了MDX来更自由的书写文档。(注:最新版本三者都已支持Markdown语法)

不管选择哪一个,确保提供API文档,及组件如何、何时使用相关的文档。后者在共享组件库中更为重要,那样人们才能在合适的位置使用正确的按钮或者布局。

2. 允许上下文的语义

译者注: 标题的原文是Allow for contextual semantics,中文翻译不好把握供参考

HTML是一门以语义化方式组织信息的语言。但是大多数的组件是由<div />标签组成的。这在某种程度上是讲得通的,因为通用组件不能假设是否应该是<article /><section />或者一个<aside />,但是这并不是理想。

相反,我们建议允许组件接受一个as属性,用以覆盖被渲染的DOM元素。下面是一个如何实现的例子:

function Grid({ as: Element, ...props }) {
    return <Eelement 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>

但是即使他们看起来很不错,单布尔属性只能允许两种可能性。开和关,显示和隐藏,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属性就应该是布尔型,因为在启用和禁用之间没有中间状态。保留他们作为真正的两个选项使用。

4. 使用props.children

React有一些与其他属性略有不同的特殊属性。一个是key,在列表项中被用来跟踪顺序,另一个是children

任何放置在组件开和闭标签间的内容都会被放在props.children属性中。因此,你应该尽可能经常使用。

原因是这样做比通过添加一个content属性或者其他专门类似文本简单值属性的方式要更加容易使用。

<TableCell content="Some text" />

// 对比

<TableCell>Some text</TableCell>

使用props.children有很多积极的意义。第一点,这有点类似平常HTML的工作方式。第二,你可以自由地传递任何你想要传的内容。不用添加leftIconrightIcon属性到你的组件中,仅仅只要传到props.children属性中即可。

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

你可能会争辩组件应该只允许接收并渲染普通文本,这在某些情况下可能是对的。至少现在,通过使用props.children,能实现一个适应未来需求变更的组件。

5. 让父组件接入内部逻辑

有时我们会创建有很多内部逻辑和状态的组件,例如自动补全的下拉组件或者交互式图表。

这些类型的组件通常都会涉及比较繁琐的接口,其中一个原因就是要支持后续的一系列覆盖和特殊使用场景。

如何能做到仅仅提供一个简单、标准化属性来让用户控制、响应或者覆盖默认组件行为呢?

Kent C.Dodds写了一篇关于“state reducers”概念的好文章,关于概念本身,和另外一篇如何用React Hooks实现

快速总结一下,这个模式通过传递一个“state reducer”函数给组件,从而让父组件可以访问任何在你的组件中派发的action。你可以改变状态,或者触发边界效应等。这是一个极好的实现高级定制的方式,而不用借助其他属性。

代码如下:

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属性可能会更好的用户体验。保留好state reducer模式以备不时之需。

6. 展开剩余属性

任何时候当你创建一个新的组件时,确保展开剩余的属性到有意义的元素上。

你不需要持续添加那些原本在父组件或父元素上并会传递到组件上的属性到你的组件中。这会让你的API更稳定,并且不会因其他开发者需要一个新的事件监听或者arial标签而发布许多小的版本。

可以像下面这样做:

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

任何时候你的组件传递一个属性到你的实现中,像一个类名或者一个onClick处理函数,确保外部的使用者同样可以做同样的事情。在类的情况,使用好用的classnames软件包来追加类名(或者使用字符串拼接)。

import classNames from 'classnames';

function ToolTip(props) {
    return (
        <span
            {...prpos}
            className={classNames('tooltip', props.className)}
        />
    )
}

对于点击事件处理函数或者其他回调,通过一个辅助函数来组合一个单独的函数,这里有一种实现方式:

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={combile(() => setVisible(true), props.onMouseIn)}
            onMouseOut={combile(() => setVisible(false), props.onMouseOut)}
        />
    );
}

7. 设置足够的默认值

无论何时如果可以的话,确保给你的属性提供足够的默认值。这样做的话,可以最小化必须要传递的属性数量,并且这样能非常简化你的实现。

举一个onClick处理函数的例子。如果在你的代码中不强制要求,那么可以提供一个空函数作为默认属性。这样的话,你可以在代码中跟一直有传递对应属性一样调用。

另一个例子例如定制的输入。除非用户提供,否则的话假设输入的字符串是一个空字符串。这可以确保总是在处理一个字符串对象,而不是undefined或null。

8. 不要重命名HTML属性

HTML作为一门语言同样有其自己的属性,而这就是HTML元素自身的API。为什么不继续保持使用这个API呢?

就如我们之前提到的,最小化API接口暴露和保持某种程度的直观是两种改善组件API的极好方式。因此比起创建自己的screenReaderLabel属性,为何不直接使用HTML已经提供给你的aria-label呢?

因此不要为了自己的方便使用而去重命名既有的HTML属性。这样并不是用一个新的API来替换既存的API,而是在顶层添加一个自己的。人们通常还可以继续和你的screenReaderLabel属性一起传递aria-label,那么最终哪个才是该使用的呢?

再说一点,确保永远不要在组件中覆盖HTML属性。一个比较好的例子就是<button />元素的type属性,具有submit(默认)、button或者reset。然而,许多开发者倾向于把这个属性用于表示按钮的可视化类型(如primary, cta等等)。

属性他用之后,你必须要添加其他新的属性来覆盖表示type属性,这样会导致困惑、疑虑和让用户愤怒。

相信我,我一次又一次的犯了这个错误,我承认这真是一个狼狈的决定。

9. 属性类型定义

没有文档能比在代码中的文档更好的。React通过prop-types软件包提供了极好的方式来声明组件API。现在就开始使用吧。

你可以指定任意类型和形式的必选和可选属性,并可以通过JSDoc注释来改善。

如果你忽略了一个必选的属性,或者传递一个无效、非期望的值,那么你会在控制台中得到运行时警告。这对开发来讲是极好的,并可以在生产打包时被删掉。

如果你是通过TypeScript或Flow来开发React应用,那么你可以通过语言特性获得这种API文档。这可以获得更好的工具支持,和更好的用户体验。

如果你自己没有使用具有类型的JavaScript,那么你应该始终考虑给你的用户提供类型定义。这样,他们在使用你的组件时会更加容易。

10. 为开发者设计

最后,最重要的一个规则。确保你的API和组件体验是为那些将会使用的人优化的—你的同事开发者。

一种改善开发者体验的方式是为不合理使用提供足够的错误信息,并在有更好的方式使用组件的情况下提供仅在开发环境的警告。

当在提供错误和警告时,尽量通过链接引用你的文档或者提供简单的代码示例。让用户越快找到错误并修复,这会让用户感到你的组件越好用。

事实证明,这些冗长的错误和警告并不会影响最终打包的大小。感谢无用代码精简的帮助,在构建生产包的时候这些文本和错误的代码都会被移除。

在这方面做得非常好的一个库就是React本身。任何时候当你忘记在列表项中指定一个key时,或者拼错一个生命周期函数,忘记继承正确的基类或者用不正确的方式调用hook时,你会在控制台中得到大量的错误信息。为什么你的组件使用人员要期望的更少呢?

因此为你的未来用户设计,为5周后的你自己设计,为当你离开后必须要接手维护你代码的可怜家伙设计!为开发者设计。

总结

从经典的API设计中我们可以学到许多很好的建议。通过遵循文中的建议、技巧、规则和最佳实践,你应该可以创建简单易用,容易维护,直观并在需要的时候非常灵活的组件。

那么你在创建一个出色的组件时有哪些最喜欢的建议?