6

大型前端项目的常见问题和解决方案

 2 years ago
source link: https://godbasin.github.io/2023/07/01/complex-front-end-project-solution/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

大型前端项目的常见问题和解决方案

By 被删

2023-07-01 更新日期:2023-07-01

或许你会感到疑惑,怎样的项目算是大型前端项目呢?我自己的理解是,项目的开发人员数量较多(10 人以上?)、项目模块数量/代码量较多的项目,都可以理解为大型前端项目了。

在前端业务领域中,除了大型开源项目(热门框架、VsCode、Atom 等)以外,协同编辑类应用(比如在线文档)、复杂交互类应用(比如大型游戏)等,都可以称得上是大型前端项目。对于这样的大型前端项目,我们在开发中常常遇到的问题包括:

  1. 项目代码量大,不管是编译、构建,还是浏览器加载,耗时都较多、性能也较差。
  2. 各个模块间耦合严重,功能开发、技术优化、重构工作等均难以开展。
  3. 项目交互逻辑复杂,问题定位、BUG 修复等过程效率很低,需要耗费不少精力。
  4. 项目规模太大,每个人只了解其中一部分,需求改动到不熟悉的模块时常常出问题。

其实大家也能看到,大型前端项目中主要的问题便是“管理混乱”。所以我个人觉得,对于代码管理得很混乱的项目,你也可以认为是“大型”前端项目(笑)。

问题 1:项目代码量过大

对于代码量过大(比如高达 30W 行)的项目,如果不做任何优化直接全量跑在浏览器中,不管是加载耗时增加导致用户等待时间过久,还是内存占用过高导致用户交互卡顿,都会给用户带来不好的体验。

性能优化的解决方案在《前端性能优化–归纳篇》一文中也有介绍。其中,对于代码量、文件过多这样的性能优化,可以总结为两个字:

  • :拆模块、拆公共库、拆组件库
  • :分流程、分步骤

项目代码量过大不仅仅会影响用户体验,对于开发来说,代码开发过程中同样存在糟糕的体验:由于代码量过大,开发的本地构建、编译都变得很慢,甚至去打水 + 上厕所回来之后,代码还没编译完。

从维护角度来看,一个项目的代码量过大,对开发、编译、构建、部署、发布流程都会同样带来不少的压力。因此除了浏览器加载过程中的代码拆分,对项目代码也可以进行拆分,一般来说有两种方式:

1. multirepo,多仓库模块管理,通过工作流从各个仓库拉取代码并进行编译、打包

  • 优点:模块可根据需要灵活选择各自的编译、构建工具;每个仓库的代码量较小,方便维护
  • 缺点:项目代码分散在各个仓库,问题定位困难(使用npm link有奇效);模块变动后,需要更新相关仓库的依赖配置(使用一致的版本控制和管理方式可减少这样的问题)

2. monorepo,单仓库模块管理,可使用 lerna 进行包管理

  • 优点:项目代码可集中进行管理,使用统一的构建工具;模块间调试方便、问题定位和修复相对容易
  • 缺点:仓库体积大,对构建工具和机器性能要求较高;对项目文件结构和管理、代码可测试和维护性要求较高;为了保证代码质量,对版本控制和 Git 工作流要求更高

两种包管理模式各有优劣,一般来说一个项目只会采用其中一种,但也可以根据具体需要进行调整,比如统一的 UI 组件库进行分仓库管理、核心业务逻辑在主仓库内进行拆包管理。

题外话:很多人常常在争论到底是单仓好还是多仓好,个人认为只要能解决开发实际痛点的仓,都是好仓,有时候过多的理论也需要实践来验证。

问题 2:模块耦合严重

不同的模块需要进行分工和配合,因此相互之间必然会产生耦合。在大型项目中,由于模块数量很多(很多时候也是因为代码量过多),常常会遇到模块耦合过于严重的问题:

  1. 模块职责和边界定义不清晰,导致模糊的工作可能存在多个模块内。
  2. 各个模块没有统一管理,导致模块在状态变更时需要手动通知相关模块。
  3. 模块间的通信方式设计不合理,导致全局事件满天飞、A 模块内直接调用 B 模块等问题,隐藏的引用和事件可能导致内存泄露。

对于模块耦合严重的模块,常见的解耦方案比如:

  • 使用事件驱动的方式,通过事件来进行模块间通信
  • 使用依赖倒置进行依赖解耦

事件驱动进行模块解耦

使用事件驱动的方式,可以快速又简单地实现模块间的解耦,但它常常又带来了更多的问题,比如:

  • 全局事件满天飞,不知道某个事件来自哪里,被多少地方监听了
  • 无法进行事件订阅的销毁管理,容易存在内存泄露的问题
  • 事件维护困难,增加和调整参数影响面广,容易触发 bug

依赖倒置进行模块解耦

我们还可以使用依赖倒置进行依赖解耦。依赖倒置原则有两个,包括:

  1. 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
  2. 抽象接口不应该依赖于具体实现,而具体实现则应该依赖于抽象接口。

使用以上方式进行设计的模块,不会依赖具体的模块和细节,只按照约定依赖抽象的接口。

如果项目中有完善的依赖注入框架,则可以使用项目中的依赖注入体系,像 Angular 框架便自带依赖注入体系。依赖注入在大型项目中比较常见,对于各个模块间的依赖关系管理很实用,比如 VsCode 中就有使用到依赖注入。

VsCode:结合事件驱动与依赖倒置进行模块解耦

