Angular依赖注入机制深入浅出
一、Angular 依赖注入基础概念
1.1 什么是依赖注入
在软件开发中,依赖注入(Dependency Injection,简称 DI)是一种设计模式,它通过将对象所依赖的其他对象(依赖)通过外部传递进来,而不是在对象内部创建这些依赖。这有助于提高代码的可测试性、可维护性和可扩展性。
在 Angular 中,依赖注入是其核心特性之一。它允许开发者将组件、服务等对象所依赖的其他对象(如服务、值、函数等)以声明的方式注入到需要它们的地方,而不是让这些对象自己去创建或查找依赖。
1.2 依赖注入的优势
- 可测试性:通过依赖注入,我们可以轻松地为测试目的替换真实的依赖为模拟对象。例如,在测试一个组件时,如果该组件依赖一个后端服务来获取数据,我们可以注入一个模拟的服务,这个模拟服务返回预先定义的数据,从而使测试不受真实后端服务的影响,更稳定和可重复。
- 可维护性:当依赖关系发生变化时,例如需要更换一个服务的实现,只需要在注入的地方进行修改,而不需要在所有使用该依赖的地方修改代码。这使得代码的维护变得更加容易,尤其是在大型项目中。
- 可扩展性:依赖注入使得代码更加灵活,易于扩展。可以在不改变现有代码结构的情况下,引入新的依赖或替换现有依赖,以满足不同的业务需求。
二、Angular 依赖注入的关键概念
2.1 注入器(Injector)
注入器是 Angular 依赖注入系统的核心。它负责创建、查找和提供依赖对象。每个注入器都维护一个自己的依赖映射表,该表记录了它所管理的依赖令牌(Token)和对应的依赖实例。
当一个组件或服务请求一个依赖时,注入器会首先在自己的依赖映射表中查找。如果找到了匹配的依赖实例,就直接返回该实例;如果没有找到,它会尝试向其父级注入器查找。如果所有的注入器都找不到该依赖,就会抛出一个错误。
在 Angular 应用中,有一个根注入器(Root Injector),它是整个应用中所有注入器的顶级注入器。根注入器负责创建和管理应用中最顶层的依赖,如全局服务等。同时,每个组件都可以有自己的注入器(称为组件注入器),组件注入器继承自其父级注入器(通常是父组件的注入器或根注入器),并可以在其基础上管理自己的依赖。
2.2 令牌(Token)
令牌是用于标识依赖的对象。在 Angular 中,令牌可以是以下几种类型:
- 类(Class):最常见的令牌类型。当我们使用一个类作为令牌时,Angular 会使用这个类来创建依赖实例。例如,我们有一个
UserService
类,我们可以直接使用UserService
作为令牌来注入该服务的实例。 - 字符串(String):可以使用字符串作为自定义令牌。这种方式通常用于注入一些简单的值或服务,并且这些值或服务没有对应的类。例如,我们可以定义一个字符串令牌
'apiUrl'
,并使用它来注入 API 的 URL。 - InjectionToken 类的实例:
InjectionToken
是 Angular 提供的一个专门用于创建令牌的类。它可以创建一个唯一的令牌,特别是在需要注入一些非类类型的对象,或者需要在不同地方使用相同类型但不同实例的依赖时非常有用。例如:
import { InjectionToken } from '@angular/core';
export const MY_CUSTOM_TOKEN = new InjectionToken<string>('My Custom Token');
2.3 提供者(Provider)
提供者用于告诉注入器如何创建和提供依赖对象。提供者通常由一个对象来表示,该对象包含了令牌和创建依赖实例的逻辑。常见的提供者类型有:
- 类提供者(Class Provider):使用类作为令牌,并使用该类的构造函数来创建依赖实例。例如:
import { NgModule } from '@angular/core';
import { UserService } from './user.service';
@NgModule({
providers: [UserService]
})
export class AppModule {}
这里 UserService
既是令牌也是用于创建实例的类。
- 值提供者(Value Provider):用于提供一个已经创建好的值,而不是通过类的构造函数来创建。例如:
import { NgModule } from '@angular/core';
import { InjectionToken } from '@angular/core';
export const apiUrl = new InjectionToken<string>('API URL');
@NgModule({
providers: [
{
provide: apiUrl,
useValue: 'https://example.com/api'
}
]
})
export class AppModule {}
这里使用 useValue
来指定要提供的值。
- 工厂提供者(Factory Provider):通过一个工厂函数来创建依赖实例。这种方式适用于需要更复杂的创建逻辑,或者依赖于其他已有的依赖。例如:
import { NgModule } from '@angular/core';
import { InjectionToken } from '@angular/core';
export const SOME_SERVICE = new InjectionToken<string>('Some Service');
function createSomeService(): string {
// 复杂的创建逻辑
return 'Some value created by factory';
}
@NgModule({
providers: [
{
provide: SOME_SERVICE,
useFactory: createSomeService
}
]
})
export class AppModule {}
这里通过 useFactory
来指定工厂函数。
- 别名提供者(Alias Provider):用于为一个已有的令牌提供一个别名。例如:
import { NgModule } from '@angular/core';
import { UserService } from './user.service';
import { AnotherUserService } from './another - user.service';
@NgModule({
providers: [
{
provide: UserService,
useExisting: AnotherUserService
}
]
})
export class AppModule {}
这里 UserService
成为了 AnotherUserService
的别名,当请求 UserService
时,实际上会得到 AnotherUserService
的实例。
三、在 Angular 组件和服务中使用依赖注入
3.1 在服务中注入依赖
假设我们有一个 LoggerService
用于记录日志,还有一个 UserService
需要使用 LoggerService
来记录用户相关的操作。
首先,创建 LoggerService
:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class LoggerService {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
然后,创建 UserService
并注入 LoggerService
:
import { Injectable } from '@angular/core';
import { LoggerService } from './logger.service';
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor(private logger: LoggerService) {}
getUser() {
this.logger.log('Getting user...');
// 实际获取用户的逻辑
return { name: 'John Doe' };
}
}
在 UserService
的构造函数中,通过 private logger: LoggerService
声明了对 LoggerService
的依赖,Angular 会自动注入 LoggerService
的实例。
3.2 在组件中注入服务
假设我们有一个 UserComponent
需要使用 UserService
来获取用户信息并显示。
import { Component } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app - user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.css']
})
export class UserComponent {
user;
constructor(private userService: UserService) {}
ngOnInit() {
this.user = this.userService.getUser();
}
}
在 UserComponent
的构造函数中,注入了 UserService
,然后在 ngOnInit
生命周期钩子中调用 UserService
的 getUser
方法获取用户信息。
四、依赖注入的作用域
4.1 根作用域(Root Scope)
当一个服务在根模块(AppModule
)中通过 providers
数组提供,或者在服务类上使用 @Injectable({ providedIn: 'root' })
时,该服务处于根作用域。根作用域的服务在整个应用中只有一个实例,所有需要该服务的组件和服务都共享这个实例。
例如,上面的 LoggerService
和 UserService
都处于根作用域,无论在哪个组件或服务中注入它们,得到的都是同一个实例。
4.2 组件作用域(Component Scope)
如果我们想为每个组件创建一个独立的服务实例,可以将服务提供在组件的 providers
数组中。例如:
import { Component } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app - user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.css'],
providers: [UserService]
})
export class UserComponent {
user;
constructor(private userService: UserService) {}
ngOnInit() {
this.user = this.userService.getUser();
}
}
在这种情况下,每个 UserComponent
实例都有自己独立的 UserService
实例。这在某些场景下非常有用,比如每个组件需要维护自己独立的状态,而不是共享全局的状态。
4.3 模块作用域(Module Scope)
在 Angular 模块(除根模块外)的 providers
数组中提供的服务,具有模块作用域。在该模块及其子模块中的组件和服务共享这些服务实例,但与其他模块中的实例是隔离的。
例如,我们有一个 FeatureModule
:
import { NgModule } from '@angular/core';
import { UserService } from './user.service';
@NgModule({
providers: [UserService]
})
export class FeatureModule {}
在 FeatureModule
及其子模块中的组件和服务会共享同一个 UserService
实例,而与 AppModule
或其他模块中的 UserService
实例不同。
五、高级依赖注入技巧
5.1 多级注入器和依赖查找顺序
在 Angular 应用中,注入器形成了一个树形结构。根注入器位于树的顶端,每个组件注入器是其子节点。当一个组件请求一个依赖时,注入器的查找顺序如下:
- 组件自身的注入器查找。如果找到了依赖实例,就返回该实例。
- 如果组件自身的注入器没有找到,就向其父组件的注入器查找。这个过程会一直向上进行,直到根注入器。
- 如果根注入器也没有找到依赖,就会抛出一个错误。
例如,我们有一个 ParentComponent
和一个 ChildComponent
,ChildComponent
依赖一个 SharedService
。如果 ChildComponent
自身的注入器没有提供 SharedService
,它会向 ParentComponent
的注入器查找,如果 ParentComponent
的注入器也没有,就会向根注入器查找。
5.2 使用 InjectionToken 注入复杂类型
InjectionToken
不仅可以用于创建字符串类型的令牌,还可以用于注入复杂类型,如函数、对象等。例如,假设我们有一个函数,用于格式化日期,我们想通过依赖注入来使用它。
首先,创建 InjectionToken
:
import { InjectionToken } from '@angular/core';
export const DATE_FORMATTER = new InjectionToken<(date: Date) => string>('Date Formatter');
然后,提供这个函数:
import { NgModule } from '@angular/core';
import { DATE_FORMATTER } from './date - formatter.token';
function formatDate(date: Date): string {
return date.toISOString();
}
@NgModule({
providers: [
{
provide: DATE_FORMATTER,
useValue: formatDate
}
]
})
export class AppModule {}
最后,在组件中注入并使用:
import { Component } from '@angular/core';
import { DATE_FORMATTER } from './date - formatter.token';
@Component({
selector: 'app - date - component',
templateUrl: './date - component.html'
})
export class DateComponent {
constructor(@Inject(DATE_FORMATTER) private dateFormatter: (date: Date) => string) {}
format() {
const now = new Date();
return this.dateFormatter(now);
}
}
这里通过 InjectionToken
注入了一个函数,使得代码更加灵活和可维护。
5.3 条件注入
有时候,我们可能需要根据不同的条件注入不同的依赖。例如,在开发环境中,我们可能想使用一个模拟的服务来进行快速开发和测试,而在生产环境中使用真实的服务。
我们可以通过在模块的 providers
数组中根据环境变量来动态地提供不同的服务。假设我们有一个 environment.ts
文件来存储环境变量:
export const environment = {
production: false
};
然后,在 AppModule
中:
import { NgModule } from '@angular/core';
import { environment } from '../environments/environment';
import { RealUserService } from './real - user.service';
import { MockUserService } from './mock - user.service';
@NgModule({
providers: [
{
provide: 'UserService',
useClass: environment.production? RealUserService : MockUserService
}
]
})
export class AppModule {}
这样,在开发环境中会注入 MockUserService
,而在生产环境中会注入 RealUserService
。
六、依赖注入与测试
6.1 测试组件时模拟依赖
在测试组件时,通常需要模拟其依赖,以确保测试的独立性和稳定性。例如,在测试 UserComponent
时,我们不想依赖真实的 UserService
,因为它可能涉及网络请求或其他复杂操作。
我们可以使用 Angular 的 TestBed
来配置测试环境,并提供模拟的 UserService
。
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserComponent } from './user.component';
import { UserService } from './user.service';
describe('UserComponent', () => {
let component: UserComponent;
let fixture: ComponentFixture<UserComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [UserComponent],
providers: [
{
provide: UserService,
useValue: {
getUser: () => ({ name: 'Mock User' })
}
}
]
});
fixture = TestBed.createComponent(UserComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should display the user name', () => {
expect(component.user.name).toBe('Mock User');
});
});
这里通过 TestBed
的 providers
数组提供了一个模拟的 UserService
,使得测试可以独立运行,不受真实 UserService
的影响。
6.2 测试服务时注入依赖
当测试一个依赖其他服务的服务时,同样需要正确地注入依赖。例如,测试 UserService
,它依赖 LoggerService
。
import { TestBed } from '@angular/core/testing';
import { LoggerService } from './logger.service';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
let loggerService: LoggerService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [UserService, LoggerService]
});
service = TestBed.inject(UserService);
loggerService = TestBed.inject(LoggerService);
});
it('should log when getting user', () => {
spyOn(loggerService, 'log');
service.getUser();
expect(loggerService.log).toHaveBeenCalledWith('Getting user...');
});
});
这里通过 TestBed
注入了 UserService
和 LoggerService
,并使用 spyOn
来验证 LoggerService
的 log
方法是否被调用。
通过合理地使用依赖注入和模拟依赖,我们可以写出高质量、可靠的测试代码,确保应用的稳定性和正确性。
七、依赖注入的最佳实践
7.1 保持依赖简单和明确
尽量使每个组件和服务的依赖保持简单,避免过多的间接依赖。过多的依赖会使代码难以理解和维护。同时,在声明依赖时,要明确依赖的用途,例如通过合理命名变量和使用注释。
7.2 合理选择依赖作用域
根据业务需求,合理选择依赖的作用域。如果一个服务需要在整个应用中共享状态,应该将其提供在根作用域;如果每个组件需要独立的状态,应该将服务提供在组件作用域。对于模块内共享的服务,可以提供在模块作用域。
7.3 使用别名和 InjectionToken 提高代码可维护性
在需要为依赖提供别名或注入复杂类型时,使用别名提供者和 InjectionToken
。这可以使代码更加清晰,易于维护和扩展。例如,在不同模块中可能需要使用不同的实现来提供相同功能的服务,通过别名和 InjectionToken
可以轻松实现这种替换。
7.4 在测试中充分利用依赖注入
在编写测试时,充分利用依赖注入来模拟依赖。这不仅可以提高测试的速度和稳定性,还可以确保测试只关注被测试对象本身,而不受外部依赖的干扰。同时,通过合理的测试设置,可以验证依赖注入的正确性和依赖之间的交互。
总之,Angular 的依赖注入机制是一个强大而灵活的工具,通过深入理解和合理使用它,可以开发出高质量、可维护、可测试的 Angular 应用。在实际项目中,遵循最佳实践,不断优化依赖注入的使用,将有助于提升整个项目的开发效率和质量。