手摸手教你真正的组件化

95 阅读13分钟

公众号|薯仔在发呆(ID:shuzai_daydreaming)

在各大开发框架的普及下,相信应该没有哪个开发还不能聊几句前端组件化、组件开发之类的东西。但当真的开始写代码的时候,当大家在团队内做CR的时候,看到了一个又一个几百行,甚至上千行的“组件”时,是不是又开始怀疑,我们到底理解了什么是组件化了吗?

什么是组件?

只要简单地搜一下,能找到无数帮我们定义什么是组件的文章,他们会说组件是单一功能的代码块;是高内聚低耦合的代码是易于扩展、易于维护、易于复用的代码等等。很明显,这些说的都没错,但是当我们拿着这些定义回看自己的项目时,好像理想和现实之间差了无数次重构与优化。其实,过于理想化的臆想和脱离现实的照本宣科都不对,理解组件一定是从实际项目出发,根据业务逻辑梳理出来的。

我们都知道组件可以分为以下几种类型:

  • 基础组件:和业务无关的组件,例如按钮、弹窗、下拉列表等
  • 业务组件:这就和各自的项目相关了,但也一定是在项目中能拆分到较细颗粒度的部分,同时可以内部维护一些和业务相关的逻辑,例如之前的项目中有一个叫RoomBadge的组件,我们希望它完成的功能是根据传入的房间类型这个参数的不同,渲染不同的文案和背景色;同时如果该房间是官方房间,那么还需要展示对应的icon。很明显这个组件是绝不可能复用到其它项目中,但是在我们的项目中就属于不可或缺的公共组件了。
  • 模块:模块是基础组件和业务组件的任意组合。对于模块来说,它可能经常被修改,在业务的基础逻辑不改动的情况下,任何的业务逻辑修改应该都能在模块的层级中完成。
  • 页面:对于展示给用户的最终层级,我们依然可以将其看成是一个组件。只是它几乎不太可能会被复用,除非有同质化的项目,需要将整个页面搬过去复用。之前也遇到了这样的情况,但当我们将已有的代码搬过来之后,发现逻辑很难梳理,同时各种在那个场景才需要的逻辑和公共逻辑夹杂在同一个文件中,如果要做修改根本无法下手,最终的结果便是直接重写了一份,这点后面会详细说明。

如何拆解组件?

从上面提到的四种组件出发,分享一下我们项目中的组件拆分步骤:

重构

我们看到很多文章在定义组件时都提到一个组件应该只做一件事(单一功能的代码块),似乎直接就跳过了重构的部分;或者觉得重构和样式没有什么关系,样式又不是功能,而且现在五花八门的设计工具(例如Figma)都能直接导出样式,作为开发者,应该更多关注交互逻辑,既然有能减少工作量的工具,就应该好好利用。

上面这些想法不能说完全错了,如果我们把这些想法放在“日抛”/“月抛”的活动页上,那问题不大,活动页甚至还可以通过一些拖拽的平台生成页面,有些连基础的逻辑都能生成出来。

但如果是一个需要长期维护的项目呢?如果是抽取的组件需要在项目内复用10多次甚至更多呢?这时候必须从最开始的步骤就要考虑到组件的各种特性。拿我们项目中的一个页面举例:

image.png

这是一个看上去很普通的“我的”页面,从设计稿中能看出来这里会分为主人态和客人态,然后经过简单地拆分,可以将这个页面里的内容按瀑布流的模式拆成四个部分:

image.png

很多时候,大家做到这个地方就开始撸起袖子写代码了,但其实页面的重构部分才刚刚开始。

由于APP主页的四个tab会复用模块1(之后将其命名为MainHeader),因此MainHeader是一个业务内的公共组件,“我的”页面的内容只有模块2、3、4。那么MainHeader的样式应该注意哪些问题呢?

  • 它的宽度一定是和屏幕宽度相同
  • 左侧的标题和右侧的操作按钮必须是分别靠两侧对齐
  • 第三点比较容易被忽略,组件的尺寸及组件内部和外部与其它组件的间隔如何处理?

