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 还算基本类似。

  1. 都是通过 class constructor 参数做注入

  2. 需要 provide 所有的 class 给 injector

  3. 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 点特性。

  1. class can be a key

    class ServiceA {}
    
    const map = new Map();
    map.set(ServiceA, 'value');
    console.log(map.get(ServiceA)); // 'value'

    class 具备识别性,所以它满足 provider key 的特性。

  2. 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

injector 查找需要 key,这里声明依赖 value 但没有表达出它依赖的是 VALUE_TOKEN,所以这表达肯定是不够的。

这样也不对,虽然表达了依赖的 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);
  }
}
连 @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 两个特性

  1. Injector 只会实例化一次 class。后续的注入会返回同一个实例。(所谓的单列模式)

  2. 当出现 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 大同小异,几个小小区别:

  1. not only class,任何类型都可以使用 DI。

  2. 单列模式

  3. 原型链

目前为止,我们大概学习了 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 内部自己玩的东西,或许以后就会被修改了。

 

 

 

posted @ 2022-12-05 21:36  兴杰  阅读(2543)  评论(2编辑  收藏  举报