Angular 18+ 高级教程 – Dependency Injection 依赖注入
前言
本来是想先介绍 Angular Component 的,但 Component 里面会涉及到一些 Dependency Injection (简称 DI) 的概念,所以还是先介绍 DI 吧。
温馨提醒:如果你对 JS class、prototype 不太熟悉的话,建议你先看这篇 JavaScript – 理解 Object, Class, This, Prototype, Function, Mixins
什么是 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) {}
}
有 3 个 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 极其复杂,我们从最简单的版本看起。
class ServiceA {}
@Injectable()
class ServiceB {
constructor(serviceA: ServiceA) {}
}
有 2 个 class,ServiceB 依赖 ServiceA
const injector = Injector.create({
providers: [ServiceA, ServiceB],
});
const serviceB = injector.get(ServiceB);
把 2 个 class 提供给 injector,然后通过 injector 获取 ServiceB。
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 只是其中一种。
我们来过一遍。(虽然有点多,但它们都满足上面 2 个 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。
就是说,2 个不同的 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);
但是,provider 不一定是 class 丫,class 的依赖难道不可以是一个 ValueProvider 吗?
我们来试试看
const VALUE_TOKEN = new InjectionToken<string>('Value'); @Injectable() class ServiceB { constructor(value: string) {} } const injector = Injector.create({ providers: [{ provide: VALUE_TOKEN, useValue: 'value 123' }, ServiceB], }); const serviceB = injector.get(ServiceB);
关键在 ServiceB 的 constructor
这样也不对,虽然表达了依赖的 key,但是 VALUE_TOKEN 是 variable,不是类型,语法错误。
要解决这个问题有 2 种方法。
第一种是改用 ConstructorProvider。
providers: [{ provide: VALUE_TOKEN, useValue: 'value 123' }, ServiceB satisfies TypeProvider] 把 TypeProvider 改成 ConstructorProvider providers: [ { provide: VALUE_TOKEN, useValue: 'value 123' }, { provide: ServiceB, deps: [VALUE_TOKEN] } satisfies ConstructorProvider ]
ConstructorProvider 可以精准声明依赖。甚至不需要 @Injectable() decorator。但这种把逻辑分两个地方的写法严重破坏代码管理,所以不推荐这种写法。
第二种方法是使用 @Inject() decorator。
@Injectable()
class ServiceB {
constructor(@Inject(VALUE_TOKEN) value: string) {}
}
同时表达了 key 和类型。
compile 后长这样
用 @Inject 注入 class
@Injectable()
class ServiceB {
constructor(@Inject(ServiceA) value: ServiceA) {}
}
@Inject 可以注入所有的 provider,当然也包括 class provider。
上面这段代码和我们单写 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); } }
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 废弃了,不过目前 v17 依然能用。
相关资讯: 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 内有 2 个很重要的属性
一个是 records,它是一个 Map 用来装所有的 providers。
另一个是 parent injector。
有了这 2 个属性,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 用于其它类型。
真实项目中 DI 的使用方式
上面例子中,我们都是自己创建 Injector,但其实,在真实项目中,Angular 会替我们创建 Injector,我们不会用到 Injector.create 这个方法。
首先创建一个项目
ng new di --routing=false --style=scss --skip-tests --ssr=false
做一个 service-a.ts
export class ServiceA {}
在 Angular 项目中,我们的代码入口是组件,而组件本身又是 class,所以它天生就可以注入。
import { Component, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ServiceA } from '../service-a'; @Component({ selector: 'app-root', standalone: true, imports: [CommonModule], templateUrl: './app.component.html', styleUrl: './app.component.scss', }) export class AppComponent { constructor() { const serviceA = inject(ServiceA); // No provder for ServiceA console.log(serviceA); } }
我们直接在 AppComponent constructor 里通过 inject 函数注入 ServiceA。
结果报错了。因为呢,虽然 Angular 替我们创建了 Injector,但是 DI 还需要 Provider,我们还没有把 ServiceA 提供给 Injector,所以它找不到。
到 app.config.ts
import { ApplicationConfig } from '@angular/core'; import { ServiceA } from '../service-a'; export const appConfig: ApplicationConfig = { providers: [ServiceA], };
appConfig.providers 就是提供给 Injector 的 providers。
main.ts
import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err) );
Angular 在 bootstrap application 的时候会把 AppComponent 和 appConfig 串联上。
于是 AppComponent 就可以 inject 到 appConfig 的 providers 了。
当然我们也可以用 @Injectable providedIn 的方式提供 Provider 给 Injector。
@Injectable({ providedIn: 'any', }) export class ServiceA {}
总结
Angular 的 DI 和其它后端 (e.g. ASP.NET Core) 的 DI 大同小异,几个小小区别:
-
not only class,任何类型都可以使用 DI。
-
单列模式
-
原型链
目前为止,我们大概学习了 50% 关于 Angular DI 的知识,另外 50% 会在后面的章节教。
为什么不一起教?因为太复杂了。当 DI 配上组件会有另一番天地,我们必须先对组件有基础的了解才能把 DI 加进去。
目录
上一篇 Angular 18+ 高级教程 – Angular Compiler (AKA ngc) Quick View
下一篇 Angular 18+ 高级教程 – Component 组件 の Angular Component vs Web Component
想查看目录,请移步 Angular 18+ 高级教程 – 目录
喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding 😊💻
当 Injection Context Injector == null 遇上 provided: 'root' 尽然可以跑?!
逛源码无意间发现的奇葩情况。
injector_compatibility.ts
当我们调用 inject 时,它会调用 internal ɵɵinject 函数。
然后调用 injectInjectorOnly 函数
有 2 种情况
1. 当 _currentInjector(也就是 injection context 的 Injector) 非 null 时,那就调用 Injector.get 获取依赖值,这个是正常情况。
2. 当 _currentInjector 是 null 时,它会跑 injectRootLimpMode 函数。
inject_switch.ts
injectRootLimpMode 内会调用 @Injection() 或 InjectionToken 的 injectable definition 的 factory 去生成依赖值。
有趣的是,只有 providedIn === 'root' 才会,'any' 和 ‘platform’ 都不会哦。
来一个 playgroud 体会一下呗
把 providedIn 升级去 'any' 或 'platform'
结果报错了。虽然不懂为什么,但我这样操作显然是不合理的,怎么可以把 Injection Context Injector 设置成 null 呢。
所以我估计这个是 Angular 内部自己玩的东西,或许以后就会被修改了。