对于该组件自身的尺寸,可以通过figma给出的宽高来确定:

image.png

然后我们会看到,MainHeader和下方的组件之间的间隔:

  • 应该直接写在组件内
  • 还是交给调用方对MainHeader的样式进行覆盖
  • 亦或是交给下方的组件处理?

image.png

很明显这几种方式对于当前这个页面来说都没有问题,怎么能实现就怎么写呗。

但如果考虑到MainHeader是在其它几个tab页面中有使用的话,我们分别考虑上述几种做法

  1. 直接写在MainHeader内的话,这个间距就成了组件的一个“特性”,对于调用方来说,每次都需要check设计稿里MainHeader的底部和下方组件的间距,如果不相同,需要将这个间距和30作比较;如果大于30还好办,直接加上差值即可,如果小于30,还得将下方组件向上移动才能满足设计稿的要求;因此将组件自身和其他组件之间的间距耦合在组件内部是不合理的。

  2. 那么交给调用方,在调用MainHeader的时候对其样式进行覆盖呢?这个做法本身来说没有什么问题,但这样容易增加阅读代码的人的心智负担,TA可能不清楚为什么每次调用MainHeader都要传参来覆盖组件的margin-bottom;当然这个问题通过看代码和页面效果很快能理解,不过如果能降低代码阅读的难度就尽量用各种手段降低吧。

  3. 最后还剩一个方法,交给下方组件处理,个人理解这种方式是相对比较优雅的,原因就是前两种方式的劣势,即MainHeader本身不用关心它和其它组件之间的间距,并且不用每次调用都对MainHeader的样式进行覆盖。

MainHeader的样式规划,我们得出一个结论,即组件的样式应该只关心组件内部的布局,组件外的布局应该交给业务组件去处理。我们会发现,即便是写样式,也实践了高内聚低耦合的原则。

再举一个小例子:

image.png

image.png

在设计稿中,右侧的通过按钮和已添加文案的定位样式是相同的,之前的同学直接使用了这块样式,很明显能看出来,当“已添加”文案左侧需要增加一个icon时,必须修改文案处的DOM结构,因为在figma给出的样式中,并未考虑整体的布局,只是给了一个简单布局的比例。在这种情况下,需要开发对组件整体有一定的掌控,需要考虑组件的结构和样式对之后的扩展是否较为容易。

交互

完成样式部分的重构后,我们的组件中几乎还没有任何的交互相关的代码,接下来就需要考虑组件内部的交互。很多时候在做需求时遇到了较为复杂的组件时,很容易将一大块的逻辑都写在同一个文件中,且不说其他同学在阅读时的困难,哪怕是这段代码在写完之后一周如果有个交互要修改,如果是我自己都很难再次梳理清楚逻辑了。在基于React的项目中,受益于React提出的Hooks,我们可以很方便地拆分组件中的交互。

在我们的项目(基于Hippy)中,有一个列表组件需求抽取,它需要实现下拉刷新、上拉加载、长按/左滑删除的功能,对Hippy比较熟悉的朋友会说,这就是用官方给出的RefreshWrapperListView结合不就行了,哪里还需要什么组件,官方都提供好了组件直接用就可以了。

但是,如何保证多人开发下对同一个逻辑的实现是相同的?如何降低对于列表组件修改时的工作量?如何保证多人开发下的样式还原是相同的?基于这些问题,需要基于框架的公共组件再封装一层业务组件。

image.png

image.png

可以看到,这个列表组件(下述PageList组件)不仅需要实现下拉刷新、上拉加载的功能,可能还需要实现自定义下拉刷新loading和自定义上拉加载文案,因此结合Hippy的组件,对PageList内部还需要自定义RefreshWidget组件和LoadMore组件。

上面也提到,交互逻辑最好不要全部放在一个文件中,能拆分的逻辑尽量提前拆分出来,简单来说:

  • 如果遇到了组合使用的useStateuseEffect,就可以将他们提取出去
  • 如果有较独立的逻辑(接口请求、一大段逻辑后返回简单的状态等),就考虑将他们提取出去

