查看原文
其他

React组件封装实践:如何拆解复杂的页面

王峰(楚枭) 阿里云开发者 2023-11-04

阿里妹导读


在日常开发中,遇到非常难以维护的页面是常态,相信不少同学也为此苦恼过,笔者在业务开发中总结了些经验希望对大家有所启发。(后台回复大数据即可获得《大数据&AI实战派》电子书)

背景

在日常开发中,遇到非常难以维护的页面是常态,相信不少同学也为此苦恼过,笔者在业务开发中总结了些经验希望对大家有所启发。下图是一个较为复杂的详情页、表单页,我截取了其中一小部分作为示例:

随着需求不断迭代,这个页面的代码变得越来越复杂,代码达到几千行,html 标签嵌套层级非常深,每次想在正确的节点改东西、加元素都非常费眼睛;每次想修改、叠加业务逻辑,看到一堆 useEffect、useState、useRef 令人望而却步。于是决定重构以改变现状。

如何重构,以拆解复杂页面

如何重构一个复杂前端页面?笔者平常主要写后端,实际工程中后端代码的腐化很多都来源于 if-else 不断叠加,要重构一般分几个层次看:
  • 如果是某一个业务概念比较复杂造成的大量 if-else 嵌套,使用策略模式重构;

  • 如果是多个业务概念交织造成的复杂度,则用责任链模式梳理好每个层次;

  • 如果是更高的抽象层次,比如不同的业务身份有不同的执行链路,或者不同的业务身份都要做某件事但做法不同,那么最好使用模板方法设计模式定义好高层次的业务抽象和默认逻辑,不同的业务身份进行选择性的重写或业务逻辑扩展。
大的思路如此,具体场景各有各的特殊性,需要灵活应对,这里也不过多展开。总之,后端的复杂度和各个场景的业务逻辑息息相关,垂直纵深很大,但前端呢?私以为前端虽然也有业务逻辑但不深,它的复杂不是来源于垂直纵深,而是水平堆积。一个页面的内容经常又多又杂,有详情、有表单、各种区块、不同标签页,里面的内容我不赘述。那么重构的方向呼之欲出:使用组合的思想拆分水平堆积的业务逻辑块。具体到 React,其实就是拆解业务、封装组件,是一个组件化的过程。


组件化

其实前端整个 React App 说白了可以抽象成一个组件树,如图:

笔者习惯将组件分成:基础组件和区块组件。
  • 基础组件:具有一定业务属性的小型组件单元。它有业务属性,但比较弱,强调通用性。

  • 区块组件:业务知识较重的大组件。它内聚了很多业务知识,通用性弱,甚至可以没有通用性,强调业务的内聚性。

按照基础组件和区块组件的划分,我开始重构上图详情页。

组件封装实践

我将页面上的展示内容按照业务块进行了划分,自顶向上对业务区块进行了重新的定义,如图:

划分好了就开始封装,列举几个组件的封装示例。

基础组件:AliTalk IM

AliTalk IM 组件定义

接收方 IM 身份的 id 作为入参,返回 IM 展示组件,点击 icon 则唤起聊天弹窗进行聊天操作,完成后可关闭弹窗。至于初始化聊天框、销毁聊天框的逻辑,以及如何进行聊天,应该在组件内封装好,外部业务不关心这些,主要代码:
type ChatProps = { uid?: string;};const Chat: FC<ChatProps> = (data: ChatProps) => { const [showChat, setShowChat] = useState(true); useEffect(() => { console.log('init Alitalk: ' + data.uid); return () => { console.log('destroy Alitalk: ' + data.uid); setShowChat(false);
const aliTalkMessageBox = document.getElementsByClassName('weblite-iframe'); for (let i = 0; i < aliTalkMessageBox.length; i++) { const item = aliTalkMessageBox[i]; item.remove(); } }; }, []);
return ( <div> {showChat && ( <Alitalk uid={data.uid} pid={'xx'} bizType={1} bizId={'xx'} > <img width={24} height={24} src={ 'https://img.alicdn.com/imgextra/i2/O1CN01acXzMG1d5JsurHGVR_!!6000000003684-2-tps-200-200.png' } /> <span style={{ marginLeft: '5px', color: '#FF6600' }}>chat now</span> </Alitalk> )} </div> );};export default Chat;

