前言
封装、代码复用、设计模式……
这些都是方法,业务才是目的。技术始终是为业务服务的。能够满足业务需求,并且用起来舒服的,都是好方法。
不存在一套适用于所有项目的最佳代码组织方法,你需要结合业务,去不断地演进。就像你不会用造汽车的方法,去造火箭,或是自行车。
这里分享一些我自己觉得比较舒服的方法。
1. 船到桥头自然直
不用非得等所有细节都想清楚了才开始动手。你只需要一个大致的方案,从第一个文件开始,先写起来。写着写着,你就会发现:
- 这里用到的代码,别的地方也用到了
- 某个函数已经长到超出显示器了
- 自己经常在来回滚动屏幕找同一个东西
- 某个变量名在同一个文件中被使用过了
- 某个地方的逻辑已经复杂到自己都很难一眼就懂了
当这些情况累积到一定程度,你开始有些为之心烦了,你自然就知道,有些代码应该重新安排了。
如果你还没遇到这些情况,或是你觉得还能忍受。不用担心,这说明当前的代码组织方式没什么问题,你什么都不用做,继续就好。
就像打伞。雨小的时候,打不打都行,手里有伞也未必撑开;雨下大了,就都知道要打伞了。
有经验的开发者,尤其是经历过多人协作的大型项目的开发者,通常都会形成一套自己觉得最舒服的打法,可以从一开始就对项目的架构,以及代码的组织方式有一个比较合理的规划。他们在写下第一行代码之前,就清楚地知道项目接下来会变成什么样,自己需要做什么,敲键盘的过程只是把它们心里已经想好的内容进行输出而已。
但如果你只是想做个一个小而美的小工具,项目规模并不大;又或是你入行时间不长,还没有形成自己的一套打发。我建议你试试我这个方法,或许你就能找到适合你的方法。
2. 关注 IOR(投入产出比)
当你的代码超过 1 万行的时候,恭喜,你成功解锁了大中型项目的成就。
(不用太纠结于 1 万这个数字,它只是个数量级而已)
这个时候的项目肯定已经有了一定的组织方式,代码分门别类地存放在不同的目录中,各司其职。
从这个阶段开始,任何全局性的改动,都会附带比较大的成本。所以每一次的决策,除了关注能否解决问题外,还有另一个需要重点考虑的,那就是 IOR —— 投入产出比。说得直白一点:如果我要这么做,我需要投入多少资源?我能得到什么?这么做值得吗?
如果经过改造,代码的结构变得更加清晰、使用起来更加符合直觉、更加契合业务的需要,那么这样的投入就是值得的。反之如果结果并没有变得更好,或是为此需要投入巨大的成本,那么就要重新考虑一下,有没有更好的办法。
3. 合理的 SoC(关注点分离)
这也是题主真正纠结的点。
我们以题主给出的场景为例:同一个页面的业务代码,在不涉及通用组件以及工具函数的情况下,要不要拆成多个组件?
显然这不是一个绝对的要或者不要的问题,业务代码的内容,当然是要由业务来决定。前端的业务结构跟路由有很大的关系,因此一个路由对应一个文件,这本身是一个非常合理的安排。
如果页面组成相对简单,所有逻辑都放在一个文件里,阅读起来会更加方便,维护成本也比较低,那就完全可以不用拆。(应该没几个人会把一个关于页面拆的稀碎吧)
但如果业务复杂到一定程度,比如一个页面中包含了若干个相对独立的部分、单文件的行数开始过千,那么就可以考虑对它们进行拆分。(想象一下如果淘宝的首页只有一个组件)
拆分之后,每个组件关心的事情更少,单文件的行数也会显著下降,配合恰当的变量命名,大概率会使代码结构变得更加清晰。
如果结合 React 等具体技术栈来看,合理的拆分甚至还能减少 DOM 操作,优化性能,典型的像输入框的交互、列表的渲染等,它们的性能都是很容易受到其他组件影响的。
至于组件间的通信,Context、EventBus、RxJS、…… 解决办法有很多。
除了按业务拆,我们还可以按分工拆,典型的就是数据与行为分离,把数据模型从视图模型中剥离出来,分工更加明确一些,各自关注的事情也更加纯粹。
至于拆到什么粒度,可以参考第一条 —— 不让自己心烦了,就算到位了。
像 Redux 等状态管理方案,其存在的最大意义就是方便开发者进行关注点分离。同样的事情,React 自己都能做,但当业务复杂到一定程度,你就会想要把它们独立出来。即便你不引入 Redux,你或许不知不觉中也已经实现了一个简易版的 Redux。
类似的还有 React 的 Custom Hooks,本质是一种代码复用方式,通过把整段的逻辑抽取成单个函数,实现了关注点分离。
4. 模块化,减少冲突
在多人协作的项目中,开发者拆分代码还有一个重要的考虑因素,就是代码冲突。
以 API 为例,把所有的 API 函数抽取到一个独立的文件已经是一个不错的实践,对于独立开发者而言,这往往已经够了。我自己有一些私人的小项目就是这么干的。即便是大型的项目,这么做也不是不可以。
但在多人协同的过程中,每个人负责不同的 Feature,可能同时有多个人都在编辑同一个文件,就很容易产生冲突。如果我们把每一个 API 都拆到一个单独的文件中去,或者按照一定的规则分个类(例如分成 user、product、comment 等 ),冲突的概率就会大大下降,因为和你编辑同一个文件的人变少了。
这本质上就是模块化的思想,每个模块之间相互独立互不干扰,只要结果正确,我并不关心你内部是怎么实现的。
5. 统一化,减少认知负担
团队管理中,一个不容忽视的成本,就是沟通成本。让所有人都步调一致,这可不是一件容易的事。
举个例子:在大型项目中,我们经常会看到这样的结构:一个组件 + 一个 Store,即便这个 Store 里的内容非常的少,完全可以由 State 来承载,甚至里面什么也没有,只是继承了一下父类,也依然会安排一个 Store。
当然,具体到这个组件来看,确实是没有必要。但当你面对几十个甚至几百个组件,你的团队也有差不多的规模的时候,问题就变了。
同一类的组件,有的有 Store 有的没有,这在一些场景下会形成认知负担,开发者需要花时间去熟悉它们的组合,可能还会需要为此编写一些容错代码。如果我们可以确保每一个组件都有一个 Store,即便它并不需要,但依然存在,问题就得到了简化。毕竟让计算机创建一个没用的对象并不会占用太多资源,但相比之下让人工来处理这个问题,成本就很高了。
这样的做法在大厂中比比皆是,有点算法复杂度中空间换时间的意思。
当然,也不排除有些是通过代码模板自动生成的。
总之
代码的管理也是一门艺术,就像写作,如何把你的文字整理成章节,需要慎重的思考。
没有人能告诉你什么是最正确的方法,勇敢去尝试,到时候了,你自然就会知道该怎么做。