Angular 20+ 高阶教程 – Dependency Injection 依赖注入
前言
本来是想先介绍 Angular Component 的,但 Component 里面会涉及到一些 Dependency Injection (简称 DI) 的概念,所以还是先介绍 DI 吧。
什么是 Dependency Injection?
何谓依赖?
class ServiceA {
plus(num1: number, num2: number) {
return num1 + num2;
}
}
首先我们有一个 class ServiceA,它有一个 plus method 可以做加法。
然后我们有另一个 ServiceB
class ServiceB {
plusAndTimesTwo(num1: number, num2: number) {
return (num1 + num2) * 2;
}
}
它有一个 plus and times two method,做了加法之后再乘于二。
上面的代码虽然可以 work,但破坏了 DRY (Don't Repeat Yourself) 原则。
加法已经在 ServiceA 实现了,怎么可以把实现代码 copy paste 到 ServiceB 呢?
所以我们需要在 ServiceB 引入 ServiceA
class ServiceB {
plusAndTimesTwo(num1: number, num2: number) {
const serviceA = new ServiceA();
const afterPlus = serviceA.plus(num1, num2);
return afterPlus * 2;
}
}
简单嘛,实例化 ServiceA 然后调用 plus 方法就可以了。
很好,这就是所谓的 "依赖"。
ServiceB 的某个方法 "依赖" 了 ServiceA 的某个方法。
何谓注入?
上面的代码虽然可以 work,但它又破坏了 OOP 的 SOLID 原则 – Dependency Inversion Principle(依赖反转原则)
我们在 plusAndTimesTwo 里直接实例化 ServiceA,这种写法在一些场景下会给我们带来麻烦。
一个经典的场景是 -- 单元测试
今天我们想测试 plusAndTimesTwo 这个方法是否实现正确。我们会这么写:
export class ServiceA {
plus(num1: number, num2: number) {
return num1 - num2;
}
}
export class ServiceB {
plusAndTimesTwo(num1: number, num2: number) {
const serviceA = new ServiceA();
const afterPlus = serviceA.plus(num1, num2);
return afterPlus * 2;
}
}
.test.ts
it("test", () => {
const service = new ServiceB();
const result = service.plusAndTimesTwo(1, 1);
expect(result).toBe(4); // (1 + 1) * 2 = 4
});
结果报错了

查看代码

没有什么问题啊,怎么会报错呢?
于是找了老半天,发现原来是 ServiceB 的依赖 ServiceA 的 plus method 的实现代码错了。

我们想测试的是 plusAndTimesTwo,但由于它依赖了 plus,结果 plus 的错误导致了 plusAndTimesTwo 也出错了。
所以,这算哪门子的 “单元” 测试呢?
怎么办?这时我们需要引入 "inject 注入" 的概念。
首先,我们不要在 plusAndTimesTwo 去实例化 ServiceA。
export class ServiceB {
constructor(private serviceA: ServiceA) {}
plusAndTimesTwo(num1: number, num2: number) {
const afterPlus = this.serviceA.plus(num1, num2);
return afterPlus * 2;
}
}
取而代之的是透过 constructor 让外部把 ServiceA “注入” 进来让 plusAndTimesTwo 使用。
然后测试代码改成这样
it("test ServiceB.plusAndTimesTwo()", () => {
const serviceA = {
plus() {
return 2;
},
} satisfies ServiceA;
const service = new ServiceB(serviceA);
const result = service.plusAndTimesTwo(1, 1);
expect(result).toBe(4); // (1 + 1) * 2 = 4
});
在创建 ServiceB 时,我们 "provide 提供" 了 ServiceB 所需要的依赖 ServiceA。
并且这个 ServiceA 不是 new ServiceA(),而是专门为了这个测试而 mock(伪造)的 ServiceA 实例。
注意看:serviceA.plus 的 return 2 是 hardcode 写上去的,没有任何 logic 和 formula。
测试结果

把依赖隔离出来,透过注入的方式去提供,这样就可以非常灵活的控制代码。
上面单元测试中,我们 mock 了 ServiceA 才提供给 ServiceB,这样我们就可以确保 ServiceA 是完全可控的,
不会因为原本 ServiceA 代码实现有误而牵连到 ServiceB,完美的做到了 "单元" 测试。
何谓依赖注入?
上面这种注入式的写法确实让代码灵活了许多,但...这难道没有代价吗?
怎么可能?!anything has price 是自古不变的真理。
我们很快就感受到了这种注入式写法的问题。看例子:
class ServiceA {}
class ServiceB {
constructor(serviceA: ServiceA) {}
}
class ServiceC {
constructor(serviceB: ServiceB) {}
}
有三个 class,ServiceA、B、C
C 依赖 B 依赖 A
我们想实例化 ServiceC 就得这样写
const serviceA = new ServiceA();
const serviceB = new ServiceB(serviceA);
const serviceC = new ServiceC(serviceB);
或者
const serviceC = new ServiceC(new ServiceB(new ServiceA()));
本来实例化依赖是 class 内部自己封装的,但被我们隔离了出来,变成外部需要提供依赖。
这就导致每一次想要实例化一个 class 时,我们就需要 provide 这个 class 的所有依赖,还有这个依赖的依赖的依赖...
这个灵活的代价有点大丫。
聪明的人们很快发现了一个关键点 -- 我们其实只是想在某一些场景灵活的替换依赖(比如,单元测试的时候)
绝大部分的时候,new ServiceA 就可以满足依赖了,并不需要什么替换。
于是,针对下面这种简单场景
const serviceC = new ServiceC(new ServiceB(new ServiceA()));
我们可以把实例化依赖封装起来,让它变成自动化,这就是所谓的依赖注入。
依赖注入长这样(注:不同语言有不同的实现方式,也有一些变种功能,但核心是差不太多的,ASP.NET Core 可以看这篇)
const injector = Injector.create({
providers: [ServiceA, ServiceB, ServiceC]
});
const serviceC = injector.get(ServiceC);
上面这个是其中一种 Angular DI 的写法。(注:只是其中一种写法,不用太在意,只是为了演示而已)
我们有 2 件事情要做,第一件是实例化 ServiceC,第二件是实例化其所有的依赖。
我们就把这 2 件事封装起来,让 injector 来负责呗。
首先把所有的 class(依赖)都 provide(提供)给 injector(注入器),然后告诉 injector 我们要 get(获取 / 实例化 / 注入) 一个 ServiceC。
injector 内部就会替我们完成上面 2 件事,实例化 ServiceC 和其所有依赖。
哇,代码瞬间就干净了。
依赖注入的实现原理
injector 是如何 "知道" class 有哪些依赖的呢?
class ServiceC {
constructor(serviceB: ServiceB) {}
}
ServiceC 的依赖被声明到了 constructor 参数中。如果是静态语言,比如 C#,
injector 可以透过反射读取到 ServiceC constructor 的参数类型,这样就知道它有哪些依赖了。
但是 JS 没有反射丫。Angular 是怎样做到的呢?自己想一想吧,下一 part 揭晓。
Angular DI の 最简化版
Angular DI 极其复杂,我们从最简单的版本看起。
创建 Angular 项目
ng new di --routing=false --style=scss --skip-tests --ssr=false --zoneless
去 main.ts 把原本 Angular 的 startup 代码全部注释掉 (注:DI 不依赖整体的 Angular 框架,它可以独立拿出来使用)
// import { bootstrapApplication } from '@angular/platform-browser';
// import { appConfig } from './app/app.config';
// import { App } from './app/app';
// bootstrapApplication(App, appConfig)
// .catch((err) => console.error(err));
import { Injectable, Injector } from "@angular/core";
class ServiceA {}
@Injectable()
class ServiceB {
constructor(serviceA: ServiceA) {}
}
define class ServiceA 和 ServiceB
接着 import injector 然后使用它
const injector = Injector.create({
providers: [ServiceA, ServiceB],
});
const serviceB = injector.get(ServiceB);
console.log('serviceB', serviceB);
把两个 class 提供给 injector,然后透过 injector 获取 ServiceB。
运行 command
ng serve --open
效果

injector 会替我们实例化 ServiceB,并且满足其依赖,相当于:
const serviceB = new ServiceB(new ServiceA());
Angular 如何知道 class 有哪些依赖?
JS 没有反射,那 Angular 怎么能从 ServiceB 的 constructor 感知到其依赖 ServiceA 呢?
class ServiceB {
constructor(serviceA: ServiceA) {}
}
答案是黑魔法 Compilation。
ServiceB 经过 compile 后会变成这样

它多了一个 ɵfac 静态方法。
从代码上可以推测出 injector.get(ServiceB),其实并不是直接执行了 new ServiceB(new ServiceA()),它只是调用了 ServiceB.ɵfac()。
而 ɵfac 内容才是 new ServiceB( inject(ServiceA) )。这句代码便是 compiler 透过反射 constructor 得知 ServiceB 依赖 ServiceA 后写出来的。
另外,inject(ServiceA) 是一个递归实例化依赖函数,里面一定是调用了 ServiceA.ɵfac()。以此类推,一直到所有的依赖全部被实例化。
简而言之,虽然 JS 没有反射,但是 Angular compiler 可以反射,然后自动编写出实例化依赖的代码。这就是 Angular DI 的实现秘诀啦。
@Injectable()
眼尖的朋友可能已经发现了 @Injectable() 在上面的 class ServiceB
@Injectable()
class ServiceB {
constructor(serviceA: ServiceA) {}
}
@Injectable decorator 有两种用途,我们先了解其中一个就好。
如果一个 class 没有声明 @Injectable decorator 那 Angular compiler 就不会理它。
@Injectable()
class ServiceB {
constructor(serviceA: ServiceA) {}
}
class ServiceC {
constructor(serviceA: ServiceA) {}
}
const injector = Injector.create({
providers: [ServiceA, ServiceB, ServiceC],
});
ServiceC 和 ServiceB 一样都声明了依赖 ServiceA,并且 ServiceC 也在 providers 里。唯一的区别是 ServiceC 没有 @Injectable decorator。
下面是 compile 后的代码

ServiceC 只是一个普普通通的 class,没有 static 方法 ɵfac。
若我们尝试去注入它
const serviceB = injector.get(ServiceC);
将得到一个 runtime error。

所以,记得要加上 @Injectable() 给有需要依赖的 class 哦。
小考题:ServiceA 没有任何依赖,那它需要 @Injectable() 吗?
答案:不需要。但通常我们还是会放的,因为上面说了,@Injectable() 还有第二个用途,下面会教。
中场休息,小总结
到目前为止,Angular 的 DI 和其它框架(后端框架)使用的 DI 还算基本类似。
-
都是透过 class constructor 参数做注入
-
需要 provide 所有的 class 给 injector
-
injector 负责实例化和满足所有 class 的依赖。
比较特别的地方是 @injectable() decorator,由于 JS 没有 reflection,Angular 无法从 class constructor 参数反射出其依赖的 class,
所以 Angular 需要利用 compiler + decorator 去实现 reflection。
好,到这边,我们大概学习了 Angular DI 大约 20% 的内容...
下半场,我们来学习 Angular DI 独有的一些特性。
Injector and Provider 详解
not only for class
上面例子中,我们都是在处理 class,但其实 Angular DI 并不限于 class,任何类型都可以使用 DI 概念。
const injector = Injector.create({
providers: [{ provide: 'key', useValue: 'hello world' }],
});
const value = injector.get('key');
console.log(value); // 'hello world'
看到吗,上面代码中,完全没有 class 的影子,但 injector 照常工作。
抽象理解 provider 和 injector
provider 的特性
class 是一种 provider,但 provider 不仅仅只限于 class。
抽象的看,provider 是一个 key value pair 对象。
key 的作用是为了识别。
value 则是一个提供最终值的 factory 函数。
只要能满足这 2 点,那它就可以被作为 provider。
那我们来看看 class 是否满足这 2 点特性。
-
class can be a key
class ServiceA {} const map = new Map(); map.set(ServiceA, 'value'); console.log(map.get(ServiceA)); // 'value'class 具备识别性,所以它满足 provider key 的特性。
-
class can be a value factory
class 有 constructor 并且能实例化出对象。
这就满足了 provider value factory 的特性。
所以,结论是 class 可以被当成 provider 使用。
Injector 的特性
injector 不仅仅是实例化机器。
抽象的看,injector 第一个任务是透过 key 查找出指定的 provider,这个 key 只要具备可识别性就可以了。比如:string,class,symbol 等等都具备识别性。
第二个任务是透过 provider value factory 生产出最终的值。当然如果这个 factory 需要依赖,injector 会先查找它所需要的依赖,注入给 factory 函数。
拿 class ServiceB 做例子:
injector 要找一个 key = ServiceA 的 provider。找到以后透过 value factory(也就是 ServiceA.ɵfac)生产出最终的值 (ServiceA 实例)。
Provider & StaticProvider
Angular 有多种不同形态的 Provider,class 只是其中一种。
我们来过一遍。(虽然有点多,但它们都满足上面两个 provider 的特性,所以其实很好理解的)
Injector.create 的 interface 长这样

Provider 和 StaticProvider 是所有 Provider 的抽象。


它俩是有重叠的,总的来说是 TypeProvider、ClassProvider、StaticClassProvider、ConstructorProvider、FactoryProvider、ValueProvider、ExistingProvider。
FactoryProvider
FactoryProvider 是最 low layer 的 Provider,它可以做到其它所有 Provider 能做到的,所以我们只要理解了 FactoryProvider,再去理解其它 Provider 就非常容易了。
const myValueProvider: FactoryProvider = {
provide: 'key',
useFactory: () => {
return 'my value';
},
};
这是一个 FactoryProvider,上面讲过,Provider 需要有两个特性
第一个是它需要有一个可识别的 key,在这个例子中就是 myValueProvider.provide
第二个是它需要有一个 value factory 方法,在这个例子中就是 myValueProvider.useFactory
FactoryProvider 是这样被使用的
const injector = Injector.create({
providers: [myValueProvider],
});
const value = injector.get('key'); // my value
FactoryProvider with InjectionToken
上面的代码有个小问题

由于我使用了 string 作为 Provider 的识别 key,所以它出现了 warning。原因是 string 太容易撞了,Jser 都知道作为识别 key 的话,Symbol 比 string 可靠。
而由于 Angular 需要有类型概念,所以连 JS 的 Symbol 都无法满足它的需求,于是 Angular 自己搞了一个 InjectionToken 类来替代 JS 的 Symbol。
下面这个是用 InjectionToken 替代了原本 string key 的写法
const MY_VALUE_TOKEN = new InjectionToken<string>('MyValue');
const myValueProvider: FactoryProvider = {
provide: MY_VALUE_TOKEN,
useFactory: (): string => {
return 'my value';
},
};
const injector = Injector.create({
providers: [myValueProvider],
});
const value = injector.get(MY_VALUE_TOKEN);
如果类型想要更准确的写法,还可以修一点
type GetInjectionTokenType<T> = T extends InjectionToken<infer R> ? R : never;
const myValueProvider: FactoryProvider = {
provide: MY_VALUE_TOKEN,
useFactory: (): GetInjectionTokenType<typeof MY_VALUE_TOKEN> => {
return 'my value';
},
};
FactoryProvider with Dependency
如果我们的 value factory 有依赖,可以这样写
const MY_VALUE_1_TOKEN = new InjectionToken<string>('MyValue1');
const MY_VALUE_2_TOKEN = new InjectionToken<string>('MyValue2');
const myValue1Provider: FactoryProvider = {
provide: MY_VALUE_1_TOKEN,
useFactory: () => 'my value 1',
};
const myValue2Provider: FactoryProvider = {
provide: MY_VALUE_1_TOKEN,
useFactory: (myValue1: string) => myValue1 + 'and my value 2',
deps: [MY_VALUE_1_TOKEN],
};
const injector = Injector.create({
providers: [myValue1Provider, myValue2Provider],
});
const value2 = injector.get(MY_VALUE_2_TOKEN); // my value 1 and my value 2
关键在 myValue2Provider
const myValue2Provider: FactoryProvider = {
provide: MY_VALUE_1_TOKEN,
useFactory: (myValue1: string) => myValue1 + 'and my value 2',
deps: [MY_VALUE_1_TOKEN],
};
属性 deps 用来声明 value factory 的依赖。当 useFactory 被调用时,injector 会注入其依赖。
好,至此我们就掌握了最底层的 Provider,其它的 Provider 都可以用 FactoryProvider 来实现,下面我一一列出来,并且附上 FactoryProvider 实现的版本。
ValueProvider
const VALUE_TOKEN = new InjectionToken<string>('Value');
const valueProvider: ValueProvider = {
provide: VALUE_TOKEN,
useValue: 'Derrick',
};
const injector = Injector.create({
providers: [valueProvider],
});
const name = injector.get(VALUE_TOKEN);
console.log(name); // 'Derrick'
value provider 和 factory provider 的区别是,它省略了 value factory,直接 hardcode 一个 value。
用 factory provider 重新表达上面这个例子,那会是这样
const valueProvider: FactoryProvider = {
provide: VALUE_TOKEN,
useFactory: () => 'Derrick'
};
TypeProvider、ConstructorProvider、ClassProvider、StaticClassProvider
这 4 个 Provider 都用于 class,它们只有微微的区别,我没有去研究为什么 Angular 搞了 4 个傻傻分不清楚的东西,但我猜想可能是历史原因,我们过一遍就好,真实开发中是不会用到的。
class ServiceA {}
class ServiceB {
constructor(serviceA: ServiceA) {}
}
ServiceB 依赖 ServiceA(注:我刻意不放 @Injectable() 来凸显不同 Provider 之前的特性)
TypeProvider
const injector = Injector.create({
providers: [
ServiceA,
ServiceB satisfies TypeProvider,
],
});
TypeProvider 就是一个普通的 class。上面的例子都是用这个。
由于我没有放 @Injectable() 若调用 injector.get(ServiceB) 将会报错哦。
ConstructorProvider
providers: [
ServiceA,
{ provide: ServiceB, deps: [ServiceA] } satisfies ConstructorProvider
],
ConstructorProvider 和 TypeProvider 的区别是它多了一个属性 deps。
它的作用是让我们放入 constructor 中的依赖,按顺序放。有了这个,即便没有 @Injectable() 也可以成功 get 到 ServiceB。
ClassProvider
const SERVICE_B_TOKEN = new InjectionToken<ServiceB>('ServiceB');
providers: [
ServiceA,
{ provide: SERVICE_B_TOKEN, useClass: ServiceB } satisfies ClassProvider,
],
ClassProvider 的特别之处是它允许我们定义任意类型的 key。
provide 声明了 key,useClass 声明了 value factory。(注:ClassProvider 不能声明 deps 哦。)
get 的方式是
const serviceB = injector.get(SERVICE_B_TOKEN);
SERVICE_B_TOKEN 是 key,injector 会找到这个 provider,发现 provider 声明了 useClass,表示它是一个 ClassProvider,然后就会实例化 useClass 的值。
任意 key 的好处是可以声明抽象,提供具体。比如
{ provide: AnimalService, useClass: DogService } satisfies ClassProvider,
依赖抽象总是好的嘛,说不定那天可以替换成一个优化版的具体实现,到时就只需要替换 useClass 的值,而不是全场替换依赖。
StaticClassProvider
providers: [
ServiceA,
{ provide: SERVICE_B_TOKEN, useClass: ServiceB, deps: [ServiceA] } satisfies StaticClassProvider,
],
StaticClassProvider是最完整的 class provider。
它可以定义 key,也可以声明 deps。相等于 ConstructorProvider + ClassProvider。
用 factory provider 重新表达上面这个例子,那会是这样
providers: [
{ provide: ServiceA, useFactory: () => new ServiceA() },
{
provide: SERVICE_B_TOKEN,
useFactory: (serviceA: ServiceA) => new ServiceB(serviceA),
deps: [ServiceA],
},
],
ExistingProvider
ExisitingProvider 的作用是提供 alias key。
就是说,两个不同的 key,但其实指向的是同一个 provider。
const NAME_1_TOKEN = new InjectionToken<string>('Name1');
const NAME_2_TOKEN = new InjectionToken<string>('Name2');
const injector = Injector.create({
providers: [
{ provide: NAME_1_TOKEN, useValue: 'Derrick' },
{ provide: NAME_2_TOKEN, useExisting: NAME_1_TOKEN },
],
});
const name = injector.get(NAME_2_TOKEN);
console.log(name); // 'Derrick'
key name1 是 value provider,它提供了 value 'Derrick'。
key name2 是 existing provider,它没有提供任何值,但它提供了一个 key,这个 key 指向 name1。
所以 injector 找 name2 > 被指向 name1 > 最终就获得了 name1 的值。
有点像 URL 301 redirect 那个概念。
用 factory provider 重新表达上面这个例子,那会是这样
{ provide: NAME_2_TOKEN, useFactory: (name1: string) => name1, deps: [NAME_1_TOKEN] },
@Inject()
下面这个是 class 依赖 class 的写法
class ServiceA {}
@Injectable()
class ServiceB {
constructor(serviceA: ServiceA) {}
}
const injector = Injector.create({
providers: [ServiceA, ServiceB],
});
const serviceB = injector.get(ServiceB);
下面这个是 class 依赖 value 的写法
const VALUE_TOKEN = new InjectionToken<string>('Value');
class ServiceB {
constructor(value: string) {}
}
const injector = Injector.create({
providers: [
{ provide: VALUE_TOKEN, useValue: 'value 123' },
{ provide: ServiceB, useClass: ServiceB, deps: [VALUE_TOKEN] }, // 关键在 deps
],
});
const serviceB = injector.get(ServiceB);
对比一下,我们会发现,这两个写法落差很大。
class B 依赖 class A 的逻辑是定义在 class B 里面

而 class B 依赖 value 的逻辑却是定义在 class B Provider

这种把逻辑分两个地方的写法严重破坏了代码管理。
好在,Angular 还有一种 class 依赖 value 的写法 -- @Inject()
它长这样
@Injectable()
class ServiceB {
constructor(@Inject(VALUE_TOKEN) value: string) {}
}
写法和 class 依赖 class 大同小异,它 compile 后长这样

除了注入 value,@Inject 也可以用于注入 class,像这样
@Injectable()
class ServiceB {
constructor(@Inject(ServiceA) value: ServiceA) {}
}
上面这段代码和我们单写 class 注入
constructor(serviceA: ServiceA) {}
compile 出来是一摸一样的,你也可以认为单写 class 只是一个便捷的写法而已,其原理还是 @Inject(key)。
inject
这段代码
@Injectable()
class ServiceB {
constructor(serviceA: ServiceA) {}
}
会被 compile 成

ɵfac 其实没有很特别,它只是做了 new ServiceB( inject(ServiceA) ) 而已。
最关键的是 ɵɵinject 这个函数。
按这个思路走...倘若我们也可以使用这个 inject 函数,那代码是否可以改成
class ServiceB {
constructor() {
const serviceA = ɵɵinject(ServiceA);
}
}
我们推测看看
function inject() {}
function constructor(depend: any) {}
有一个全局的 inject 函数,和一个 constructor 函数 with 一个依赖参数。
const depend = inject();
constructor(depend);
透过 inject 获取到 depend,然后调用 constructor 传入 depend
对比
function constructor() {
const depend = inject();
}
constructor();
直接在 constructor 内调用 inject 获得 depend。
这 2 种写法在语法上都是成立的。
所以,Angular 确实允许我们使用这个 inject 函数,也可以像上面那样子去注入依赖,甚至这是 v14 版本后的 best practice。
完整的例子:
const VALUE_TOKEN = new InjectionToken<string>('Value');
class ServiceA {
constructor() {
const value = inject(VALUE_TOKEN); // 'Derrick'
}
}
const injector = Injector.create({
providers: [{ provide: VALUE_TOKEN, useValue: 'Derrick' }, ServiceA],
});
const serviceA = injector.get(ServiceA);
注1:
我甚至省略了 @Injectable() decorator,因为这个写法就相等于我们自己写了 ɵfac 方法,
所以就不再需要 compiler 透过反射 constructor 参数写出 ɵɵinject 了。
注2:
我们使用的是 inject 函数,而不是 ɵɵinject 函数。
但凡 starts with ɵ 这个 symbol 的东西都是 Angular 框架 internal 用的,开发者不要去用哦,那都是不稳定的。
而 inject 则是 Angular 公开给我们使用的,其内部就是调用了 ɵɵinject 函数。
injection context & runInInjectionContext
下面这段代码报错了。
import { inject } from '@angular/core';
class ServiceA {}
class ServiceB {
constructor() {
console.log(inject(ServiceA));
}
}
const serviceB = new ServiceB(); // Error: inject() must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`
为什么会报错?为什么 Error 会说 inject() 只能用在一些特定的地方?
我们知道 DI 概念核心是 Injector 和 Provider,上面的代码里,我们没有任何 Injector,所以 inject 自然就不成立。
inject 是一个全局方法,它并没有什么黑魔法,它之所以可以实现依赖注入,依靠的是作用域内(aka injection context)的 Injector。
上面 new ServiceB() 并不会产生任何 injection context,所以 inject 自然就失效了。
来看一个成功的例子
class ServiceA {}
class ServiceB {
constructor() {
console.log(inject(ServiceA));
}
}
const injector = Injector.create({
providers: [ServiceA, ServiceB],
});
const serviceB = injector.get(ServiceB);
injector.get 会产生 injection context,也就是说会有一个全局变量 injector,所以全局函数 inject 可以使用这个 injector 去找到 ServiceA。
除了上面这种方式,还有一个方式可以创建出 injection context。
runInInjectionContext(injector, () => {
const serviceA = inject(ServiceA);
const serviceB = inject(ServiceB);
});
总之
1. inject 只可以在 injection context 内使用。
2. 要创造出 injection context 你要先创建一个 injector。
3. 总结就是:要用 inject 就得先要有 injector。
@Inject()、Provider.deps、inject 傻傻分不清楚
由于众多历史原因,导致了 Angular 有多种方式可以实现同一个功能,这对开发来说是非常不友好的,但幸好 Angular 总是有 best practice。
只要我们乖乖 follow best practice,哪怕傻傻分不清楚也能顺风顺水的开发项目。
不过呢,要搞清楚它们也不是件很困难的事,这里我就来讲讲 @Inject()、inject()、Provider.deps 傻傻分不清楚的地方呗。
@Inject()
@inject 主要的使用场景是在 class constructor 注入 token。
const VALUE_TOKEN = new InjectionToken<string>('Value');
@Injectable()
class ServiceA {
constructor(@Inject(VALUE_TOKEN) value: string) {}
}
const injector = Injector.create({
providers: [ServiceA, { provide: VALUE_TOKEN, useValue: 'Hello World' }],
});
const serviceA = injector.get(ServiceA);
注: 要搭配 @Injectable decorator 哦。
inject 函数可以完全取代 @Inject decorator,上面代码可以改成这样
class ServiceA {
constructor() {
const value = inject(VALUE_TOKEN);
}
}
连 @Injectable decorator 也可以省略掉哦。
Provider.deps
除了 @Inject,还有一种注入方式是透过 Provider.deps
const VALUE_1_TOKEN = new InjectionToken<string>('Value1');
const VALUE_2_TOKEN = new InjectionToken<string>('Value2');
const injector = Injector.create({
providers: [
{ provide: VALUE_1_TOKEN, useValue: 'value 1' },
{
provide: VALUE_2_TOKEN,
useFactory: (value1: string) => `${value1} and value2`,
deps: [VALUE_1_TOKEN],
},
],
});
const value2 = injector.get(VALUE_2_TOKEN);
这个同样可以被 inject 函数替代。
providers: [
{ provide: VALUE_1_TOKEN, useValue: 'value 1' },
{
provide: VALUE_2_TOKEN,
useFactory: (value1: string) => `${inject(VALUE_1_TOKEN)} and value2`,
},
],
inject 函数
显然,inject 函数就是用来替代 @Inject 和 Provider.deps 的,所以尽量用 inject 少用 @Inject 和 Provider.deps 就对了。
Multiple Provider
当出现相同 Provider 时(provide === provide 代表是相同的 Provider),第二个会覆盖第一个
const VALUE_TOKEN = new InjectionToken<string>('Value');
const injector = Injector.create({
providers: [
{ provide: VALUE_TOKEN, useValue: 'first value' },
{ provide: VALUE_TOKEN, useValue: 'second value' },
],
});
const value = injector.get(VALUE_TOKEN); // second value
injector.get 获取到的是 second value。
我们可以透过设置 multi: true 来声明不要 override,取而代之的是把所有的 value 放入 array 返回。
{ provide: VALUE_TOKEN, useValue: 'first value', multi: true }, // 添加 multi: true
{ provide: VALUE_TOKEN, useValue: 'second value', multi: true }, // 添加 multi: true
最终会得到所有 values 的 array
const value = injector.get(VALUE_TOKEN); // ['first value', 'second value']
Injection Optional and Default Value
class ServiceA {}
const injector = Injector.create({
providers: [],
});
const serviceA = injector.get(ServiceA); // Error: No provider for ServiceA
没有 Provider,inject 时就会报错。如果我们不想它报错,想自己处理的话,可以使用 default value。
const serviceA = injector.get(ServiceA, null); // null
get 的第二参数是当找不到 Provider 时,提供一个 default value,这样也就不会报错了。
inject 函数的写法
class ServiceB {
constructor() {
const serviceA = inject(ServiceA, { optional: true }); // null
}
}
注: 它的值是 null 而不是 undefined 哦,Angular 更中意使用 null 而不是 undefined。
@inject 的写法
@Injectable()
class ServiceB {
constructor(@Inject(ServiceA) @Optional() serviceA: ServiceA | null) {
console.log(serviceA);
}
}
用到了 @Optional decorator。decorator 的写法已经逐渐被淘汰了,建议统一用 inject 函数就好了,下面的教程我也不会再提了。
Provider.deps 的写法
const injector = Injector.create({
providers: [
{
provide: ServiceB,
useFactory: (serviceA: ServiceA | null) => {
return new ServiceB(serviceA);
},
deps: [[new Optional(), ServiceA]], // 关键
},
],
});
这个写法比较冷门,而且 deps 的类型是 any[],没有翻文档很难想到它是这么写的。另外 Optional 和 @Optional 是同一个 Optional 哦。
Provider.deps 的写法也已经逐渐被淘汰了,建议统一用 inject 函数就好了,下面的教程我也不会再提了。
循环引用
Angular DI 不支持循环引用。
class ServiceA {
serviceB = inject(ServiceB); // Error: Circular dependency in DI
// 上面的写法相等于下面 constructor 写法,这个是基本 JS 语言
// constructor(){
// serviceB = inject(ServiceB);
// }
}
class ServiceB {
serviceA = inject(ServiceA);
}
const injector = Injector.create({
providers: [ServiceA, ServiceB],
});
const serviceB = injector.get(ServiceB);
直接报错 circular dependency。

破解之法
有些循环引用是设计问题,我们应该要修改设计,但绝大部分情况是因为依赖注入的局限。
依赖注入要求透过 constructor 作为注入入口,但很多时候我们并不需要在 constructor 阶段使用到依赖,更多的是在某些方法中才使用到依赖。
这种情况下循环依赖在设计上,其实并没有问题。
我们可以透过先注入 Injector 的方式来破解。
class ServiceA {
injector = inject(Injector);
method() {
const serviceB = injector.get(ServiceB);
}
}
ServerA 不在 constructor 阶段注入 ServiceB,就不会有循环依赖了,
取而代之的是注入当前的 Injector,在 method 被调用时才去注入 ServiceB。
注:下面这个写法是错误的
class ServiceA {
method() {
const serviceB = inject(ServiceB);
}
}
因为 inject 没有在 injection context 中,只有 constructor 阶段才处于 injection context 内。
我们可以透过 runInInjectionContext 来处理。
class ServiceA {
injector = inject(Injector);
method() {
runInInjectionContext(this.injector, () => {
const serviceB = inject(ServiceB);
});
}
}
这样就 ok 了。
Singleton 单列模式
Injector.get 创建 value 后会把 value 缓存起来,用在 class 的话,这叫单列模式。
class ServiceA {}
const injector = Injector.create({
providers: [ServiceA],
});
const serviceA1 = injector.get(ServiceA);
const serviceA2 = injector.get(ServiceA);
console.log(serviceA1 === serviceA2); // true
下面 useFactory 方法只会被调用一次
const injector = Injector.create({
providers: [{ provide: ServiceA, useFactory: () => new ServiceA() }],
});
const serviceA1 = injector.get(ServiceA);
const serviceA1 = injector.get(ServiceA);
这个是 Angular DI 的设计,如果我们不想要单列,希望每一次都跑 factory 方法....不太容易,下一 part 会教。
Hierarchical Injector
Angular 的 Injector 有 Prototype 的概念(parent child 原型链)
class ParentService {}
class ChildService {}
const parentInjector = Injector.create({
name: 'Parent Injector',
providers: [ParentService],
});
const childInjector = Injector.create({
name: 'Child Injector',
parent: parentInjector, // 连接上 parent
providers: [ChildService],
});
在 Injecter.create 时声明它的 parent 就可以把 injector 串联起来了。
继承的作用自然是原型链查找咯。
const parentService = childInjector.get(ParentService);
透过 childInjector 也可以获取到 ParentService。这就是它的基本玩法。
当 Hierarchical 遇上 Override Provider 和单列模式
上面有提过 Injector 两个特性
-
Injector 只会实例化一次 class。后续的注入会返回同一个实例。(所谓的单列模式)
-
当出现 same Provider 时,后一个会覆盖掉前一个 (除非声明 multi: true)
而这两个特性遇上继承时会有一些些变化,看例子:
each Injector have own scope
class ParentService {}
const parentInjector = Injector.create({
name: 'Parent Injector',
providers: [ParentService],
});
const childInjector = Injector.create({
name: 'Child Injector',
parent: parentInjector,
providers: [ParentService], // override ParentService
});
const parentService1 = parentInjector.get(ParentService);
const parentService2 = childInjector.get(ParentService);
console.log(parentService1 === parentService2); // false
每一个 Injector 都有自己的 scope。它和原型链的查找非常相似。当 childInjector 拥有属于自己的 Provider 时。它就不会去查找 ParentInjector 了。
childInjector.get(ParentService) 会产生一个新的 ParentService 实例。这也是我们避开 Singleton 的唯一办法。
此外, 当声明 multi: true,也不会出现 double service
class ParentService {}
const parentInjector = Injector.create({
name: 'Root',
providers: [{ provide: ParentService, multi: true }],
});
const childInjector = Injector.create({
name: 'Child',
parent: parentInjector,
providers: [{ provide: ParentService, multi: true }],
});
const parentService2 = childInjector.get(ParentService);
console.log(parentService2); // [ParentService]
同样是因为当 childInjector 有属于自己的 Provider 时,它就完全不理会 ParentInjector 了。
self, skipSelf 限制查找
class ParentService {}
class ChildService {}
const parentInjector = Injector.create({
name: 'Parent Injector',
providers: [ParentService],
});
const childInjector = Injector.create({
name: 'Child Injector',
parent: parentInjector,
providers: [ChildService],
});
const parentService1 = childInjector.get(ParentService); // ok
const parentService2 = childInjector.get(ParentService, undefined, {
self: true,
}); // 报错
由于声明了 self: true,childInjector 只能在自己的作用域查找 Provider,所以无法获取到 ParentService 也就报错了。
同理 skipSelf 就是不找自己的作用域,只找祖先的 Injector。
const childService = childInjector.get(ChildService, undefined, {
skipSelf: true,
}); // 报错
@Injectable() and InjectionToken as Provider
特别声明:
下面例子中会用到 providedIn: 'any',这个东西已经在 Angular v15 废弃了,不过目前 v20 依然能用。
相关资讯: Docs – Deprecated APIs and features
本篇为了不涉及其它概念又想体现 "@Injectable() and InjectionToken as Provider“,所以才用了这个 providedIn: 'any'。
虽然 providedIn: 'any' 是废弃了,但是 providedIn 没有废弃,比如 providedIn: 'root' | 'platform' 都还在。
所以你依然可以照着它的概念去理解。
上面所有例子中,我们提供 providers 给 Injector 的方式,对代码管理和 tree shaking 是扣分的。
// service-a.ts
class ServiceA {}
// service-b.ts
class ServiceB {}
// injector.ts
import { ServiceA } from './service-a.ts';
import { ServiceB } from './service-b.ts';
export const injector = Injector.create({
providers: [ServiceA, ServiceB],
});
// app.ts
import { injector } from './injector.ts';
import { ServiceB } from './service-b.ts';
injector.get(ServiceB);
第一,我们必须把所有可能会用到的 Provider 通通 pass 给 Injector,这个很麻烦丫,不小心漏掉一个怎么办,代码管理扣分。
第二,我在 app.ts 只用到了 ServiceB,而且 ServiceB 本身并不依赖 ServiceA,所以整个项目 ServiceA 是应该被 shaking 掉了,
但由于 injector.ts 需要 import 所有可能被用到的 Provider,导致无论如何 ServiceA 都不会被 shaking 掉。tree shaking 扣分。
为了解决上述的问题,Angular 搞了另一种提供 Provider 给 Injector 的方式。
@Injectable() as Provider
下面这样会报错
class ServiceA {}
const injector = Injector.create({
providers: [],
});
console.log(injector.get(ServiceA)); // Error: No provider for ServiceA!
因为 providers 是空的。
我们加上 @Injectable
@Injectable({
providedIn: 'any',
})
class ServiceA {}
const injector = Injector.create({
providers: [],
});
console.log(injector.get(ServiceA)); // OK
这样就不会报错了。
上面我们有提到过,@Injectable 除了可以配合 Angular Compilation 搞黑魔法,它的另一个主要作用便是 as Provider。
为什么 providedIn: 'any' 后,injector 不需要 providers 也可以 get 到 ServiceA 呢?
Injector 源码逛一逛
Injector.create 方法的源码在 injector.ts。

看注释理解。
createInjector 函数源码在 create_injector.ts

createInjectorWithoutInjectorInstances 函数

关键就是创建了 R3Injector 对象。
class R3Injector 的源码在 r3_injector.ts
R3Injector 继承自 EnvironmentInjector

EnvironmentInjector 实现了抽象的 Injector 接口,我们平时用的就是 Injector 接口。

R3Injector 内有两个很重要的属性

一个是 records,它是一个 Map 用来装所有的 providers。
另一个是 parent injector。
有了这两个属性,Injector.get 就可以查找 providers 和祖先 providers 了。
那如果 records 里找不到 Provider 呢?

这个 injectable definition 指的是,经过 compile + @Injectable() 后 ServiceA 的 ɵprov static property。

当 records 找不到,Injector 会去拿 ServiceA.ɵprov injectable definition。
然后检查它的 scope

providedIn: 'any' 表示 in scope(注: 除了 any 其实还可以放其它值,比如 'root' 和 'platform',这个以后章节会教)
接着调 factory 出来执行就拿到 provider value 了。



总结,injector 有 2 种方式可以找到 provider,第一个是去 records 找 provider,另一个是去 definition 找 provider。
InjectionToken as Provider
@Injectable({
providedIn: 'any',
})
class ServiceA {
value = 'Hello World';
}
const VALUE_TOKEN = new InjectionToken('Value', {
providedIn: 'any',
factory() {
const serviceA = inject(ServiceA); // 注入其它依赖
return serviceA.value;
},
});
const injector = Injector.create({
providers: [],
});
console.log(injector.get(VALUE_TOKEN)); // 'Hello World'
和 @Injectable 同样原理,只是 @Injectable 用于 class,InjectionToken 用于其它类型。
Destroy Injector
Injector 是有生命周期的,我们可以随时 destroy 掉它。
class ServiceA {}
const injector = Injector.create({ providers: [ServiceA] });
injector.destroy(); // destroy injector
const serviceA = injector.get(ServiceA); // get ServiceA after destroy
效果

injector destroyed 之后就不能再使用了。
DestroyRef
我们可以透过 DestroyRef 对象来监听 Injector destroy。
const destroyRef = injector.get(DestroyRef);
const removeListener = destroyRef.onDestroy(() => console.log('injector destroyed')); // 当 Injector.destroy() 时会触发这个 callback
// removeListener(); 如果想移除监听,可以调用这个返回函数。
你可能会好奇,DestroyRef 是 Provider 吗?为什么我们可以 injector.get 到它。
相关源码在 r3_injector.ts
R3Injector 在 get 的时候,第一步会做一个 hasOwnProperty 检查。

如果有相关 property 的话 (它应该要是一个方法),就会调用它,并且把 injector 传进去。
DestroyRef 的这个方法会把传入的 injector 直接返回出去。
也就是说
console.log(injector === destroyRef as any); // true
destroyRef 其实就是 injector 来的。
所以我们也可以这样子写
const injector = Injector.create({ providers: [ServiceA] }) as ɵR3Injector;
const removeListener = injector.onDestroy(() => console.log('injector destroyed'));
效果一模一样,只是需要强转类型去 ɵR3Injector 而已 (注:建议还是使用 DestroyRef 会顺风水一点)。
真实项目中 DI 的使用方式
上面例子中,我们都是自己创建 Injector,但其实,在真实项目中,Angular 会替我们创建 Injector,我们不会用到 Injector.create 这个方法。
首先创建一个项目
ng new di --routing=false --style=scss --skip-tests --ssr=false --zoneless
做一个 service-a.ts
export class ServiceA {}
在 Angular 项目中,我们的代码入口是组件,而组件本身又是 class,所以它天生就可以注入。
import { Component, inject } from '@angular/core';
import { ServiceA } from './service-a';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.scss'
})
export class App {
constructor() {
const serviceA = inject(ServiceA); // No provider for ServiceA
console.log(serviceA);
}
}
我们直接在 App 组件 constructor 里透过 inject 函数注入 ServiceA。
结果报错了。因为呢,虽然 Angular 替我们创建了 Injector,但是 DI 还需要 Provider,我们还没有把 ServiceA 提供给 Injector,所以它找不到。
到 app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import { ServiceA } from './service-a';
export const appConfig: ApplicationConfig = {
providers: [
ServiceA, // 1. 添加 Provider
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(),
]
};
appConfig.providers 就是提供给 Injector 的 providers。
main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));
Angular 在 bootstrap application 的时候会把 App 组件和 appConfig 串联上。
于是 App 组件就可以 inject 到 appConfig 的 providers 了。
当然我们也可以用 @Injectable providedIn 的方式提供 Provider 给 Injector。
import { Injectable } from "@angular/core";
@Injectable({
providedIn: 'root', // 'any' 废弃了,通常是用 'root'
})
export class ServiceA {}
这样就可以不需要写在 appConfig 了。
总结
Angular 的 DI 和其它后端 (e.g. ASP.NET Core) 的 DI 大同小异,几个小小区别:
-
not only for class,任何类型都可以使用 DI。
-
单列模式
-
原型链
目前为止,我们大概学习了 50% 关于 Angular DI 的知识,另外 50% 会在后面的章节教。
为什么不一起教?因为太复杂了。当 DI 配上组件会有另一番天地,我们必须先对组件有基础的了解才能把 DI 加进去。
目录
上一篇 Angular 20+ 高阶教程 – Angular Compiler (AKA ngc) Quick View
下一篇 Angular 20+ 高阶教程 – Component 组件 の Angular Component vs Web Component
想查看目录,请移步 Angular 20+ 高阶教程 – 目录
喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding 😊💻


浙公网安备 33010602011771号