第4章 依赖注入与服务模式面试
核心考点浓缩: DI原理 → Provider配置(useClass / useValue / useFactory / useExisting) → 单例 vs 工厂 → 可摇树优化(providedIn: 'root' / 'any')。本章覆盖80%的Angular架构师DI面试原题。
面试站位提示: DI是Angular的“脊柱”,面试官会从“如何控制服务作用域”到“Tree‑shakable providers”层层追问。建议先用一句话说清DI本质,再用Provider配置展示技术深度。
---
---
### 面试问答卡片 ①
**面试问题:** 请解释 Angular 依赖注入的原理,并说明 Injector 层级的作用。
| 考察点 | 说明 |
|--------|------|
| 底层理解 | 是否清楚DI是“实例化+生命周期+作用域”管理器 |
| 层级概念 | 能否区分 ModuleInjector / ElementInjector / NullInjector |
| 实际经验 | 是否遇到过多层级注入的坑(如循环依赖) |
**回答框架:**
DI本质 → Injector树结构 → 查找优先级(自下而上,直到NullInjector抛出错误) → 举例说明(如组件内注入服务但未提供,会向上查找)。
**示例回答:**
“Angular DI 通过 Injector 树管理依赖。组件自身 Injector 未提供服务时,依次向父模块、根模块查找,最终到 NullInjector 抛错。我曾在懒加载模块中将服务写在 forChild 内,造成服务实例分裂,改用 providedIn: 'root' 解决。”
**可能追问:**
- ModuleInjector 和 ElementInjector 的区别?
- @Injectable({providedIn:'platform'}) 与 'root' 的区别?
**避坑提醒:**
不要只说“依赖注入就是传参”,要强调 Injector 树和 singleton 归属。避免把 providedIn 和 providers 混为一谈。
---
### 面试问答卡片 ②
**面试问题:** Provider 有哪几种配置方式?实际开发中如何选择?
| 考察点 | 说明 |
|--------|------|
| 配置多样性 | 是否全面掌握 useClass / useValue / useExisting / useFactory |
| 场景适配 | 能否根据配置化、Mock、动态参数选择合适方式 |
| 运行效率 | 是否了解 useExisting 的别名复用机制 |
**回答框架:**
列举4种方式 → 分别给出典型场景(useClass 默认,useValue 用于配置对象,useExisting 用于别名/接口多态,useFactory 用于需依赖其他服务才可构造的情况) → 总结选择原则。
**示例回答:**
“useClass 最常用,如 ApiService;useValue 适合配置常量;useExisting 用于日志服务别名;useFactory 用于需要动态参数或依赖多个服务时才实例化的场景。例如我曾在环境配置中使用 APP_CONFIG = { apiUrl: '...' } 注入。”
**可能追问:**
- useFactory 中 deps 参数怎么写?
- useClass 和 useExisting 在多模块下实例相同吗?
**避坑提醒:**
不要在懒加载模块中使用 useClass 配置同一服务,会导致多实例;如果期望多实例,故意在模块的 providers 中声明。另外 useFactory 注意闭包陷阱。
---
### 面试问答卡片 ③
**面试问题:** 如何实现单例服务和工厂服务?Angular 的可摇树优化如何生效?
| 考察点 | 说明 |
|--------|------|
| 单例控制 | 是否知道 providedIn: 'root' 与根模块 providers 的区别 |
| 工厂模式 | 能否用 useFactory 或 @Injectable() 配合 factory 函数 |
| 树摇优化 | 理解 Tree‑shakable providers 的编译原理(dead code elimination) |
**回答框架:**
单例:providedIn: 'root'(推荐) vs 根模块 providers(传统) → 工厂:useFactory 或提供工厂类 → 摇树优化:未引用的服务不会被打包;providedIn 使编译期直接分析引用关系。
**示例回答:**
“使用 @Injectable({providedIn: 'root'}) 声明单例,Angular CLI 在构建时会自动移除未被任何组件或模块注入的服务代码。若需多实例,在组件的 providers 声明。工厂方式用 useFactory: (dep) => new Service(dep),并声明 deps。”
**可能追问:**
- providedIn: 'any' 与 'root' 的区别?
- 如何强制实现模块级单例(只在某懒加载模块内唯一)?
**避坑提醒:**
不要在全模块 providers 和 providedIn 混用时忘记重叠;懒加载模块中用 providers 会导致子模块多实例。摇树优化要求服务不被直接字符串引用,尽量使用类 。
---
⚠️ 常见追问变形: “如果两个懒加载模块都提供了同一个服务,它们的实例是否相同?” 答案:实例不同,除非该服务声明为 providedIn: 'root'。面试官常用此测试你对作用域的理解。
---
## 项目案例
**项目名称:** 企业级权限管理系统(RBAC + 可配置菜单)
**业务背景:** 为集团公司内部开发的多租户权限中心,需统一管理角色、菜单、按钮级权限。前端需要支持动态菜单生成、权限指令、服务层统一鉴权。
**规模指标:**
- 共计 28 个功能模块(14 个懒加载模块)
- 日均请求 12 万次,权限校验服务被 170+ 组件注入
- 代码量约 45 万行
**技术栈:** Angular 15 + NgRx + Angular Material + 自定义指令 + 工厂模式 Provider
**核心难点:**
1. 多租户下权限配置动态变化,需要 useFactory 在应用启动时从后端拉取权限数据然后注入。
2. 避免权限服务在懒加载模块中产生多个实例,需统一 providedIn: 'root'。
3. 摇树优化:未使用的权限指令、服务自动从 dist 包中剔除。
**面试高频追问:**
- Q:权限服务中的 useFactory 如何设计?
- Q:动态菜单组件如何利用依赖注入实现多态?
- Q:如何保证权限服务在微前端场景下也是单例?
**可迁移回答话术:**
“我在权限系统中用 useFactory 动态配置权限服务:`useFactory: (config) => new PermissionService(config),deps: [APP_CONFIG]`,然后 APP_CONFIG 在根模块用 useValue 注入。所有懒加载模块共用同一个 PermissionService 实例,因为 providedIn: 'root' 保证单例。对于摇树优化,我们将权限指令中未使用的角色判断分支通过 Angular CLI 的 sideEffects 标记剔除。”
---
---
Provider 配置类型速查表
| 配置类型 | 典型用法 | 实例数 | 适用场景 |
|---|---|---|---|
useClass |
{ provide: ApiService, useClass: MockApiService } |
每个 Injector 一个 | Mock、环境切换 |
useValue |
{ provide: APP_CONFIG, useValue: { apiUrl } } |
常量副本 | 配置、常量 |
useExisting |
{ provide: Logger, useExisting: ConsoleLogger } |
共享单例 | 别名、多态 |
useFactory |
{ provide: MyService, useFactory: (dep) => ..., deps: [Dep] } |
按工厂逻辑/次 | 动态构造、依赖多服务 |
面试加分点: 提到
providedIn: 'platform' 可让服务在微前端的不同 Angular 实例间共享;提到 @Injectable() 的 providedIn: 'any' 在每个懒加载模块中创建独立实例(与 'root' 相反)。