在 VsCode 中,我们也可以看到使用了依赖注入框架和标准化的Event/Emitter事件监听的方式,来对各个模块进行解耦(可参考《VSCode 源码解读:事件系统设计》):

  • 各个模块的生命周期(初始化、销毁)统一由框架进行管理:通过提供通用类Disposable,统一管理相关资源的注册和销毁
  • 模块间不直接引入和调用,而是通过声明依赖的方式,从框架中获取相应的服务并使用
  • 不直接使用全局事件进行通信,而是通过订阅具体服务的方式来处理:通过使用同样的方式this._register()注册事件和订阅事件,将事件相关资源的处理统一挂载到dispose()方法中

使用依赖注入框架的好处在于,各个模块之间不会再有直接联系。模块以服务的方式进行注册,通过声明依赖的方式来获取需要使用的服务,框架会对模块间依赖关系进行分析,判断某个服务是否需要初始化和销毁,从而避免了不必要的服务被加载。

在对模块进行了解耦之后,每个模块都可以专注于自身的功能开发、技术优化,甚至可以在保持对外接口不变的情况下,进行模块重构。

实际上,在进行代码编程过程中,有许多设计模式和理念可以参考,其中有不少的内容对于解耦模块间的依赖很有帮助,比如接口隔离原则、最少的知识原则/迪米特原则等。

除了解决问题,还要思考如何避免问题的发生。对于模块耦合严重这个问题,要怎么避免出现这样的情况呢?其实很依赖项目管理的主动意识和规范落地,比如:

  1. 项目规模调整后,对现有架构设计进行分析,如果不再合适则需要进行及时的调整和优化。
  2. 使用模块解耦的技术方案,将各个模块统一交由框架处理。
  3. 梳理各个模块的职责,明确每个模块负责的工作和提供的功能,确定各个模块间的边界和调用方式。

问题 3:问题定位效率低

在对模块进行拆分和解耦、使用了模块负责人机制、进行包拆分管理之后,虽然开发同学可以更加专注于自身负责模块的开发和维护,但有些时候依然无法避免地要接触到其它模块。

对于这样大型的项目,维护过程(熟悉代码、定位问题、性能优化等)由于代码量太多、各个函数的调用链路太长,以及函数执行情况黑盒等问题,导致问题定位异常困难。要是遇到代码稍微复杂点,比如事件反复横跳的,即使使用断点调试也能看到眼花,蒸汽眼罩都得多买一些(真的贵啊)。

对于这些问题,其实可以有两个优化方式:

  1. 维护模块指引文档,方便新人熟悉现有逻辑。文档主要介绍每个模块的职责、设计、相关需求,以及如何调试、场景的坑等。
  2. 尝试将问题定位过程进行自动化实现,比如模块负责人对自身模块执行的关键点进行标记(使用日志或者特定的断点工具),其他开发可根据日志或是通过开启断点的方式来直接定位问题

这个过程,其实是将模块负责人的知识通过工具的方式授予其他开发,大家可以快速找到某个模块经常出问题的地方、模块执行的关键点,根据建议和提示进行问题定位,可极大地提升问题定位的效率。

除了问题定位以外,各个模块和函数的调用关系、调用耗时也可以作为系统功能和性能是否有异常的参考。之前这块我也有简单研究过,可以参考《大型前端项目要怎么跟踪和分析函数调用链》

因此,我们还可以通过将调用堆栈收集过程自动化、接入流水线,在每次发布前合入代码时执行相关的任务,对比以往的数据进行分析,生成系统性能和功能的风险报告,提前在发布前发现风险。

问题 4:项目复杂熟悉成本过高

即使在项目代码量大、项目模块过多、耦合严重的情况下,项目还在不断地进行迭代和优化。遇到这样的项目,基本上没有一个人能熟悉所有模块的所有细节,这会带来一些问题:

  • 对于新需求、新功能,开发无法完整地评估技术方案是否可以实现、会不会带来新的问题
  • 需求开发时需要改动不熟悉的代码,无法评估是否存在风险
  • 架构级别的优化工作,难以确定是否可以真正落地
  • 一些模块遗留的历史债务,由于工作进行过多次交接,相关逻辑已无人熟悉,无法进行处理

导致这些问题的根本原因有两个:

  • 开发无法专注于某个模块开发
  • 同一个模块可能被多个人调整和变更

对于这种情况,可以使用模块负责人的机制来对模块进行所有权分配,进行管理和维护:

  1. 每个开发都认领(或分配)一个或多个模块,并要求完全熟悉和掌握模块的细节,且维护文档进行说明。
  2. 对于需求开发、BUG 修复、技术优化过程中涉及到非自身的模块,需要找到对应模块的负责人进行风险评估和代码 Review。
  3. 模块的负责人负责自身模块的技术优化方案,包括性能优化、自动化测试覆盖、代码规范调整等工作。
  4. 对于较核心/复杂的模块,可由多个负责人共同维护,协商技术细节。

通过模块负责人机制,每个模块都有了对应的开发进行维护和优化,开发也可以专注于自身的某些模块进行功能开发。在人员离职和工作内容交接的时候,也可以通过文档 + 负责人权限的方式进行模块交接。

大型项目的这些痛点,其实只是我们工作中痛点的缩影。技术上能解决的问题都是小事,管理和沟通上的事情才更让人头疼。

除此之外,在我们的日常工作中,通常也会局限于某块功能的实现和某个领域的开发。如果这些内容并没有足够的深度可以挖掘,对个人的成长发展也可能会有限制。在这种情况下,我们还可以主动去了解和学习其它领域的知识,也可以主动承担起更多的工作内容。

</div


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK