前端组件设计之一——设计原则

14,072 阅读7分钟

前言:
    前端面试中,请你设计一个通用的Select组件,要求手撸代码或说一下设计思想?
    我只会写常用的业务组件,最多再把数据抽象一下,其他的就不知如何下手了。
    今天的文章我们就先了解一下组件的设计原则,懂理论才能更好的实践。

组件设计的基本原则

一个组件的复杂度,主要来源就是自身的状态;即组件自身需要维护多少个不依赖于外部输入的状态。

组件开发中,如何将数据和UI解耦,是最重要的工作。

组件开发过程中,时刻谨记、思考是否符合以下的原则,可以帮助你开发一个更完善的通用组件。

单一职责

你的组件是否符合只实现一个职责,并且只有一个改变状态的理由

如fetch请求和渲染逻辑,应该分离。因为fetch请求时会造成组件重新渲染,渲染时的样式或数据格式变化,也会引起组件重新渲染。

单一职责可以保证组件是最细的粒度,且有利于复用。但太细的粒度有时又会造成组件的碎片化。

因此单一职责组件要建立在可复用的基础上,对于不可复用的单一职责组件,我们仅仅作为独立组件的内部组件即可。

通用性

组件开发要服务于业务,为了更好的复用,又要从业务中抽离。

下面代码实现了需求A:实现一个基础的select组件: menu:是select的下拉列表,menu上面的div是select的选择框头部,包含一个值和一个箭头。

<div className={dropdownClass}>
  <div
    className={`${baseClassName}-control ${disabledClass}`}
    onMouseDown={this.handleMouseDown.bind(this)}
    onTouchEnd={this.handleMouseDown.bind(this)}
  >
    {value}
    <span className={`${baseClassName}-arrow`} />
  </div>
  {menu}
</div>

此时又有一个新的需求B,要求将select选择框头部渲染为一个图片。

虽然B的交互模式和 A一模一样,但因为二者在 DOM 结构上的巨大差别,导致我们无法复用上面的这个 Select 来实现它。 只能去修改源代码、或重新写一个符合需求的组件。

因此组件开发时最好的做法是放弃对DOM的掌控,只提供最基础的DOM、交互逻辑,将DOM的结构转移给开发者。

下面的代码是Antd的组件DropDown,可以看到只有最基础的DOM,提供了多个渲染函数和处理逻辑。

return (
  <Trigger
    {...otherProps}
    prefixCls={prefixCls}
    ref="trigger"
    popupClassName={overlayClassName}
    popupStyle={overlayStyle}
    builtinPlacements={placements}
    action={trigger}
    showAction={showAction}
    hideAction={hideAction}
    popupPlacement={placement}
    popupAlign={align}
    popupTransitionName={transitionName}
    popupAnimation={animation}
    popupVisible={this.state.visible}
    afterPopupVisibleChange={this.afterVisibleChange}
    popup={this.getMenuElement()}
    onPopupVisibleChange={this.onVisibleChange}
    getPopupContainer={getPopupContainer}
  >
    {children}
  </Trigger>
);
  • 复用一个组件时,即复用其职责,所以只有单一职责的组件,是最便于复用的
  • 当一个组件错误地有多个职责时,就会增加复用时的开销。
  • 尽量避免代码重复,重复两次及以上的代码,考虑一下是否可以复用?

通用性虽好,但会浪费开发者很多精力,因此在抽象业务组件之前,请问自己:

* 存在代码重复吗?如果只使用一次,或者只是某个特定用例,可能嵌入组件中更好。

* 如果它只是几行代码,分隔它反而需要更多的代码,那是否可以直接嵌入组件中?

* 性能会收到影响吗?更改state/props会导致重新渲染,当发生这种情况时,你需要的是 只是重新去渲染经过diff之后得到的相关元素节点。在较大的、关联很紧密的组件中,你可能会发现状态更改会导致在不需要它的许多地方重新呈现,这时应用的性能就可能会开始受到影响。

* 你是否有一个明确的理由?分离代码我想要实现什么?更松散的耦合、可以被复用等,如果回答不了这个问题,那最好先不要从组件中抽离。

* 这些好处是否超过了成本?分离代码需要花费一定的时间和精力,我们要在业务中去衡量,有所取舍。

封装

良好的组件封装应该隐藏内部细节和实现意义,并通过props来控制行为和输出。

减少访问全局变量:因为它们打破了封装,创造了不可预测的行为,并且使测试变得困难。可以将全局变量作为组件的props,而不是直接引用。

组合

具有多个功能的组件,应该转换为多个小组件。
单一责任原则描述了如何将需求拆分为组件,封装描述了如何组织这些组件,组合描述了如何将整个系统粘合在一起。

纯组件和非纯组件

非纯组件有显示的副作用,我们要尽量隔离非纯代码。

将全局变量作为props传递给组件,而非将其注入到组件的作用域中。

将网络请求和组件渲染分离,只将数据传递给组件,保证组件职责的单一性,也能将非纯代码从组件中隔离。

可测试

测试不仅仅是自动检测错误,更是检测组件的逻辑。

如果一个组件测试不易于测试,很大可能是你的组件设计存在问题。

富有意义

开发人员大部分时间都在阅读和理解代码,而不是实际编写代码。
有意义的函数、变量命名,可以让代码具有良好的可读性。

组件设计的最佳实践

组件的UML类图

前端组件的架构其实是一个是树状图,当我们设计一个组件时,推荐用UML类图的形式,将组件结构、数据流动状态、处理函数明确标注。先构思组件细节,再写代码,可以避免代码的多次反复。

  • 用UML类图的形式,将网页中的组件树结构画出来
  • 每个类图中,标明需要的state、props、methods,

我们想要实现一个Table组件,Table组件包含行数(RowCount)、header、body。

Table的数据源data、行数RowCount,都来自props;Table内部的排序函数,需要的状态sortPerperty、ascending来自state;

Table的操作包括onRowClick,来自props;排序setSortProperty,来自state;

UML类图如下所示,可以直观的了解组件的UI层结构,数据流动和处理函数,写代码时再也不怕会重构了^_^

辅助代码分离

为了让下一个接手的同事更好的理解代码,我们有时会在核心代码中,添加必要的注释,让代码更清晰。

let params = {
    pageNum: pageNum,
    pageSize: pageSize,
    status: 4, // 参照:ROBOT_STATUS, 0-新导入,1-审核中,2-审核通过,3-审核未通过,4-上架,5-下架
    title: search,
    queryType: 0 // 0--只查询列表, 1--查询申请状态
};

代码如上,阅读代码时,你能快速了解各个字段的含义,但你会额外分心去看注释,思路被打倒,导致中断了整个函数的逻辑分析。

因此,假数据、非技术说明文档、配置代码,建议放在代码外,而不要放在核心代码中,会影响用户体验。

扁平化的state和props

给组件传递props时,建议用更扁平化的props,而不要用嵌套的对象或数组。

<DetailModal
    {...modalData}
    visible={showModal}
    tagType={ROBOT_TYPE}
    sceneList={sceneList}
    handleCloseModal={() => this.handleCloseModal()} />
  • 如上面的代码所示,传递的数据结构是modalData,但用户不清楚modalData中包含哪些属性,且还可能存在多余的属性,开发中应尽量避免传递给组件不需要的属性。
  • react中,如果要修改对象、数组,必须创建一个副本;可能会因为浅复制而造成页面的重新渲染。

参考文章链接:

1.前端组件设计原则

2.可靠React组件设计的7个准则

3.通用前端组件的开发

4.重新设计React组件库