文章目录

Angular 依赖注入:DI 系统与服务

发布于 2026-04-05 02:30:52 · 浏览 11 次 · 评论 0 条

Angular 依赖注入:DI 系统与服务


依赖注入(Dependency Injection,简称 DI)是 Angular 框架最核心的概念之一。它不是 Angular 独有的技术,但 Angular 将其发挥到了极致,成为构建可维护、可测试应用的基石。本文将深入解析 Angular 的 DI 系统,帮你彻底掌握这一关键机制。


一、为什么需要依赖注入

1.1 传统模式的困境

在没有依赖注入的情况下,组件之间需要相互协作时,通常会在组件内部直接创建依赖对象的实例。考虑以下场景:一个 UserService 需要调用 HTTP 接口获取用户数据,而 UserComponent 需要使用这个服务。

传统写法是这样的:

// user.service.ts
export class UserService {
  constructor() {
    // 直接在内部创建 HttpClient 实例
    this.http = new HttpClient();
  }

  getUsers() {
    return this.http.get('/api/users');
  }
}

// user.component.ts
export class UserComponent {
  private service: UserService;

  constructor() {
    // 组件内部直接创建服务实例
    this.service = new UserService();
  }
}

这种写法存在几个严重问题。首先是紧耦合问题UserService 内部直接依赖 HttpClient 的具体实现,如果想换一种 HTTP 实现方式,必须修改 UserService 的源代码。其次是测试困难,在单元测试时,无法轻易替换 HttpClient 为 mock 对象,导致测试用例难以编写。最后是职责混乱,类同时承担了业务逻辑和对象创建的职责,违背了单一职责原则。

1.2 依赖注入的解决思路

依赖注入的核心思想是:类不应该自己创建依赖对象,而应该从外部接收依赖对象。这个"外部"就是注入器(Injector)。

采用依赖注入后,代码变成这样:

// user.service.ts
export class UserService {
  constructor(private http: HttpClient) {}

  getUsers() {
    return this.http.get('/api/users');
  }
}

// user.component.ts
export class UserComponent {
  constructor(private userService: UserService) {}
}

现在,UserService 不再关心 HttpClient 是如何创建的,UserComponent 也不关心 UserService 是如何创建的。所有对象的创建和组装工作交给 DI 系统处理。


二、Angular DI 系统的核心概念

Angular 的 DI 系统由三个核心角色组成:注入器(Injector)、提供者(Provider)和依赖(Dependency)。

2.1 注入器:对象的制造工厂

注入器是 Angular DI 系统的核心,相当于一个对象工厂。当某个类需要依赖时,向注入器发出请求,注入器负责创建并返回所需的实例。

Angular 应用启动时,会自动创建一个根注入器。这个注入器负责管理整个应用中的依赖关系。随着组件和模块的创建,还会形成注入器的层次结构,这在后续章节会详细讲解。

2.2 提供者:告诉注入器如何创建对象

提供者描述了注入器应该如何创建某个依赖。它告诉注入器:"当有人请求这个服务时,请按照这种方式给我一个实例"。

Angular 提供了多种定义提供者的方式,最常用的是 useClassuseValueuseClass 指定用哪个类来创建实例,useValue 则直接提供一个具体的值。

// 使用 useClass 创建提供者
{ provide: UserService, useClass: UserServiceImpl }

// 使用 useValue 提供值
{ provide: API_URL, useValue: 'https://api.example.com' }

2.3 依赖:类之间的关联关系

依赖是指一个类在构造函数中声明需要使用的其他类或值。当 Angular 实例化某个类时,会检查它的构造函数,识别出所有依赖需求,然后从注入器中获取这些依赖并注入进去。


三、服务的创建与注册

3.1 创建服务