AliTalk IM 组件定义组件引用

直接引入 <Chat> 标签,一行代码即可:
<Descriptions style={{ marginBottom: 24 }} title="买家信息"> <Descriptions.Item label="买家旺旺"> <Chat uid={detailData?.data?.buyerAliTalkId} /> </Descriptions.Item></Descriptions>

区块组件:操作栏行动点弹窗

弹窗组件定义

以移交服务单为例,点击按钮则唤起转交表单弹窗,填完表单后提交则发起请求,完成后自动关闭弹窗。表单提交的逻辑,操作栏展示区块并不关心,封装一个 TransferOrderModalForm 组件内聚这些业务逻辑即可。

弹窗组件引用

<Fragment> <Button.Group> <EstimatedQuotationModalForm orderId={detailData?.id} />
<DomesticWarehouseReceivingModalForm orderId={detailData?.id} />
<OfficialQuotationModalForm orderId={detailData?.id} />
<MarkOrderPaidModalForm orderId={detailData?.id} />
<MarkOrderExceptionModalForm orderId={detailData?.id} />
<MarkOrderClosedModalForm orderId={detailData?.id} />
{/* 移交服务单 ModalForm */} <TransferOrderModalForm orderId={detailData?.id} /> </Button.Group></Fragment>

按照组件拆分后,主页面的代码行数从几千行降低到 200 行,主页面仅仅只做了对其他组件的引用和页面编排,其引用的业务区块组件如果够复杂,还能继续再次拆分组件,整个页面就成了一个挂载的组件树,但每个区块都只关心自己的业务抽象层次,符合 SLAP 原则。

组件封装的思考

关于组件设计思想

基础组件应该做成原子能力,不要陷入业务场景中,参数要设计得普适性强一点,这样设计出来的组件复用性强,比如聊天组件、获取当前登录用户组件、鉴权组件等等,都符合这种情形。
而业务区块组件恰恰相反,完全没必要考虑复用性,目标就是把不同业务抽象层次进行拆分、隔离,使得整个业务层层递进,每个层次都只关心自己应该关心的业务,这样设计出来的组件高内聚、易读、易维护,当然,如果能复用那更好,算是增值收益了,但这不是目标。
业务区块组件应该自顶向下设计,开始的时候应该设计得粗粒度一点,随着业务不断的迭代可以慢慢下沉,而一开始就想设计精细,想要一步到位,反而会随着后续的业务迭代不断要打破进行调整,丧失了灵活性。

关于前后端思想上的融会贯通

虽然前端的组件和后端的类要怎么设计、怎么实现,看起来区别很大,但咱们剖析表象看本质,思想其实是一脉相承的,举几个例子:
战术上,React 现在推行的是函数式组件,给一组入参,返回展示元素,简单的输入输出无副作用;后端也一样,要尽量避免一个对象参数在不同的方法不同的节点被改来改去,最后改成了啥都不知道,不利于维护,也容易出 bug,所以很多 API 比如 Stream.map() 的设计都提倡不要把对象改来改去,而应该干净利落的使用纯函数。
战略上,SLAP 单一抽象层次原则从来都不针对是前端还是后端,前端组件也好,后端类也好,都要搞清楚每个业务层次关心的核心要素是什么,把不该关心的东西丢给其他业务层次完成,不要把编码变成了一个翻译业务需求的动作,而应该像画家作画一样,先构图再落笔。
《黑客与画家》中描绘了黑客与画家的诸多相同点,“画作永远没有完工的一天,你只是不再画下去而已”。希望追求卓越的你能始终保持那份对设计的热忱。

阿里云开发者社区,千万开发者的选择


阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。

继续滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存