PageList组件中,发现下拉刷新和加载更多是两块比较独立的逻辑,并且只需要返回对应的状态和回调,因此这两块逻辑以hooks的形式抽取出去,最终,PageList组件的结构如下:

image.png

这些文件的代码行数都比较少,最多的是组件的入口文件,但代码量也就不到140行,大大降低了之后维护的心智负担。

参数、回调函数和钩子函数

在完成了组件内部的处理之后,我们需要考虑如何让调用方更方便地使用组件提供的能力,我们需要将组件内部的能力以参数、回调函数的形式暴露出去给调用方使用。

这块其实比较直观,唯一需要注意的是,如果某些组件在之后想做成组件库,那么其提供的参数和回调函数不要掺杂任何的业务语义。

优化项

对于大多数组件来说,在完成上述几点之后,一个功能齐全的组件其实就已经完成了。但是某些组件还需要考虑性能优化的问题,例如这个组件的渲染是否会对项目整体有影响,组件的交互是否符合用户的交互预期,组件的动画效果是否让用户感到流畅等等,这些优化项需要在实际的工作中反复打磨细节。

Page as component

大多数情况下,我们的页面都不会完整地当成一个组件去跨项目复用,但对于一些看上去就不是特别定制化的功能,我们就需要很小心地去构建页面。之前有一个青少年模式的功能,最开始的想法是因为之前的APP里已经有这个功能了,并且也是基于Hippy的,那么应该可以将其copy过来,只改动样式风格和接口即可。结果在读了之前的代码后,直接放弃了这个想法。

从页面上说,现在的APP只需要两个页面

image.png

image.png

之前的APP还有一个通过短信验证密码的页面,但是所有的代码都在这三个文件里

image.png

并且三个文件的代码量都是300行左右

image.png

image.png

image.png

代码量不算大,但是样式、公共方法、常量、类型定义等所有相关内容都在同一个文件里,对于部分可复用的结构也没有提取业务组件,接口请求中掺杂了大量的业务逻辑,导致迁移时很难确定是否需要移除。由于这些原因,最终还是没有复用这个页面,而是重写了整套青少年模式的功能:

image.png

重写后,我们可以看到整个功能被拆分成了多个hooks和业务组件,而两个入口文件home/index.tsxpassword/index.tsx分别只有27行和99行:

image.png

image.png

当然,这里面的逻辑也有值得商榷的地方,但整体的结构是更清晰一些的。因此对于一些看上去不是本项目定制化的功能,在做这个功能时需要更多的思考和架构。

小结和一些其它的思考

对于组件的拆解和架构,需要从重构开始,如果重构的不合理,可能会因为一个需求调整,导致修改整个组件甚至是页面结构。

其次对交互的处理需要将相对独立的逻辑拆分出来维护,将相对独立的结构拆分成子组件维护。

对于非项目定制化的功能,需要考虑整体复用的可能性,需要更全局的角度来架构组件。

虽然对组件的拆解和架构有不少好处,但它有一个不可回避的现实,那就是很有可能要增加需求开发时间。因为开发者需要更多的时间思考组件的架构、思考组件和项目的关系、思考多人协作开发下组件的使用、思考在CR时的复杂度等等问题。

但我们认为这是值得花时间去思考的问题,特别是在一个庞大的前端项目中,如何更优雅地维护代码?如何用更少的时间实现日益增长的复杂需求?这需要开发在早期对项目有整体的规划和全局的视角,而这些问题都需要时间思考。所以,希望大家都在各自的需求开发下,多多以全局的视角思考项目和组件的架构,期待和大家做更多的交流。

欢迎关注公众号:薯仔在发呆(shuzai_daydreaming)

有任何互联网从业方面的疑问,或简历修改,或大厂岗位内推,欢迎 +wx:henry_yangs

十年 web 开发、前腾讯高级工程师 Henry

互联网从业者、全栈工程师、独立开发者、代码+产品体验强迫症患者、生活体验家、旅行爱好人士