在 Angular 中,服务就是一个普通的 TypeScript 类,只是通常不包含模板相关的逻辑。创建一个服务需要使用 @Injectable() 装饰器,这个装饰器至关重要,它标记该类可以被注入器管理。

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'  // 这个配置很重要,下文会解释
})
export class LoggingService {
  log(message: string): void {
    console.log(`[LOG]: ${message}`);
  }
}
```

`@Injectable()` 装饰器有一个 `providedIn` 属性,它决定了服务由哪个注入器提供。`providedIn: 'root'` 表示该服务是应用级别的单例,由根注入器管理。这是最推荐的配置方式,因为它支持树摇优化(Tree Shaking),未被使用的服务不会被包含在最终打包中。

### 3.2 注册服务的方式

**方式一:使用 `providedIn`(推荐)**

这种方式直接在服务类上声明,不需要在模块或组件中显式注册:

```typescript
@Injectable({
  providedIn: 'root'        // 根注入器,整个应用单例
  // providedIn: 'platform'  // 平台注入器,多个应用共享
  // providedIn: 'any'       // 每个懒加载模块都有自己的实例
})
export class DataService {}
```

**方式二:在模块中注册**

在模块的 `providers` 数组中注册服务:

```typescript
@NgModule({
  providers: [
    UserService,
    { provide: ConfigService, useClass: ConfigServiceImpl }
  ]
})
export class AppModule {}
```

**方式三:在组件中注册**

在组件的 `providers` 数组中注册服务,该服务只在该组件及其子组件中可见:

```typescript
@Component({
  selector: 'app-user',
  template: `<user-list></user-list>`,
  providers: [UserService]  // 这个组件及其子组件共享这个服务实例
})
export class UserComponent {}
```

### 3.3 服务的作用域与生命周期

服务实例的生命周期与注册它的注入器紧密相关。理解这一点对于避免常见的 bug 至关重要。

当服务注册在根注入器(`providedIn: 'root'`)时,该服务是应用级别的单例,整个应用共享同一个实例,从应用启动到关闭始终存在。

当服务注册在组件的 `providers` 数组中时,该服务是组件级别的实例。每次创建该组件的新实例时,都会创建一个新的服务实例。这个组件的所有子组件共享这个服务实例,但兄弟组件之间的实例是独立的。

---

## 四、依赖注入的完整流程

让我们通过一个完整的例子,梳理 Angular DI 的工作流程。

```typescript
// 1. 定义日志服务接口
export interface ILogger {
  log(message: string): void;
}

// 2. 实现日志服务
@Injectable()
export class ConsoleLogger implements ILogger {
  log(message: string): void {
    console.log(`[Console]: ${message}`);
  }
}

// 3. 使用日志服务的业务服务
@Injectable({
  providedIn: 'root'
})
export class OrderService {
  constructor(private logger: ILogger) {}

  createOrder(order: Order): void {
    this.logger.log(`Creating order: ${order.id}`);
    // 业务逻辑...
  }
}

// 4. 使用业务服务的组件
@Component({
  selector: 'app-order',
  template: `<button (click)="addOrder()">Add Order</button>`
})
export class OrderComponent {
  constructor(private orderService: OrderService) {}

  addOrder(): void {
    this.orderService.createOrder({ id: 1 });
  }
}

上述代码的执行流程如下:

  1. 应用启动:Angular 创建根注入器。
  2. 扫描依赖:Angular 发现 OrderService 依赖 ILogger 接口。
  3. 查找提供者:Angular 在注入器中查找 ILogger 的提供者。
  4. 创建实例:Angular 创建 ConsoleLogger 实例,并将其注入到 OrderService 的构造函数中。
  5. 组件注入:当创建 OrderComponent 时,Angular 找到已有的 OrderService 单例(根注入器中),将其注入到组件中。

整个过程中,组件不需要知道服务是如何创建的,也不需要知道依赖是如何解析的,只需要声明自己需要什么,Angular 的 DI 系统会自动完成剩余工作。


五、注入器的层次结构

Angular 的注入器不是扁平的,而是形成了一个树形层次结构。这个特性是理解 Angular DI 的关键,也是避免"为什么我的服务不共享"这类问题的钥匙。

5.1 注入器树的结构

Angular 应用中存在三种类型的注入器,它们形成如下的层次关系:

根注入器 (root injector)
├── 根组件注入器 (root component injector)
│   ├── 子组件A注入器
│   │   └── 深层子组件注入器
│   └── 子组件B注入器
└── 懒加载模块注入器
    └── 该模块内的组件注入器

当 Angular 需要解析某个依赖时,会按照从下到上的顺序在注入器链中查找:首先检查当前组件的注入器,然后是父组件的注入器,直到根注入器。一旦找到匹配的提供者,就停止搜索并返回实例。

5.2 实际应用场景

假设我们有这样一个组件树结构:AppComponentParentComponentChildComponent。我们在不同层级注册了同一个服务:

// 根组件中注册服务
@Component({
  selector: 'app-root',
  template: `<app-parent></app-parent>`,
  providers: [SharedService]  // 根组件级别的实例
})
export class AppComponent {}

// 父组件中注册同名服务
@Component({
  selector: 'app-parent',
  template: `<app-child></app-child>`,
  providers: [SharedService]  // 父组件级别的实例,会"遮蔽"根组件的实例
})
export class ParentComponent {}

// 子组件中不注册,使用最近的提供者的实例
@Component({
  selector: 'app-child',
  template: `<div>{{ sharedData }}</div>`
})
export class ChildComponent {
  constructor(public sharedService: SharedService) {
    // 这里得到的是 ParentComponent 注入器中的实例
    // 而不是 AppComponent 注入器中的实例
  }
}

在这个例子中,ChildComponent 获取到的是 ParentComponent 注入器中注册的 SharedService 实例,而不是 AppComponent 注入器中的实例。这就是注入器的查找机制。

5.3 何时使用组件级别注入器

组件级别注入器适用于以下场景:需要为组件及其子组件提供独立的服务实例,常见于以下情况:

