文章正文

编写高质量可维护的代码:组件的抽象与粒度

2020-11-30 发布于 · 阅读量:499

前言

作为一名精致的前端猪猪女孩,也有那么点想让自己的代码同样看起来精致一点。所以在拿到新需求的 UI 设计稿时,经常会面临如下问题:如何拆解页面?如何划分组件才算是合理?好像用于组件拆分的 A 方案和 B 方案在当前业务场景下也都还算合理,那究竟要怎么选择?组件的抽象与粒度貌似是一个老生常谈的问题了~学习了很多前辈的文章,那么今天结合业务场景,也来讲下我的心得~

什么是组件

React 官方文档上说:组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。

Vue 官方文档说:组件是可复用的 Vue 实例,且带有一个名字。我们可以在一个通过 new Vue 创建的 Vue 根实例中,把这个组件作为自定义元素来使用。

其实总的来说,无论什么语言框架,组件就是一段代码片段,它可以实现某些指定的功能或渲染特定的展示效果,我们一般可以通过 import 的方式将其引入到项目代码中。本文接下来将以 React 为例进行相关描述。

组件的抽象

组件抽象的过程就是将通用性代码“提取”或是“抽取”出去的过程,那么问题来了,我们为什么要抽组件呢?

为什么要抽组件

说到为什么要抽取组件,不知道各位读者有没有遇到过一个 js 文件中有 1k+ ~ 2k+ 行 React 代码,甚至更多行代码的情况。这种情况往往导致代码难以维护,当有新的需求涉及相关改动时,在一定程度上增加了代码的学习成本(特别是当你刚刚新接手了一份完全不熟悉的项目的时候)。

其次,某些情况下,有一部分代码在不同场景下其实是可以复用的,例如新增和编辑的弹窗,可能只有弹窗的标题和某些字段有部分差异,此时没必要把高度相似的代码复制两遍,增加代码的冗余。

图片

因此,在我们日常开发中,组件抽取是有必要的,其目的在于代码的分层复用,降低项目的复杂度。

组件抽象的基本原则

单一性

单一性要求一个组件具有高内聚,低耦合的特征,它只负责一件事情,不要耦合一些没必要的逻辑,并且尽量不要和其他组件有过于多的双向交互和互相依赖关系。单一性并不代表着不可以引用其他组件,当前组件可能是外层的容器组件,里面包含一些子组件,这样的设计是没问题的。

复用性/通用性

在设计组件的时候,一定要考虑组件的复用性或者说是通用性。这是指,当组件封装好后,可以在类似的使用场景中直接调用。这要求我们在设计组件的时候,考虑组件功能的通用性,以及考虑组件入参的合理性。

此时有两种情况:

一种是很多不同的项目间,可能存在类似的使用场景,因此会提炼出一个公共的组件,为了复用。一般我们称之为基础组件或业务组件,姑且叫它公共组件吧。

另一种是在项目内部,仅在当前场景下作为一个独立的模块可以抽取出来作为一个组件,暂时称之为项目组件。

公共组件和项目组件在设计上的侧重也有所不同,公共组件要更多的考虑通用性,通过一个组件满足不同项目中相似的使用场景,比如 AntD 基础组件库。而项目组件更多的是处理当前业务中的特殊场景,可能是页面拆解后的不同模块,也可能是不同操作的弹窗,往往这种组件不适合直接“移植”到其他项目中使用。

然鹅,对于一个组件来说,个人认为也不能一味的追求通用性使其变得难以维护。例如,当遇到下述页面的时候,要如何抽象组件呢?

图片

不难发现,页面中交易方式、基础配置和合同设置这三个模块其实是具有一定共性的,全部呈现为列表形式,只是在某些列上有展示差异。前辈的做法是,考虑了所有情况,抽象成一个组件。通过 title 区分模块名称,由于仅在交易方式模块有操作列,因此通过 areaCode 区分当前页面下的不同模块等。

<TableConfiguration
  // 基本参数
  title="基础配置" // 标题名称
  data={baseSettingData} // 展示数据
  areaCode="baseSettingConfig" // 模块 code
  config={baseSettingConfig} // 一些业务逻辑参数
  // 新增参数
  pageId={this.pageId} // 当前页面 Id
  userIdentity={userIdentity} // 用户身份
/>

在业务发展前期,这样抽取的组件的确使用起来很方便,且通用性很强。但随着业务的膨胀,同一项目中不同页面开始出现相类似的模块,于是新增 pageId 标识,用于区分不同的页面以及对应页面的特殊逻辑。又过了一段时间,新增 userIdentity 标识,用于控制不同登录用户对页面的查看或操作权限。

长此以往,新增的参数越来越多,组件内部开始出现大量的判断逻辑,尽管这个组件通用性很好,能应对各种页面展示逻辑,但这也使它本身变得逐渐难以维护。还有一种比较好的解决方案是通过表单中心生成一份这样的页面,可参考动态化表单设计

分离处理

师父曾教导我说抽组件最好做一下业务层和视图层的分离处理,其中视图层主要负责页面展示样式和交互,业务层主要负责处理业务逻辑,比如接口调用,数据结构调整等。这样做的好处除了职责分离,还可以有效提高组件性能(比如视图层可以用 PureComponent 处理)。

另外,例如上述的新增和编辑弹窗,当新增和编辑两个操作需要分别调用不同接口时,业务层和视图层的分离处理可以避免组件中耦合对“新增”或“编辑”的判断,它们可以共用一个视图,并在各自的业务层实现不同的业务逻辑。

