写了五年Angular才敢说:依赖注入玩得越花,排查故障时越像盲人摸象
摘要:Angular依赖注入灵活但易失控,层级注入、工厂提供者与修饰符组合会让依赖图难以静态还原。文章结合真实排查困境,提出收敛令牌、显式作用域、克制工厂与成对使用修饰符的边界原则。
Angular 的依赖注入大概是主流前端框架里最“重”的一套设计。它不像 React 那样把依赖当作普通参数随手传递,也不像 Vue 的 provide/inject 那样轻量到近乎语法糖。Angular 把它做成了容器、层级、令牌、装饰器、生命周期钩子交织的一整套体系。写第一年的时候,觉得它啰嗦;写到第三年,觉得它优雅;写到第五年,才敢承认一个不太体面的事实:这套东西玩得越花,线上出问题时你越像盲人摸象——摸到的每一块都真实,拼起来却未必是那头象。
这不是在否定依赖注入本身。恰恰相反,DI 是 Angular 能在大型团队里撑住场面的核心原因之一。问题出在“玩得花”这三个字上。层级注入、`@Host`、`@Self`、`@SkipSelf`、`@Optional`、工厂提供者、`useExisting` 别名、`InjectionToken` 配合工厂函数、路由级与组件级双重覆盖……这些能力单独看都有明确用途,组合起来却能构造出一张连作者自己都难以在脑中还原的依赖图。而故障排查最怕的,就是依赖图无法被静态还原。
先看一个典型场景。某个共享组件在开发环境表现正常,进了某个懒加载路由后开始报空指针。第一反应是数据没到,加日志、加判空、加兜底,改了三轮都没解决。最后发现是组件级 providers 里用 `useExisting` 指向了一个父级已存在的服务,而该父级在懒加载边界之外被另一条路由以不同配置初始化过。依赖没有缺失,只是拿到的是另一个实例。这种问题在代码审查里几乎不可见,因为每一处写法都“合法”,坏在组合。
Angular 的注入器是分层的,模块、路由、组件各自可以形成作用域。分层本身是为了隔离与复用,但它同时意味着同一个令牌在不同位置可能解析到不同实例。当团队规模上来、模块被多个业务线复用时,这种“同名不同物”的隐式差异会迅速累积。排查时你在组件里看到注入成功,在服务里看到注入成功,在拦截器里也看到注入成功,可它们未必是同一个对象。盲人摸象的比喻在这里非常贴切:每个局部观察都成立,整体行为却对不上。
更隐蔽的是工厂提供者。`useFactory` 允许在运行时根据配置、环境、甚至异步结果决定返回什么。灵活性极高,代价是依赖关系从声明式变成了命令式。静态分析工具读不到工厂内部引用了谁,打包器也未必能正确摇树。一旦工厂里出现了条件分支,同一个令牌的解析路径就有了多条。线上出问题时,你甚至无法确定当前走的是哪一条,除非把工厂逻辑完整重放一遍。相比之下,直接用构造函数注入具体类型虽然不够灵活,却让依赖关系一目了然。
装饰器修饰符的滥用是另一个重灾区。`@Optional` 让缺失的依赖静默变成 null,`@Self` 限制只在当前注入器查找,`@SkipSelf` 跳过当前层。这些修饰符在解决特定边界问题时非常有效,但它们把“依赖是否存在”从编译期问题推到了运行期问题。一个被 `@Optional` 包裹的依赖缺失时不会报错,只会在某个分支里悄悄失效。等到用户反馈某个功能不生效,你已经很难从堆栈里看出根因,因为错误根本没有抛出。
那是不是应该回到最朴素的写法?也不必走极端。依赖注入的价值在于可测试性与可替换性,完全放弃等于把大型项目的可维护性拱手让人。真正需要的是给“花活”设定边界。第一,令牌要收敛。能用具体类就别造 `InjectionToken`,能一个令牌解决就别拆成三个语义相近的。第二,作用域要显式。组件级 providers 只在确实需要独立实例时使用,并且写清楚为什么需要,而不是顺手加上。第三,工厂要克制。工厂里只做轻量的选择逻辑,不要在工厂里发起请求、读取全局状态或依赖时序。第四,修饰符要成对出现。用了 `@Optional` 就必须在消费处显式处理 null,而不是假设它一定存在。
工具层面也有可做的事。Angular 的编译期检查能覆盖一部分注入错误,但覆盖不了运行时分支。团队可以约定:凡是使用 `useFactory`、`useExisting` 或注入修饰符的地方,必须在同一文件内留下注释说明意图与失效场景。这不是形式主义,而是在为未来的排查者保留还原依赖图的线索。另一个有效做法是把关键服务的实例身份在开发环境打印出来,用对象引用或自增 id 标识,让“是不是同一个实例”这个问题在日志里就能回答,而不是靠猜。
说到底,依赖注入是一种把复杂度前移的设计。写的时候多花心思声明清楚,排查时就少走弯路;写的时候图省事玩花活,排查时就只能靠运气。五年下来最大的体会不是“DI 不好用”,而是“DI 的灵活性必须配合同等强度的约束”。框架给了你一把很锋利的刀,切菜还是切手,取决于你有没有给它配个刀鞘。盲人摸象的困境不是象的错,也不是盲人的错,是没人愿意先把象画出来。写代码的人,最好就是那个画象的人。