多实例组件时,每个标签页、每个列表项需要独立的状态管理。例如,一个标签页组件管理自己的标签列表,多个标签页之间不应该共享同一个标签列表数据。

隔离第三方服务时,当使用第三方库时,将其服务注册在组件级别可以避免对其他组件造成意外影响,便于隔离测试和替换。


六、进阶技巧与最佳实践

6.1 使用 useFactory 创建动态依赖

有时候服务的创建逻辑比较复杂,无法用简单的类来描述。这时可以使用工厂提供者:

// 根据环境决定使用哪个日志服务
export function loggerFactory(isDebug: boolean): ILogger {
  return isDebug ? new DebugLogger() : new ProductionLogger();
}

// 在模块中注册工厂提供者
providers: [
  {
    provide: ILogger,
    useFactory: () => loggerFactory(environment.debugMode)
  }
]

工厂函数还可以接收参数,通过 deps 属性声明依赖:

{
  provide: LoggerService,
  useFactory: (config: ConfigService, http: HttpClient) => {
    return new LoggerServiceImpl(config, http);
  },
  deps: [ConfigService, HttpClient]
}

6.2 使用 useExisting 实现别名

useExisting 可以将一个服务别名到另一个服务,这在重构或者接口适配时非常有用:

providers: [
  NewLoggerService,
  {
    provide: OldLoggerService,
    useExisting: NewLoggerService  // 两者指向同一个实例
  }
]

6.3 注入抽象类或接口

TypeScript 的类型信息在运行时会被擦除,所以不能直接注入接口。Angular 提供了 InjectionToken 来解决这个问题:

import { InjectionToken } from '@angular/core';

// 定义 token
export const LOGGER_TOKEN = new InjectionToken<ILogger>('Logger');

// 在模块中注册
providers: [
  {
    provide: LOGGER_TOKEN,
    useClass: ConsoleLogger
  }
];

// 在组件中注入
constructor(@Inject(LOGGER_TOKEN) private logger: ILogger) {}

6.4 可选依赖

如果某个依赖不是必需的,可以使用 @Optional() 装饰器。当找不到对应的提供者时,注入器会返回 null 而不是报错:

constructor(
  @Optional() private logger: ILogger
) {
  if (this.logger) {
    this.logger.log('Service initialized');
  }
}

6.5 自定义注入器装饰器

除了使用 @Optional(),Angular 还提供了其他注入装饰器:

@Self() 表示只从当前组件的注入器中查找依赖,不向上查找父组件的注入器。

@SkipSelf() 表示跳过当前组件的注入器,从父组件的注入器开始查找。

constructor(
  @Self() private localService: LocalService,  // 只在当前组件查找
  @SkipSelf() private parentService: ParentService  // 从父组件开始查找
) {}

七、常见问题与解决方案

7.1 服务不共享的问题

症状:同一个服务在不同的组件中表现不一致,像是不同的实例。

原因:服务被注册在组件级别的注入器中,而不是根注入器中。

解决:将服务的 providedIn 设置为 'root',或者将服务移到模块的 providers 数组中:

// 错误写法
@Component({ providers: [DataService] })
export class MyComponent {}

// 正确写法
@Injectable({ providedIn: 'root' })
export class DataService {}

7.2 循环依赖问题

症状RangeError: Maximum call stack size exceeded 或类似错误。

原因:两个服务相互依赖,形成循环引用。

解决:使用 forwardRef 延迟引用:

@Injectable({ providedIn: 'root' })
export class ServiceA {
  constructor(@Optional() private serviceB: ServiceB) {}
}

@Injectable({ providedIn: 'root' })
export class ServiceB {
  constructor(@Optional() private serviceA: ServiceA) {}
}

如果循环依赖无法避免,可以将其中一个服务改为工厂注入,或者提取公共部分到第三个服务。

7.3 找不到提供者错误

症状NullInjectorError: No provider for XXX!

原因:需要的服务没有在任何注入器中注册。

解决:确认服务已正确注册。如果使用 providedIn: 'root',检查 @Injectable() 装饰器是否正确应用。


八、总结

Angular 的依赖注入系统是一个强大而灵活的工具。掌握它需要理解三个核心概念:注入器负责创建对象,提供者描述创建方式,依赖声明需要什么。服务是 DI 的主要应用载体,通过 @Injectable() 装饰器和 providedIn 属性管理其作用域。

注入器的层次结构是 Angular DI 的高级特性,理解这一机制可以帮你更好地控制服务实例的生命周期。进阶技巧如 useFactoryInjectionToken 和各种注入装饰器,为复杂场景提供了灵活的解决方案。

最佳实践是:优先使用 providedIn: 'root' 注册服务,保持服务的单例性;使用接口和 InjectionToken 抽象依赖,提高代码灵活性;合理使用组件级别注入器,避免不必要的复杂性。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文