如果只能用一个词来概括 Angular 的优点,那我会选「工程化」;如果要换个文艺点的词,我会说 「大巧若工,大道至简」。这个诞生于一群 Google 工程师之手的框架,从一开始就打上了鲜明的「工程化」烙印。

什么是工程化开发呢?它是和手工作坊式的开发相对而言的。主要的特征是需要多人、多时段的分工与配合,而不是一个超级程序员写完90%的代码,其他人只负责打下手。

文生图:一个对比鲜明的场景。左边是一个混乱的手工作坊,工具和零件散落一地,一个工匠满头大汗。右边是一条整洁、明亮的自动化流水线,机械臂正在精确地组装一个复杂的产品。风格:扁平化概念插画。

但大中型系统面对的挑战不是靠超级程序员就能解决的。如果一个系统的复杂性逐渐累积,迟早会让它的维护成本越来越高,变成鸡肋一般的存在。要解决复杂性的问题,首要办法就是从架构层面上隔离系统的各个部分,让它们彼此之间相互独立,互不干扰,也就是说,分而治之,各个击破。然而这并不容易。

多人开发最大的挑战就是如何分工协作。#

在现代 Angular 中,支持分工协作的主要基础设施,是清晰的代码组织结构与强大的依赖注入(DI)体系

在过去,我们严重依赖 NgModule 来划分应用的「硬边界」。而在以独立组件(Standalone Components)为主流的今天,这种边界感通过一种更灵活、更自然的方式得以实现。想象一个 Monorepo 工作区,我们通常会建立应用和库两类目录:

  • 库目录:这里存放的是可复用的「零件」,比如 ui-kit(共享组件库)、data-access(数据访问服务)、utils(工具函数)等。每个库都由一个或多个专注的团队负责。

  • 应用目录:这里存放的是最终的产品,比如 main-app(主应用)、admin-app(后台应用)。

这种组织方式,天然地形成了团队间的「硬边界」。ui-kit 团队的核心使命就是打磨出高质量、可复用的 UI 组件。他们无需关心 main-app 的业务逻辑。

而将这些独立的「零件」和「产品」无缝粘合起来的「万能胶」,就是 Angular 的依赖注入体系。

  • main-app 中的一个组件,可以通过 inject(DataService) 来使用来自 data-access 库的服务,而无需关心这个服务内部是如何实现 HTTP 请求的。

  • 如果将来我们需要更换数据来源,data-access 团队可以无声无息地替换掉服务的内部实现,而 main-app 的代码一行都不用改。

这种模式,将「代码的物理组织」和「逻辑的依赖关系」完美解耦。每个团队都可以专注于自己的「一亩三分地」(自己的项目或库),并通过 DI 这个「标准接口」与其他团队协作,避免了混乱的意外耦合。这才是现代前端工程中,「分而治之」的精髓。

除了意外耦合的问题之外,多人开发的另一个挑战就是契约。#

手工作坊式的开发可能不需要显式的契约,因为超级程序员「心怀宇宙」,了解整个系统的每一处关键点。但人一旦多了,这种方式的弊端就会显露出来,毕竟人类的心灵并不相通,并没有神圣的卡拉连接你我。这个时候,就需要显式地描述契约,并借助工具来保障这些契约没有被误解。

在 Angular 中,用来应对契约问题的主要基础设施是类型测试

Angular 借助 TypeScript 来对类型提供支持。通过类型,Angular 可以对接口契约进行一定程度的表达。在开启了严格模式,特别是 strictTemplates 之后,这份「契约」甚至延伸到了模板中,确保你在绑定属性、调用方法时,类型都是安全的。我们都知道,发现错误越早,解决它的代价就越低。

不过,类型只能对契约进行一定程度的表达,但它无法表达「我要一个大于10小于1000的数字」之类的契约,这时候就需要不同层次的测试出手了。从最早的 AngularJS 开始,测试就一直是重中之重。在现代 Angular 中,测试一个独立组件变得前所未有地简单,因为你不再需要为了测试而建立一个复杂的 TestBed 模块,只需在 TestBed.configureTestingModuleimports 中加入该组件依赖的少数几个「零件」即可。

多人开发中一个稍微容易但更加繁琐的问题是代码风格。#

一个团队要想形成统一的代码风格,「写得像一个人一样」其实相当麻烦。光靠制订代码规范是没用的,《规范.exe》胜于《规范.txt》。Angular CLI 从创建项目伊始,就为你配置好了 ESLint 和 Prettier,并提供了官方的风格指南。这让团队可以把精力从「代码该怎么写好看」的争论中,解放出来,聚焦于业务逻辑本身。

时间是系统最大的客户,也是系统最大的敌人。#

一个只为打印一次 Hello, world! 而写的程序不需要任何工程化。但那些真正的系统势必会随着时间的推移而不断演化,而演化的过程,就是一场与「熵增」(系统混乱度增加)的持续对抗。

Angular 如何抵抗岁月的侵蚀呢?

首先,Angular 的设计原则是每个部件只关注一件事(关注点分离 SoC)。

  • 从宏观上说,通过对应用与库的划分,ui-kit 只需要关心 UI,data-access 只需要关心数据。

  • 从微观上说,服务拆分出了业务逻辑,组件只负责展现,而指令和管道则负责能力的扩展与数据的转换。这种清晰的职责划分,让需求变更带来的影响被最小化了。

其次,Angular 的版本发布策略在保持稳定和拥抱变化之间做出了较好的平衡。 Angular 遵循语义化版本(semver)规范,并提供了强大的 ng update 命令。这个命令不仅仅是简单地升级版本号,它还会运行一系列「迁移脚本(migration schematics)」,自动地、安全地将你的代码从旧语法更新到新语法。从 NgModule 到独立组件,从旧的控制流指令到新的 @if/@for 语法,这些重大的演进,很多都可以通过 ng update 来平滑过渡。更重要的是,这个命令不止覆盖 Angular 框架的内置功能,Angular 生态中略有规模的第三方库也同样支持这种能力,让你可以无脑升级,安心升级。这是 Angular 工程化能力的极致体现。

最后,Angular 既老且新。#

说它「老」,是因为 Angular 采用了大量有几十年历史的、经历过岁月重重考验的成熟软件工程实践,比如依赖注入、面向接口编程、关注点分离等。难能可贵的是,Angular 从未给它们发明新的名词进行包装,要知道,前端圈最喜欢做的事可能就是发明新名词了。

说它「新」,是因为 Angular 热情拥抱其它上下游技术,并积极追随标准。从最初选择与少年时期的 TypeScript 合作,到与 RxJS 的深度整合,再到近年来,它更是以壮士断腕的决心,引入了 Signals 这一更精细的响应式模型,并大刀阔斧地推行独立组件,朝着更简洁、更高效、甚至无 Zone.js 的未来迈进。

结语#

工程化的本质目的就是支持团队化开发并帮助系统抵抗时间的侵蚀。Angular 展现出的这些工程之美 —— 无论是其稳定的软件工程内核,还是其不断进化的现代化「形态」 —— 都让我对 Angular 的未来充满信心。

周虽旧邦,其命维新!