组件分类

业务组件 vs UI 组件

业务组件侧重于数据和业务的逻辑处理,其中数据一般通过接口获取。目前本团队维护的业务组件库,可以使开发人员即来即用,组件内部有完善的功能和接口数据处理,将组件引入到项目后可直接实现对应功能。

UI 组件一般也可以称为基础组件,它们经常在多个地方被复用,且不耦合任何的业务功能,例如:AntD 组件库。UI 组件侧重于页面展示效果,大部分 UI 组件具有原子性,一些复杂的 UI 组件可以由基本的 UI 组件构成。一般情况下组件内部的数据来源于父组件传递过来的 props。

纯组件 vs 非纯组件

有一天,我看到前辈大神这么写的代码

export default class NotFound extends PureComponent {
  // 此处省略具体代码
}

于是去学习了下纯组件和非纯组件的区别,首先让我们了解下React 中的各种组件一文中对 React 组件重新渲染机制的描述:

一般当一个组件的 props (属性)或者 state (状态)发生改变的时候,也就是父组件传递进来的 props 发生变化或者使用 this.setState 函数时,组件都会进行重新渲染。

而在接受到新的 props 或者 state 到组件更新之间,其实会执行生命周期中的一个方法 shouldComponentUpdate,当该方法返回 true 时才会进行重渲染,如果返回 false 则不会进行重渲染。

纯组件和非纯组件的区别在于,一般情况下非纯组件并未自动实现 shouldComponentUpdate 方法的功能(但可以手动调用这个钩子),而纯组件中利用 shallowEqual 的方法对 props 和 state 做浅比较实现了该功能。实际应用中,纯组件一般用于纯展示型组件,相对于非纯组件来说,减少了手动判断 props 或者 state 变化的繁琐操作。并且,纯组件可以通过减少 render 调用次数来降低性能损耗,但是使用过程中也一定要确保此类组件的渲染仅取决于 props 与 state。

非纯组件的话,其实我们日常开发中比较常用。一般情况下,在不做特殊处理时,正常 extends Component 出来的组件都可以认为是非纯组件。

export default class MyComponent extends Component {}

我们可以根据实际的开发场景选择继承自 PureComponent 还是 Component。值得注意的是,由于纯组件中做的是浅比较,因此带有深层嵌套的数据是对比不出来的,请慎用~

组件的粒度

提到组件的粒度,大多数人的第一反应可能认为拆分的越细越好。但是,这样一定是最优解嘛?个人认为其实不是的。

组件拆解的过于细致可能导致某些参数从父组件开始一层层向子组件传递,容易漏传,错传,或者其中某层组件忘记判空的时候,可能会导致页面报错。虽然可以通过 React Context 去获取,不过好像还是“徒手传递”的人更多一点。但组件如果拆解的太粗略往往也会导致复用率低、难以维护等问题。

讲到这里,让我想到了原子设计。原子设计是 Brad Forst 于 2013 年提出的设计概念,该作者用 5 个层级来描述组件库的设计。做下类比,映射到开发人员使用和熟知的组件中,个人认为也适合描述组件粒度。

图片

  • 原子组件

如果说,原子是物质的基本组成部分,那么原子组件就可以作为构成我们所有页面的最基本组成部分。原子组件,可以为上文中提到的基础 UI 组件,例如一个 Input 或一个 Button。它们往往具有不可再拆分的特性,是其他组件的基础。

  • 分子组件

分子组件一般由几个简单的原子组件组成,比如由一个 Label 和一个 Input 组成的姓名输入组件。这种粒度的组件初步具有一定形态和自身属性,与原子组件相比,有一定的可操作性。

图片

  • 生物组件

生物组件是由原子组件及分子组件组成的相对复杂的构成物,它是一种作为一个单元发挥作用的集合体。比如由姓名输入组件和一组按钮组成的搜索组件。在这个组件中,姓名输入组件被放置在一种使用环境中,实现了简单的功能。

图片

有些生物组件是由不同的分子组件构成,但也有可能由相同的分子组件构成,比如网站首页的商品展示组件,该组件由六宫格组成,每个格子使用同一个分子组件进行渲染和展示。

图片

  • 模板组件

模板组件是由原子、分子、生物组件按照一定布局结构组成的区块。它们专注于页面的基础内容结构,而不是页面的最终内容。模板组件是更复杂一点的生物组件,更多的赋能于功能和展示。

图片

  • 页面

最终,通过不同模板组件的拼装,可以生成一个完整的页面。

在实际应用中,组件设计时的粒度往往也需要依据具体的场景具体分析,但原则可以参考高内聚,低耦合的思路,使自己的组件易于维护,同时使自己的整个项目代码看起来干净利落。

总结

其实,本人真心认为组件的抽象与抽象粒度这件事,没有一个一成不变的统一标准,也没有对与错。在基本原则不变的情况下,更多的应该去关注如何适配不同的业务场景和需求要求,求的是“适合”。有时,同样的场景,组件粒度的标准也会随业务场景变化而变化,甚至可能随场景而持续重构。不过为了代码更好的维护和分层,以及避免代码逻辑的过度叠加和膨胀,团队中可以制定一些组件抽象的规范稍稍加以约束。

参考文献

React组件设计实践总结02 - 组件的组织

React 中的各种组件

React PureComponent 使用指南

组件化设计:原子设计实践

❉ 作者介绍 ❉