Angular 理解依赖注入(DI)
一、依赖注入(DI)核心概念
- 定义与地位:DI 是 Angular 核心概念之一,内置于框架中,支持带 Angular 装饰器的类(组件、指令、管道、可注入对象)配置所需依赖。
- 核心角色:DI 系统包含两个关键角色------依赖消费者(需使用依赖的类)和依赖提供者(提供依赖的来源)。
- 注入器(Injector)作用:作为连接消费者与提供者的抽象层,当请求依赖时,先检查自身注册表是否有可用实例,无则创建新实例并存储。Angular 在应用启动时自动创建应用级"根注入器",多数场景下无需手动创建注入器。
- 依赖类型:除类之外,还支持函数、对象、原始类型(字符串、布尔值等)作为依赖,具体可参考"依赖项提供程序"相关内容。
二、依赖项提供方式
依赖项可在多个层级提供,不同方式对应不同使用范围和特性,具体如下:
| 提供方式 | 配置方法 | 适用场景 | 关键特性 |
|---|---|---|---|
应用根级别(providedIn,首选) |
在 @Injectable 装饰器中设置 providedIn: 'root' |
需在整个应用中共享依赖(如全局服务) | 1. 生成单一共享实例,注入到所有请求该依赖的类 2. 支持代码优化(树摇,移除未使用服务) |
| 组件级别 | 在 @Component 装饰器的 providers 字段中配置(如 providers: [HeroService]) |
依赖仅需在特定组件及其实例、模板内组件/指令中使用 | 1. 每个组件实例对应一个新的依赖实例 2. 即使依赖未使用,也会被包含在应用中,无法树摇 |
应用根级别(ApplicationConfig) |
1. 定义 ApplicationConfig 对象,在 providers 中配置依赖(如 providers: [{ provide: HeroService }]) 2. 将配置传入 bootstrapApplication 函数(在 main.ts 中) |
需在应用全局配置依赖,且不适合用 providedIn 的场景 |
即使依赖未使用,也会被包含在应用中,无法树摇 |
| 基于 NgModule 的应用 | 在 @NgModule 装饰器的 providers 字段中配置 |
基于 NgModule 架构的应用,需在模块范围内共享依赖 | 1. 依赖对模块的所有声明项及共享同一 ModuleInjector 的其他模块可用 2. 即使依赖未使用,也会被包含在应用中,无法树摇 3. 特殊场景需参考"分层注入器"文档 |
基础配置前提
无论哪种提供方式,首先需为类添加 @Injectable 装饰器,表明该类可被注入,基础代码如下:
typescript
@Injectable()
class HeroService {}
三、依赖注入/使用方法
- 核心工具 :使用 Angular 的
inject函数获取依赖。 - 使用场景 :可在注入上下文中使用,常见场景包括组件、指令、服务、管道的类属性初始化器或类构造函数。
- 代码示例:
typescript
import { inject, Component } from 'angular/core';
@Component({/* 组件配置 */})
export class UserProfile {
// 在属性初始化器中注入依赖
private userClient = inject(UserClient);
constructor() {
// 在构造函数中注入依赖
const logger = inject(Logger);
}
}
- 注入流程 :
- 当 Angular 发现组件依赖某服务时,先检查注入器是否有该服务的现有实例;
- 若无实例,注入器通过已注册的提供者创建新实例,并添加到注册表,再返回给 Angular;
- 所有请求的服务解析完成后,Angular 以这些服务为参数调用组件构造函数。
Angular 创建可注入服务
一、核心概念与原则
- 服务定义:服务是涵盖应用所需值、函数或特性的广泛类别,通常是具有明确、单一用途的类;组件是可使用依赖注入(DI)的类之一。
- Angular 设计原则:区分组件与服务以提升模块性和可重用性,将组件视图相关功能与其他处理分离,使组件类精简高效。
- 组件与服务职责划分:组件仅负责支持用户体验,提供数据绑定所需的属性和方法,协调视图与应用逻辑;服务承担组件委托的任务,如从服务器获取数据、验证用户输入、控制台日志记录等,且服务可通过配置同类型不同提供程序增强应用适应性。
- Angular 支持方式:不强制上述原则,但通过 DI 机制,方便开发者将应用逻辑拆分到服务中,并让组件可使用这些服务。
二、服务示例
- 日志服务(Logger) :向浏览器控制台输出日志,包含
log(普通日志)、error(错误日志)、warn(警告日志)三种方法,代码如下:
typescript
export class Logger {
log(msg: unknown) { console.log(msg); }
error(msg: unknown) { console.error(msg); }
warn(msg: unknown) { console.warn(msg); }
}
- 英雄服务(HeroService) :依赖
Logger服务记录日志,使用BackendService获取英雄数据,且BackendService可能依赖HttpClient服务从服务器异步获取英雄数据,代码如下:
typescript
import { inject } from "@angular/core";
export class HeroService {
private heroes: Hero[] = [];
private backend = inject(BackendService);
private logger = inject(Logger);
async getHeroes() {
// 从后端获取英雄数据
this.heroes = await this.backend.getAll(Hero);
// 记录获取到的英雄数量日志
this.logger.log(`Fetched ${this.heroes.length} heroes.`);
return this.heroes;
}
}
三、使用 CLI 创建可注入服务
- 生成服务命令 :在
src/app/heroes文件夹生成HeroService类,运行 Angular CLI 命令ng generate service heroes/hero。 - 默认生成的服务代码 :包含
@Injectable()装饰器,指定该类可在 DI 系统中使用,providedIn: 'root'表示服务在整个应用中可提供,代码如下:
typescript
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class HeroService {}
- 完善服务功能 :添加
getHeroes()方法,从mock-heroes.ts中获取英雄模拟数据,代码如下:
typescript
import { Injectable } from '@angular/core';
import { HEROES } from './mock-heroes';
@Injectable({
// 声明该服务由根应用注入器创建
providedIn: 'root',
})
export class HeroService {
getHeroes() {
return HEROES;
}
}
- 开发建议:为保证代码清晰性和可维护性,组件与服务应分别定义在不同文件中。
四、服务注入方式
(一)注入到组件
- 使用
inject函数 :在组件中声明表示依赖项的类字段,通过inject函数初始化,示例(HeroListComponent中注入HeroService):
typescript
import { inject } from "@angular/core";
export class HeroListComponent {
private heroService = inject(HeroService);
}
- 使用组件构造函数:在组件构造函数中声明依赖项,示例:
typescript
constructor(private heroService: HeroService)
- 注入限制 :
inject方法可在类和函数中使用,构造函数注入仅能在类构造函数中使用;两种方式均需在有效的注入上下文(通常是组件构造或初始化过程)中进行。
(二)注入到其他服务
- 注入模式 :与注入到组件的模式一致,在依赖服务中通过
inject函数获取所需服务。 - 示例(
HeroService中注入Logger服务):
typescript
import { inject, Injectable } from '@angular/core';
import { HEROES } from './mock-heroes';
import { Logger } from '../logger.service';
@Injectable({
providedIn: 'root',
})
export class HeroService {
private logger = inject(Logger);
getHeroes() {
// 获取英雄数据时记录日志
this.logger.log('Getting heroes.');
return HEROES;
}
}
Angular 定义依赖项提供程序
一、核心概述
- 依赖项扩展类型 :除类实例外,
boolean、string、Date及对象等也可作为依赖项,Angular 提供灵活 API 支持这些值在 DI 中使用。 - 提供程序配置基础 :类提供程序语法是简写形式,实际会扩展为符合
Provider接口的配置对象,核心包含provide(依赖项令牌,作为消费依赖值的键)和提供程序定义对象(告知注入器如何创建依赖值)。
二、提供程序令牌(Provider Token)
-
默认行为 :若将服务类指定为提供程序令牌,注入器默认用
new运算符实例化该类,示例:typescriptproviders: [Logger] // 简写形式 -
扩展配置形式 :上述简写会展开为完整配置对象,可通过该形式关联令牌与不同类或值,示例:
typescript[{ provide: Logger, useClass: Logger }] // 完整配置
三、五种核心提供程序类型
1. 类提供程序(useClass)
-
作用:创建并返回指定类的新实例,可替换通用/默认类的实现(如不同策略、扩展默认类、测试中模拟真实类行为)。
-
基础示例 :请求
Logger依赖时实例化BetterLogger:typescript[{ provide: Logger, useClass: BetterLogger }] -
带依赖的类提供 :若替换类(如
EvenBetterLogger)自身有依赖(如UserService),需在父模块/组件的providers中同时配置依赖,示例:typescript[UserService, { provide: Logger, useClass: EvenBetterLogger }]其中
EvenBetterLogger通过注入UserService实现用户名称日志输出:typescript@Injectable() export class EvenBetterLogger extends Logger { private userService = inject(UserService); override log(message: string) { const name = this.userService.user.name; super.log(`Message to ${name}: ${message}`); } }
2. 别名提供程序(useExisting)
-
作用:将一个令牌映射为另一个令牌,使第一个令牌成为第二个令牌关联服务的别名,实现通过两种方式访问同一服务实例。
-
示例 :请求
OldLogger或NewLogger时,均注入NewLogger的单例实例:typescript[NewLogger, { provide: OldLogger, useExisting: NewLogger }] -
注意事项 :不可用
useClass实现别名(会创建两个不同实例)。
3. 工厂提供程序(useFactory)
- 作用:通过调用工厂函数创建依赖对象,支持基于 DI 及应用中其他信息动态生成值。
- 使用场景 :如根据用户授权状态控制
HeroService是否返回秘密英雄数据。 - 实现步骤 :
-
定义
HeroService,接收isAuthorized标志控制秘密英雄显示:typescriptclass HeroService { constructor(private logger: Logger, private isAuthorized: boolean) { } getHeroes() { const auth = this.isAuthorized ? 'authorized' : 'unauthorized'; this.logger.log(`Getting heroes for ${auth} user.`); return HEROES.filter(hero => this.isAuthorized || !hero.isSecret); } } -
创建工厂函数,结合
UserService获取授权状态:typescriptconst heroServiceFactory = (logger: Logger, userService: UserService) => new HeroService(logger, userService.user.isAuthorized); -
配置工厂提供程序,指定依赖(
deps数组按顺序注入工厂函数参数):typescriptexport const heroServiceProvider = { provide: HeroService, useFactory: heroServiceFactory, deps: [Logger, UserService] };
-
4. 值提供程序(useValue)
- 作用:将静态值与 DI 令牌关联。
- 适用场景:提供运行时配置常量(如网站基础地址、功能标志),或单元测试中用模拟数据替代生产数据服务。
5. InjectionToken 提供程序
- 作用:为非类依赖项(如对象、基本类型)提供令牌,解决接口无法作为 DI 令牌的问题(TypeScript 接口无运行时表示)。
- 使用步骤 :
-
定义
InjectionToken及对应接口:typescriptimport { InjectionToken } from '@angular/core'; export interface AppConfig { title: string; } export const APP_CONFIG = new InjectionToken<AppConfig>('app.config description'); -
注册值与令牌关联:
typescriptconst MY_APP_CONFIG_VARIABLE: AppConfig = { title: 'Hello' }; providers: [{ provide: APP_CONFIG, useValue: MY_APP_CONFIG_VARIABLE }] -
注入使用:
typescriptexport class AppComponent { constructor() { const config = inject(APP_CONFIG); this.title = config.title; } }
-
四、接口与 DI 的关系
- 局限性:TypeScript 接口仅为设计时工具,转译为 JavaScript 后消失,无运行时表示,无法作为 DI 令牌,也不能直接注入。
- 错误示例 :
- 不可用接口作为提供程序令牌:
[{ provide: AppConfig, useValue: MY_APP_CONFIG_VARIABLE }](错误) - 不可用接口作为注入类型:
private config = inject(AppConfig)(错误)
- 不可用接口作为提供程序令牌:
Angular 注入上下文(Injection Context)
一、核心定义
Angular 的依赖注入(DI)系统依赖运行时的注入上下文 ,该上下文需确保当前注入器(injector)可被访问,只有在注入上下文中执行代码,注入器才能正常工作;在注入上下文中,可使用 inject() 函数注入实例。
二、注入上下文的可用场景
- 由 DI 系统实例化的类(如带
@Injectable或@Component装饰器的类)的构造函数(constructor)执行期间。 - 上述类的字段初始化器中。
Provider或@Injectable的useFactory所指定的工厂函数中。InjectionToken所指定的factory函数中。- 运行在注入上下文中的调用栈帧内(如路由器守卫等特定 API 场景)。
三、关键使用场景与示例
(一)类构造函数(Class constructors)
DI 系统实例化类时,会自动在注入上下文中执行类的构造函数,可在构造函数或字段初始化时用 inject() 注入依赖。
typescript
class MyComponent {
private service1: Service1;
private service2: Service2 = inject(Service2); // 字段初始化时注入(在上下文中)
constructor() {
this.service1 = inject(Service1) // 构造函数中注入(在上下文中)
}
}
(二)上下文内调用栈帧(Stack frame in context)
部分 Angular API 设计为在注入上下文中运行,如路由器守卫(CanActivateFn),可在守卫函数内用 inject() 访问服务。
typescript
const canActivateTeam: CanActivateFn =
(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
return inject(PermissionsService).canActivate(inject(UserToken), route.params.id);
};
(三)在注入上下文中运行函数(Run within an injection context)
若需在非注入上下文执行函数,可通过 runInInjectionContext() 手动指定注入器(如 EnvironmentInjector),使函数在注入上下文中运行。
typescript
@Injectable({ providedIn: 'root',})
export class HeroService {
private environmentInjector = inject(EnvironmentInjector);
someMethod() {
runInInjectionContext(this.environmentInjector, () => {
inject(SomeService); // 在指定注入上下文中注入服务
});
}
}
注:
inject()仅在注入器能解析所需令牌时返回实例。
(四)断言注入上下文(Asserts the context)
使用 assertInInjectionContext() 辅助函数,可断言当前上下文为注入上下文,若不是则抛出清晰错误(需传入调用函数引用,使错误信息指向正确 API 入口)。
- 定义断言函数
typescript
import { ElementRef, assertInInjectionContext, inject } from '@angular/core';
export function injectNativeElement<T extends Element>(): T {
assertInInjectionContext(injectNativeElement);
return inject(ElementRef).nativeElement;
}
- 调用限制:仅能在注入上下文(构造函数、字段初始化器、提供者工厂、
runInInjectionContext()执行的代码)中调用,非上下文调用会失败。
typescript
import { Component, inject } from '@angular/core';
import { injectNativeElement } from './dom-helpers';
@Component({ /* ... */ })
export class PreviewCard {
readonly hostEl = injectNativeElement<HTMLElement>(); // 成功:字段初始化在注入上下文中
onAction() {
const anotherRef = injectNativeElement<HTMLElement>(); // 失败:不在注入上下文中
}
}
四、错误情况
在非注入上下文调用 inject() 或 assertInInjectionContext(),会抛出 error NG0203。
优化注入令牌
一、网页基本信息
- 网页类型:普通网站(Angular官方文档页面)
- 核心主题:介绍如何通过轻量级注入令牌(lightweight injection tokens)优化Angular客户端应用体积,尤其针对库开发者提供依赖注入设计方案
- 所属文档体系:隶属于Angular官方文档的"深度指南(In-depth Guides)- 依赖注入(Dependency Injection)"板块,是该板块下的重要内容之一
二、核心背景与目标
- 问题根源:Angular中注入令牌的存储方式可能导致未使用的组件/服务无法被"摇树优化"(tree-shaking),即便应用未实际使用,其代码仍会保留在最终bundle中,增加应用体积
- 核心目标:帮助库开发者设计依赖结构,确保客户端应用仅引入实际使用的库功能代码,未使用代码可被有效移除,优化应用bundle体积
- 责任主体:由于应用开发者无法察觉或解决库中的摇树问题,因此优化注入令牌以支持摇树的责任落在库开发者身上
三、关键概念与问题解析
(一)令牌保留的场景与原因
- 令牌保留的核心场景:当组件被用作注入令牌且处于"值位置"(value position)时,编译器会在运行时保留该令牌,导致组件无法被摇树
- 具体案例说明 :以库中的
lib-card和lib-header组件为例- 常规实现中,
LibCardComponent通过@ContentChild(LibHeaderComponent)获取lib-header组件,此时LibHeaderComponent存在两个引用- 类型位置(type position):
header: LibHeaderComponent,TypeScript转译后会被编译器清除,不影响摇树 - 值位置(value position):
@ContentChild(LibHeaderComponent),编译器需在运行时保留,导致即便应用不使用lib-header,其代码仍会保留在bundle中
- 类型位置(type position):
- 常规实现中,
- 其他令牌保留场景 :除内容查询(content query)的"值位置"外,构造函数注入中用作类型说明符的令牌,也会被转换为"值位置"引用(如
constructor(@Optional() other: OtherComponent)会转为constructor(@Optional() @Inject(OtherComponent) other)),导致组件无法摇树
(二)轻量级注入令牌的适用场景
当组件被用作注入令牌时,需使用轻量级注入令牌模式,具体包括两种情况:
- 令牌用于内容查询(content query)的"值位置"
- 令牌用作构造函数注入的类型说明符
四、轻量级注入令牌的实现方案
(一)核心设计思路
通过"抽象类作为注入令牌+后续提供具体实现"的方式,让体积小、无实际逻辑的抽象类被保留,而有具体实现的组件可被摇树(未使用时移除)
(二)分步实现步骤
- 定义轻量级注入令牌 :创建一个无具体实现的抽象类(如
LibHeaderToken),作为注入令牌 - 实现组件并关联令牌 :在具体组件(如
LibHeaderComponent)中,让组件继承抽象令牌类,并在组件的providers配置中,将抽象令牌与具体组件关联({provide: LibHeaderToken, useExisting: LibHeaderComponent}) - 注入令牌而非具体组件 :在依赖该组件的父组件(如
LibCardComponent)中,通过@ContentChild()/@ContentChildren()注入抽象令牌(如@ContentChild(LibHeaderToken) header: LibHeaderToken|null = null),不再直接引用具体组件 - 示例代码框架:
typescript
// 1. 定义抽象令牌
abstract class LibHeaderToken {}
// 2. 具体组件继承令牌并关联
@Component({
selector: 'lib-header',
providers: [{provide: LibHeaderToken, useExisting: LibHeaderComponent}]
// 其他配置...
})
class LibHeaderComponent extends LibHeaderToken {}
// 3. 父组件注入令牌
@Component({
selector: 'lib-card'
// 其他配置...
})
class LibCardComponent {
@ContentChild(LibHeaderToken) header: LibHeaderToken|null = null;
}
(三)扩展:令牌用于API定义
当父组件需调用子组件方法时,可在抽象令牌类中声明抽象方法,具体实现放在子组件中,既保证类型安全,又不影响摇树:
typescript
// 抽象令牌声明方法
abstract class LibHeaderToken {
abstract doSomething(): void;
}
// 子组件实现方法
class LibHeaderComponent extends LibHeaderToken {
doSomething(): void {
// 具体逻辑...
}
}
// 父组件调用(需先判断子组件是否存在)
class LibCardComponent implements AfterContentInit {
@ContentChild(LibHeaderToken) header: LibHeaderToken|null = null;
ngAfterContentInit(): void {
this.header?.doSomething(); // 子组件未使用时,无运行时引用,不会调用
}
}
五、命名规范
遵循Angular风格指南,轻量级注入令牌命名需满足:
- 与关联组件保持命名关联,同时明确区分
- 推荐格式:组件基础名 + 后缀"Token",如组件
LibHeaderComponent对应的令牌命名为LibHeaderToken
实际应用中的依赖注入
(一)使用@Inject创建自定义提供器(Custom providers with @Inject)
- 应用场景:为隐式依赖项(如浏览器内置API)提供具体实现,同时便于测试时替换为模拟服务
- 核心原理 :通过
InjectionToken定义令牌,结合factory函数提供依赖实例,再用inject函数在服务中注入该令牌对应的依赖 - 示例案例 :实现
BrowserStorageService服务,将浏览器localStorage作为依赖注入- 定义令牌:创建
BROWSER_STORAGE注入令牌,指定providedIn: 'root'(根级别提供),factory函数返回localStorage - 注入依赖:在
BrowserStorageService中用inject(BROWSER_STORAGE)初始化storage属性,封装get/set方法操作本地存储 - 测试优势:测试时可替换
BROWSER_STORAGE的实现为模拟localStorage,避免依赖真实浏览器API
- 定义令牌:创建
- 关键代码片段:
typescript
import { inject, Injectable, InjectionToken } from '@angular/core';
// 定义注入令牌
export const BROWSER_STORAGE = new InjectionToken<Storage>('Browser Storage', {
providedIn: 'root',
factory: () => localStorage // 提供localStorage实例
});
// 注入令牌并封装方法
@Injectable({ providedIn: 'root' })
export class BrowserStorageService {
public storage = inject(BROWSER_STORAGE);
get(key: string) { return this.storage.getItem(key); }
set(key: string, value: string) { this.storage.setItem(key, value); }
}
(二)注入组件的DOM元素(Inject the component's DOM element)
- 应用场景:当视觉效果实现、第三方工具集成等场景需要直接操作DOM时,安全获取组件/指令的底层DOM元素
- 核心原理 :Angular通过
ElementRef注入令牌,暴露@Component或@Directive对应的DOM元素,开发者可通过nativeElement属性访问原生DOM节点 - 示例案例 :实现
HighlightDirective指令,修改元素文本颜色- 注入
ElementRef:在指令中用inject(ElementRef)获取DOM元素引用 - 操作DOM:在
update方法中通过this.element.nativeElement.style.color修改文本颜色为红色
- 注入
- 关键代码片段:
typescript
import { Directive, ElementRef } from '@angular/core';
@Directive({ selector: '[appHighlight]' })
export class HighlightDirective {
private element = inject(ElementRef); // 注入DOM元素引用
update() {
this.element.nativeElement.style.color = 'red'; // 操作DOM样式
}
}
(三)使用前向引用解决循环依赖(Resolve circular dependencies with a forward reference)
- 问题背景 :TypeScript中类声明有顺序限制,未定义的类无法直接引用;当两个类互相引用(如A引用B、B引用A),或类在自身
providers中引用自身时,会产生循环依赖问题 - 核心解决方案 :使用Angular的
forwardRef()函数创建间接引用,让Angular在后续解析阶段识别依赖关系,打破循环引用 - 示例案例 :在
MenuItem组件的providers中引用自身- 场景:
MenuItem组件需在providers中提供PARENT_MENU_ITEM令牌,且令牌对应的值为组件自身 - 解决方式:通过
forwardRef(() => MenuItem)间接引用MenuItem类,避免因类未定义导致的引用错误
- 场景:
- 关键代码片段:
typescript
// 在组件的providers数组中使用forwardRef解决自引用
providers: [
{
provide: PARENT_MENU_ITEM,
useExisting: forwardRef(() => MenuItem), // 间接引用未定义的MenuItem类
},
],