萱萱宝儿 發表於 2022-12-5 21:36:00

Angular 20+ 高阶教程 – Dependency Injection 依赖注入

<h2>前言</h2>
<p>本来是想先介绍 Angular Component 的,但 Component 里面会涉及到一些 Dependency Injection (简称 DI) 的概念,所以还是先介绍 DI 吧。</p>
<p>&nbsp;</p>
<h2>什么是&nbsp;Dependency Injection?</h2>
<h3>何谓依赖?</h3>
<pre class="language-javascript highlighter-hljs"><code>class ServiceA {
plus(num1: number, num2: number) {
    return num1 + num2;
}
}</code></pre>
<p>首先我们有一个 class ServiceA,它有一个 plus method 可以做加法。</p>
<p>然后我们有另一个 ServiceB</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceB {
plusAndTimesTwo(num1: number, num2: number) {
    return (num1 + num2) * 2;
}
}</code></pre>
<p>它有一个 plus and times two method,做了加法之后再乘于二。</p>
<p>上面的代码虽然可以 work,但破坏了 DRY (Don't Repeat Yourself) 原则。</p>
<p>加法已经在 ServiceA 实现了,怎么可以把实现代码 copy paste 到 ServiceB 呢?</p>
<p>所以我们需要在 ServiceB 引入 ServiceA</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceB {
plusAndTimesTwo(num1: number, num2: number) {
    const serviceA = new ServiceA();
    const afterPlus = serviceA.plus(num1, num2);
   
    return afterPlus * 2;
}
}</code></pre>
<p>简单嘛,实例化 ServiceA 然后调用 plus 方法就可以了。</p>
<p>很好,这就是所谓的 "依赖"。</p>
<p>ServiceB 的某个方法 "依赖" 了 ServiceA 的某个方法。</p>
<h3>何谓注入?</h3>
<p>上面的代码虽然可以 work,但它又破坏了&nbsp;OOP 的 SOLID 原则 – Dependency Inversion Principle(依赖反转原则)</p>
<p>我们在 plusAndTimesTwo 里直接实例化&nbsp;ServiceA,这种写法在一些场景下会给我们带来麻烦。</p>
<p>一个经典的场景是 --&nbsp;单元测试</p>
<p>今天我们想测试 plusAndTimesTwo 这个方法是否实现正确。我们会这么写:</p>
<pre class="language-javascript highlighter-hljs"><code>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;
}
}</code></pre>
<p>.test.ts</p>
<pre class="language-javascript highlighter-hljs"><code>it("test", () =&gt; {
const service = new ServiceB();
const result = service.plusAndTimesTwo(1, 1);
expect(result).toBe(4); // (1 + 1) * 2 = 4
});</code></pre>
<p>结果报错了</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202310/641294-20231008162009468-497355427.png"></p>
<p>查看代码</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202310/641294-20231008162101186-1011902529.png"></p>
<p>没有什么问题啊,怎么会报错呢?</p>
<p>于是找了老半天,发现原来是 ServiceB 的依赖 ServiceA 的 plus method 的实现代码错了。</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202310/641294-20231008162219119-1874658159.png"></p>
<p>我们想测试的是 plusAndTimesTwo,但由于它依赖了 plus,结果 plus 的错误导致了&nbsp;plusAndTimesTwo 也出错了。</p>
<p>所以,这算哪门子的 “单元” 测试呢?</p>
<p>怎么办?这时我们需要引入 "inject 注入" 的概念。</p>
<p>首先,我们不要在 plusAndTimesTwo 去实例化 ServiceA。</p>
<pre class="language-javascript"><code>export class ServiceB {
constructor(private serviceA: ServiceA) {}

plusAndTimesTwo(num1: number, num2: number) {
    const afterPlus = this.serviceA.plus(num1, num2);

    return afterPlus * 2;
}
}</code></pre>
<p>取而代之的是透过 constructor 让外部把 ServiceA “注入” 进来让 plusAndTimesTwo 使用。</p>
<p>然后测试代码改成这样</p>
<pre class="language-javascript highlighter-hljs"><code>it("test ServiceB.plusAndTimesTwo()", () =&gt; {
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
});</code></pre>
<p>在创建 ServiceB 时,我们 "provide 提供" 了 ServiceB 所需要的依赖 ServiceA。</p>
<p>并且这个 ServiceA 不是 new ServiceA(),而是专门为了这个测试而 mock(伪造)的 ServiceA 实例。</p>
<p>注意看:serviceA.plus 的 return 2 是 hardcode 写上去的,没有任何 logic 和 formula。</p>
<p>测试结果</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202310/641294-20231008164856767-886168996.png"></p>
<p>把依赖隔离出来,透过注入的方式去提供,这样就可以非常灵活的控制代码。</p>
<p>上面单元测试中,我们 mock 了 ServiceA 才提供给 ServiceB,这样我们就可以确保 ServiceA 是完全可控的,</p>
<p>不会因为原本 ServiceA 代码实现有误而牵连到 ServiceB,完美的做到了 "单元" 测试。</p>
<h3>何谓依赖注入?</h3>
<p>上面这种注入式的写法确实让代码灵活了许多,但...这难道没有代价吗?</p>
<p>怎么可能?!anything has price 是自古不变的真理。</p>
<p>我们很快就感受到了这种注入式写法的问题。看例子:</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceA {}

class ServiceB {
constructor(serviceA: ServiceA) {}
}

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

// bootstrapApplication(App, appConfig)
//   .catch((err) =&gt; console.error(err));

import { Injectable, Injector } from "@angular/core";

class ServiceA {}

@Injectable()
class ServiceB {
constructor(serviceA: ServiceA) {}
}</code></pre>
<p>define class ServiceA 和 ServiceB</p>
<p>接着 import&nbsp;injector 然后使用它</p>
<pre class="language-javascript highlighter-hljs"><code>const injector = Injector.create({
providers: ,
});
const serviceB = injector.get(ServiceB);

console.log('serviceB', serviceB);</code></pre>
<p>把两个 class 提供给 injector,然后透过 injector 获取 ServiceB。</p>
<p>运行 command</p>
<div class="cnblogs_code">
<pre>ng serve --open</pre>
</div>
<p>效果</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250526174845342-256708151.png"></p>
<p>injector 会替我们实例化 ServiceB,并且满足其依赖,相当于:</p>
<pre class="language-javascript highlighter-hljs"><code>const serviceB = new ServiceB(new ServiceA());</code></pre>
<h3>Angular 如何知道 class 有哪些依赖?</h3>
<p>JS 没有反射,那 Angular 怎么能从 ServiceB 的 constructor 感知到其依赖 ServiceA 呢?</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceB {
constructor(serviceA: ServiceA) {}
}</code></pre>
<p>答案是黑魔法&nbsp;Compilation。</p>
<p>ServiceB 经过 compile 后会变成这样</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202310/641294-20231008203912936-329573576.png"></p>
<p>它多了一个&nbsp;ɵfac 静态方法。</p>
<p>从代码上可以推测出 injector.get(ServiceB),其实并不是直接执行了 new ServiceB(new ServiceA()),它只是调用了 ServiceB.ɵfac()。</p>
<p>而 ɵfac 内容才是 new ServiceB( inject(ServiceA) )。这句代码便是 compiler 透过反射 constructor 得知 ServiceB 依赖 ServiceA 后写出来的。</p>
<p>另外,inject(ServiceA) 是一个递归实例化依赖函数,里面一定是调用了&nbsp;ServiceA.ɵfac()。以此类推,一直到所有的依赖全部被实例化。</p>
<p>简而言之,虽然 JS 没有反射,但是 Angular compiler 可以反射,然后自动编写出实例化依赖的代码。这就是 Angular DI 的实现秘诀啦。</p>
<h3>@Injectable()</h3>
<p>眼尖的朋友可能已经发现了 @Injectable() 在上面的 class ServiceB</p>
<pre class="language-javascript highlighter-hljs"><code>@Injectable()
class ServiceB {
constructor(serviceA: ServiceA) {}
}</code></pre>
<p>@Injectable decorator 有两种用途,我们先了解其中一个就好。</p>
<p>如果一个 class 没有声明 @Injectable decorator 那 Angular compiler 就不会理它。</p>
<pre class="language-javascript highlighter-hljs"><code>@Injectable()
class ServiceB {
constructor(serviceA: ServiceA) {}
}

class ServiceC {
constructor(serviceA: ServiceA) {}
}

const injector = Injector.create({
providers: ,
});</code></pre>
<p>ServiceC 和 ServiceB 一样都声明了依赖 ServiceA,并且 ServiceC 也在 providers 里。唯一的区别是 ServiceC 没有 @Injectable decorator。</p>
<p>下面是 compile 后的代码</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202310/641294-20231008211531808-456756318.png"></p>
<p>ServiceC 只是一个普普通通的 class,没有 static 方法&nbsp;ɵfac。</p>
<p>若我们尝试去注入它</p>
<pre class="language-javascript highlighter-hljs"><code>const serviceB = injector.get(ServiceC);</code></pre>
<p>将得到一个 runtime error。</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202310/641294-20231008211817126-892531970.png"></p>
<p>所以,记得要加上 @Injectable() 给有需要依赖的 class 哦。</p>
<p>小考题:ServiceA 没有任何依赖,那它需要 @Injectable() 吗?</p>
<p>答案:不需要。但通常我们还是会放的,因为上面说了,@Injectable() 还有第二个用途,下面会教。</p>
<p>&nbsp;</p>
<h2>中场休息,小总结</h2>
<p>到目前为止,Angular 的 DI 和其它框架(后端框架)使用的 DI 还算基本类似。</p>
<ol>
<li>
<p>都是透过 class constructor 参数做注入</p>
</li>
<li>
<p>需要 provide 所有的 class 给 injector</p>
</li>
<li>
<p>injector 负责实例化和满足所有 class 的依赖。</p>
</li>
</ol>
<p>比较特别的地方是 @injectable() decorator,由于 JS 没有 reflection,Angular 无法从 class constructor 参数反射出其依赖的 class,</p>
<p>所以 Angular 需要利用 compiler + decorator 去实现 reflection。</p>
<p>好,到这边,我们大概学习了 Angular DI 大约 20% 的内容...</p>
<p>下半场,我们来学习 Angular DI 独有的一些特性。</p>
<p>&nbsp;</p>
<h2>Injector and&nbsp;Provider 详解</h2>
<h3>not only for class</h3>
<p>上面例子中,我们都是在处理 class,但其实 Angular DI 并不限于 class,任何类型都可以使用 DI 概念。</p>
<pre class="language-javascript highlighter-hljs"><code>const injector = Injector.create({
providers: [{ provide: 'key', useValue: 'hello world' }],
});

const value = injector.get('key');
console.log(value); // 'hello world'</code></pre>
<p>看到吗,上面代码中,完全没有 class 的影子,但 injector 照常工作。</p>
<h3>抽象理解 provider 和 injector</h3>
<h4>provider 的特性</h4>
<p>class 是一种 provider,但 provider 不仅仅只限于 class。</p>
<p>抽象的看,provider 是一个 key value pair 对象。</p>
<p>key 的作用是为了识别。</p>
<p>value 则是一个提供最终值的 factory 函数。</p>
<p>只要能满足这 2 点,那它就可以被作为 provider。</p>
<p>那我们来看看 class 是否满足这 2 点特性。</p>
<ol>
<li>
<p>class can be a key</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceA {}

const map = new Map();
map.set(ServiceA, 'value');
console.log(map.get(ServiceA)); // 'value'</code></pre>
<p>class 具备识别性,所以它满足 provider key 的特性。</p>
</li>
<li>
<p>class can be a value factory</p>
<p>class 有 constructor 并且能实例化出对象。</p>
<p>这就满足了 provider value factory 的特性。</p>
<p>所以,结论是 class 可以被当成 provider 使用。</p>
</li>
</ol>
<h4>Injector 的特性</h4>
<p>injector 不仅仅是实例化机器。</p>
<p>抽象的看,injector 第一个任务是透过 key 查找出指定的 provider,这个 key 只要具备可识别性就可以了。比如:string,class,symbol 等等都具备识别性。</p>
<p>第二个任务是透过 provider value factory 生产出最终的值。当然如果这个 factory 需要依赖,injector 会先查找它所需要的依赖,注入给 factory 函数。</p>
<p>拿 class ServiceB 做例子:</p>
<p>injector 要找一个 key = ServiceA 的 provider。找到以后透过 value factory(也就是 ServiceA.ɵfac)生产出最终的值 (ServiceA 实例)。</p>
<h3>Provider &amp; StaticProvider</h3>
<p>Angular 有多种不同形态的 Provider,class 只是其中一种。</p>
<p>我们来过一遍。(虽然有点多,但它们都满足上面两个 provider 的特性,所以其实很好理解的)</p>
<p>Injector.create 的 interface 长这样</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202310/641294-20231009043544202-1550101708.png"></p>
<p>Provider 和 StaticProvider 是所有 Provider 的抽象。</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202310/641294-20231009043626484-362622670.png"></p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202310/641294-20231009043641087-1911891121.png"></p>
<p>它俩是有重叠的,总的来说是 TypeProvider、ClassProvider、StaticClassProvider、ConstructorProvider、FactoryProvider、ValueProvider、ExistingProvider。</p>
<h3>FactoryProvider</h3>
<p>FactoryProvider 是最 low layer 的 Provider,它可以做到其它所有 Provider 能做到的,所以我们只要理解了 FactoryProvider,再去理解其它 Provider 就非常容易了。</p>
<pre class="language-javascript highlighter-hljs"><code>const myValueProvider: FactoryProvider = {
provide: 'key',
useFactory: () =&gt; {
    return 'my value';
},
};</code></pre>
<p>这是一个 FactoryProvider,上面讲过,Provider 需要有两个特性</p>
<p>第一个是它需要有一个可识别的 key,在这个例子中就是 myValueProvider.provide</p>
<p>第二个是它需要有一个 value factory 方法,在这个例子中就是&nbsp;myValueProvider.useFactory</p>
<p>FactoryProvider 是这样被使用的</p>
<pre class="language-javascript highlighter-hljs"><code>const injector = Injector.create({
providers: ,
});

const value = injector.get('key'); // my value</code></pre>
<h3>FactoryProvider with InjectionToken</h3>
<p>上面的代码有个小问题</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202311/641294-20231121141341546-1003949333.png"></p>
<p>由于我使用了 string 作为 Provider 的识别 key,所以它出现了 warning。原因是 string 太容易撞了,Jser 都知道作为识别 key 的话,Symbol 比 string 可靠。</p>
<p>而由于 Angular 需要有类型概念,所以连 JS 的 Symbol 都无法满足它的需求,于是 Angular 自己搞了一个&nbsp;InjectionToken 类来替代 JS 的 Symbol。</p>
<p>下面这个是用&nbsp;InjectionToken 替代了原本 string key 的写法</p>
<pre class="language-javascript highlighter-hljs"><code>const MY_VALUE_TOKEN = new InjectionToken&lt;string&gt;('MyValue');

const myValueProvider: FactoryProvider = {
provide: MY_VALUE_TOKEN,
useFactory: (): string =&gt; {
    return 'my value';
},
};

const injector = Injector.create({
providers: ,
});

const value = injector.get(MY_VALUE_TOKEN);</code></pre>
<p>如果类型想要更准确的写法,还可以修一点</p>
<pre class="language-javascript highlighter-hljs"><code>type GetInjectionTokenType&lt;T&gt; = T extends InjectionToken&lt;infer R&gt; ? R : never;

const myValueProvider: FactoryProvider = {
provide: MY_VALUE_TOKEN,
useFactory: (): GetInjectionTokenType&lt;typeof MY_VALUE_TOKEN&gt; =&gt; {
    return 'my value';
},
};</code></pre>
<h3>FactoryProvider with Dependency</h3>
<p>如果我们的 value factory 有依赖,可以这样写</p>
<pre class="language-javascript highlighter-hljs"><code>const MY_VALUE_1_TOKEN = new InjectionToken&lt;string&gt;('MyValue1');
const MY_VALUE_2_TOKEN = new InjectionToken&lt;string&gt;('MyValue2');

const myValue1Provider: FactoryProvider = {
provide: MY_VALUE_1_TOKEN,
useFactory: () =&gt; 'my value 1',
};

const myValue2Provider: FactoryProvider = {
provide: MY_VALUE_1_TOKEN,
useFactory: (myValue1: string) =&gt; myValue1 + 'and my value 2',
deps: ,
};

const injector = Injector.create({
providers: ,
});

const value2 = injector.get(MY_VALUE_2_TOKEN); // my value 1 and my value 2</code></pre>
<p>关键在 myValue2Provider</p>
<pre class="language-javascript highlighter-hljs"><code>const myValue2Provider: FactoryProvider = {
provide: MY_VALUE_1_TOKEN,
useFactory: (myValue1: string) =&gt; myValue1 + 'and my value 2',
deps: ,
};</code></pre>
<p>属性 deps 用来声明 value factory 的依赖。当 useFactory 被调用时,injector 会注入其依赖。</p>
<p>好,至此我们就掌握了最底层的 Provider,其它的 Provider 都可以用 FactoryProvider 来实现,下面我一一列出来,并且附上 FactoryProvider 实现的版本。</p>
<h3>ValueProvider</h3>
<pre class="language-javascript highlighter-hljs"><code>const VALUE_TOKEN = new InjectionToken&lt;string&gt;('Value');

const valueProvider: ValueProvider = {
provide: VALUE_TOKEN,
useValue: 'Derrick',
};

const injector = Injector.create({
providers: ,
});

const name = injector.get(VALUE_TOKEN);
console.log(name); // 'Derrick'</code></pre>
<p>value provider 和 factory provider 的区别是,它省略了 value factory,直接 hardcode 一个 value。</p>
<p>用 factory provider 重新表达上面这个例子,那会是这样</p>
<pre class="language-javascript highlighter-hljs"><code>const valueProvider: FactoryProvider = {
provide: VALUE_TOKEN,
useFactory: () =&gt; 'Derrick'
};</code></pre>
<h3>TypeProvider、ConstructorProvider、ClassProvider、StaticClassProvider</h3>
<p>这 4 个 Provider 都用于 class,它们只有微微的区别,我没有去研究为什么 Angular 搞了 4 个傻傻分不清楚的东西,但我猜想可能是历史原因,我们过一遍就好,真实开发中是不会用到的。</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceA {}
class ServiceB {
constructor(serviceA: ServiceA) {}
}</code></pre>
<p>ServiceB 依赖 ServiceA(注:我刻意不放 @Injectable() 来凸显不同 Provider 之前的特性)</p>
<h4>TypeProvider</h4>
<pre class="language-javascript highlighter-hljs"><code>const injector = Injector.create({
providers: [
    ServiceA,
    ServiceB satisfies TypeProvider,
],
});</code></pre>
<p>TypeProvider 就是一个普通的 class。上面的例子都是用这个。</p>
<p>由于我没有放 @Injectable() 若调用 injector.get(ServiceB) 将会报错哦。</p>
<h4>ConstructorProvider&nbsp;</h4>
<pre class="language-javascript highlighter-hljs"><code>providers: [
ServiceA,
{ provide: ServiceB, deps: } satisfies ConstructorProvider
],</code></pre>
<p>ConstructorProvider 和 TypeProvider 的区别是它多了一个属性 deps。</p>
<p>它的作用是让我们放入 constructor 中的依赖,按顺序放。有了这个,即便没有 @Injectable() 也可以成功 get 到 ServiceB。</p>
<h4>ClassProvider</h4>
<pre class="language-javascript highlighter-hljs"><code>const SERVICE_B_TOKEN = new InjectionToken&lt;ServiceB&gt;('ServiceB');

providers: [
ServiceA,
{ provide: SERVICE_B_TOKEN, useClass: ServiceB } satisfies ClassProvider,
],</code></pre>
<p>ClassProvider 的特别之处是它允许我们定义任意类型的 key。</p>
<p>provide 声明了 key,useClass 声明了 value factory。(注:ClassProvider 不能声明 deps 哦。)</p>
<p>get 的方式是</p>
<pre class="language-javascript highlighter-hljs"><code>const serviceB = injector.get(SERVICE_B_TOKEN);</code></pre>
<p>SERVICE_B_TOKEN&nbsp;是 key,injector 会找到这个 provider,发现 provider 声明了 useClass,表示它是一个 ClassProvider,然后就会实例化 useClass 的值。</p>
<p>任意 key 的好处是可以声明抽象,提供具体。比如</p>
<pre class="language-javascript highlighter-hljs"><code>{ provide: AnimalService, useClass: DogService } satisfies ClassProvider,</code></pre>
<p>依赖抽象总是好的嘛,说不定那天可以替换成一个优化版的具体实现,到时就只需要替换 useClass 的值,而不是全场替换依赖。</p>
<h4>StaticClassProvider</h4>
<pre class="language-javascript highlighter-hljs"><code>providers: [
ServiceA,
{ provide: SERVICE_B_TOKEN, useClass: ServiceB, deps: } satisfies StaticClassProvider,
],</code></pre>
<p>StaticClassProvider是最完整的 class provider。</p>
<p>它可以定义 key,也可以声明 deps。相等于 ConstructorProvider + ClassProvider。<span style="font-size: 1.17em"><br></span></p>
<p>用 factory provider 重新表达上面这个例子,那会是这样</p>
<pre class="language-javascript highlighter-hljs"><code>providers: [
{ provide: ServiceA, useFactory: () =&gt; new ServiceA() },
{
    provide: SERVICE_B_TOKEN,
    useFactory: (serviceA: ServiceA) =&gt; new ServiceB(serviceA),
    deps: ,
},
],</code></pre>
<h3>ExistingProvider</h3>
<p>ExisitingProvider 的作用是提供 alias key。</p>
<p>就是说,两个不同的 key,但其实指向的是同一个 provider。</p>
<pre class="language-javascript highlighter-hljs"><code>const NAME_1_TOKEN = new InjectionToken&lt;string&gt;('Name1');
const NAME_2_TOKEN = new InjectionToken&lt;string&gt;('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'</code></pre>
<p>key name1 是 value provider,它提供了 value 'Derrick'。</p>
<p>key name2 是 existing provider,它没有提供任何值,但它提供了一个 key,这个 key 指向 name1。</p>
<p>所以 injector 找 name2 &gt; 被指向 name1 &gt; 最终就获得了 name1 的值。</p>
<p>有点像 URL 301 redirect 那个概念。</p>
<p>用 factory provider 重新表达上面这个例子,那会是这样</p>
<pre class="language-javascript highlighter-hljs"><code>{ provide: NAME_2_TOKEN, useFactory: (name1: string) =&gt; name1, deps: },</code></pre>
<p>&nbsp;</p>
<h2>@Inject()</h2>
<p>下面这个是 class 依赖 class 的写法</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceA {}

@Injectable()
class ServiceB {
constructor(serviceA: ServiceA) {}
}

const injector = Injector.create({
providers: ,
});

const serviceB = injector.get(ServiceB);</code></pre>
<p>下面这个是 class 依赖 value 的写法</p>
<pre class="language-javascript highlighter-hljs"><code>const VALUE_TOKEN = new InjectionToken&lt;string&gt;('Value');

class ServiceB {
constructor(value: string) {}
}

const injector = Injector.create({
providers: [
    { provide: VALUE_TOKEN, useValue: 'value 123' },
    { provide: ServiceB, useClass: ServiceB, deps: }, // 关键在 deps
],
});

const serviceB = injector.get(ServiceB);</code></pre>
<p>对比一下,我们会发现,这两个写法落差很大。</p>
<p>class B 依赖 class A 的逻辑是定义在 class B 里面</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250524235837816-434400558.png"></p>
<p>而 class B 依赖 value 的逻辑却是定义在 class B Provider</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250524235950632-1376088310.png"></p>
<p>这种把逻辑分两个地方的写法严重破坏了代码管理。</p>
<p>好在,Angular 还有一种 class 依赖 value 的写法 --&nbsp;@Inject()</p>
<p>它长这样</p>
<pre class="language-javascript highlighter-hljs"><code>@Injectable()
class ServiceB {
constructor(@Inject(VALUE_TOKEN) value: string) {}
}</code></pre>
<div>
<p>写法和 class 依赖 class 大同小异,它 compile 后长这样</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202310/641294-20231009144526837-1501520933.png"></p>
<p>除了注入 value,@Inject 也可以用于注入 class,像这样</p>
<pre class="language-javascript highlighter-hljs"><code>@Injectable()
class ServiceB {
constructor(@Inject(ServiceA) value: ServiceA) {}
}</code></pre>
<p>上面这段代码和我们单写 class 注入</p>
<pre class="language-javascript highlighter-hljs"><code>constructor(serviceA: ServiceA) {}</code></pre>
<p>compile 出来是一摸一样的,你也可以认为单写 class 只是一个便捷的写法而已,其原理还是 @Inject(key)。</p>
<p>&nbsp;</p>
<h2>inject</h2>
<p>这段代码</p>
<pre class="language-javascript highlighter-hljs"><code>@Injectable()
class ServiceB {
constructor(serviceA: ServiceA) {}
}</code></pre>
<p>会被 compile 成</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202310/641294-20231009145835289-1493914173.png"></p>
<p>ɵfac 其实没有很特别,它只是做了 new ServiceB( inject(ServiceA) ) 而已。</p>
<p>最关键的是&nbsp;ɵɵinject 这个函数。</p>
<p>按这个思路走...倘若我们也可以使用这个 inject 函数,那代码是否可以改成</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceB {
constructor() {
    const serviceA = ɵɵinject(ServiceA);
}
}</code></pre>
<p>我们推测看看</p>
<pre class="language-javascript highlighter-hljs"><code>function inject() {}
function constructor(depend: any) {}</code></pre>
<p>有一个全局的 inject 函数,和一个 constructor 函数 with 一个依赖参数。</p>
<pre class="language-javascript highlighter-hljs"><code>const depend = inject();
constructor(depend);</code></pre>
<p>透过 inject 获取到 depend,然后调用&nbsp;constructor 传入 depend</p>
<p>对比</p>
<pre class="language-javascript highlighter-hljs"><code>function constructor() {
const depend = inject();
}
constructor();</code></pre>
<p>直接在&nbsp;constructor 内调用 inject 获得 depend。</p>
<p>这 2 种写法在语法上都是成立的。</p>
<p>所以,Angular 确实允许我们使用这个 inject 函数,也可以像上面那样子去注入依赖,甚至这是 v14 版本后的 best practice。</p>
<p>完整的例子:</p>
<pre class="language-javascript highlighter-hljs"><code>const VALUE_TOKEN = new InjectionToken&lt;string&gt;('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);</code></pre>
<p>注1:</p>
<p>我甚至省略了 @Injectable() decorator,因为这个写法就相等于我们自己写了&nbsp;ɵfac 方法,</p>
<p>所以就不再需要 compiler 透过反射 constructor 参数写出&nbsp;ɵɵinject 了。</p>
<p>注2:</p>
<p>我们使用的是 inject 函数,而不是 ɵɵinject 函数。</p>
<p>但凡 starts with&nbsp;ɵ 这个 symbol 的东西都是 Angular 框架 internal 用的,开发者不要去用哦,那都是不稳定的。</p>
<p>而 inject 则是 Angular 公开给我们使用的,其内部就是调用了&nbsp;ɵɵinject 函数。</p>
<h3>injection context &amp; runInInjectionContext</h3>
<p>下面这段代码报错了。</p>
<pre class="language-javascript highlighter-hljs"><code>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`</code></pre>
<p>为什么会报错?为什么 Error 会说 inject() 只能用在一些特定的地方?</p>
<p>我们知道 DI 概念核心是 Injector 和 Provider,上面的代码里,我们没有任何 Injector,所以 inject 自然就不成立。</p>
<p>inject 是一个全局方法,它并没有什么黑魔法,它之所以可以实现依赖注入,依靠的是作用域内(aka injection context)的 Injector。</p>
<p>上面 new ServiceB() 并不会产生任何 injection context,所以 inject 自然就失效了。</p>
<p>来看一个成功的例子</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceA {}
class ServiceB {
constructor() {
    console.log(inject(ServiceA));
}
}
const injector = Injector.create({
providers: ,
});

const serviceB = injector.get(ServiceB);</code></pre>
<p>injector.get 会产生 injection context,也就是说会有一个全局变量 injector,所以全局函数 inject 可以使用这个 injector 去找到 ServiceA。</p>
<p>除了上面这种方式,还有一个方式可以创建出 injection context。</p>
<pre class="language-javascript highlighter-hljs"><code>runInInjectionContext(injector, () =&gt; {
const serviceA = inject(ServiceA);
const serviceB = inject(ServiceB);
});</code></pre>
<p>总之</p>
<p>1. inject 只可以在 injection context 内使用。</p>
<p>2. 要创造出 injection context 你要先创建一个 injector。</p>
<p>3. 总结就是:要用 inject 就得先要有 injector。</p>
<p>&nbsp;</p>
<h2>@Inject()、Provider.deps、inject 傻傻分不清楚</h2>
<p>由于众多历史原因,导致了 Angular 有多种方式可以实现同一个功能,这对开发来说是非常不友好的,但幸好 Angular 总是有 best practice。</p>
<p>只要我们乖乖 follow best practice,哪怕傻傻分不清楚也能顺风顺水的开发项目。</p>
<p>不过呢,要搞清楚它们也不是件很困难的事,这里我就来讲讲 @Inject()、inject()、Provider.deps 傻傻分不清楚的地方呗。</p>
<h3>@Inject()</h3>
<p>@inject 主要的使用场景是在 class constructor 注入 token。</p>
<pre class="language-javascript highlighter-hljs"><code>const VALUE_TOKEN = new InjectionToken&lt;string&gt;('Value');

@Injectable()
class ServiceA {
constructor(@Inject(VALUE_TOKEN) value: string) {}
}

const injector = Injector.create({
providers: ,
});

const serviceA = injector.get(ServiceA);</code></pre>
<p>注: 要搭配 @Injectable decorator 哦。</p>
<p>inject 函数可以完全取代 @Inject decorator,上面代码可以改成这样</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceA {
constructor() {
    const value = inject(VALUE_TOKEN);
}
}</code></pre>
连 @Injectable decorator 也可以省略掉哦。
<h3>Provider.deps</h3>
<p>除了 @Inject,还有一种注入方式是透过 Provider.deps</p>
<pre class="language-javascript highlighter-hljs"><code>const VALUE_1_TOKEN = new InjectionToken&lt;string&gt;('Value1');
const VALUE_2_TOKEN = new InjectionToken&lt;string&gt;('Value2');

const injector = Injector.create({
providers: [
    { provide: VALUE_1_TOKEN, useValue: 'value 1' },
    {
      provide: VALUE_2_TOKEN,
      useFactory: (value1: string) =&gt; `${value1} and value2`,
      deps: ,
    },
],
});

const value2 = injector.get(VALUE_2_TOKEN);</code></pre>
<p>这个同样可以被 inject 函数替代。</p>
<pre class="language-javascript highlighter-hljs"><code>providers: [
{ provide: VALUE_1_TOKEN, useValue: 'value 1' },
{
    provide: VALUE_2_TOKEN,
    useFactory: (value1: string) =&gt; `${inject(VALUE_1_TOKEN)} and value2`,
},
],</code></pre>
<h3>inject 函数</h3>
<p>显然,inject 函数就是用来替代 @Inject 和 Provider.deps 的,所以尽量用 inject 少用 @Inject 和 Provider.deps 就对了。</p>
<p>&nbsp;</p>
<h2>Multiple Provider</h2>
<p>当出现相同 Provider 时(provide === provide 代表是相同的 Provider),第二个会覆盖第一个</p>
<pre class="language-javascript highlighter-hljs"><code>const VALUE_TOKEN = new InjectionToken&lt;string&gt;('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</code></pre>
<p>injector.get 获取到的是 second value。</p>
<p>我们可以透过设置 multi: true 来声明不要 override,取而代之的是把所有的 value 放入 array 返回。</p>
<pre class="language-javascript highlighter-hljs"><code>{ provide: VALUE_TOKEN, useValue: 'first value', multi: true }, // 添加 multi: true
{ provide: VALUE_TOKEN, useValue: 'second value', multi: true }, // 添加 multi: true</code></pre>
<p>最终会得到所有 values 的 array</p>
<pre class="language-javascript highlighter-hljs"><code>const value = injector.get(VALUE_TOKEN); // ['first value', 'second value']</code></pre>
<p>&nbsp;</p>
<h2>Injection Optional and Default Value</h2>
<pre class="language-javascript highlighter-hljs"><code>class ServiceA {}
const injector = Injector.create({
providers: [],
});
const serviceA = injector.get(ServiceA); // Error: No provider for ServiceA</code></pre>
<p>没有 Provider,inject 时就会报错。如果我们不想它报错,想自己处理的话,可以使用 default value。</p>
<pre class="language-javascript highlighter-hljs"><code>const serviceA = injector.get(ServiceA, null); // null</code></pre>
<p>get 的第二参数是当找不到 Provider 时,提供一个 default value,这样也就不会报错了。</p>
<p>inject 函数的写法</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceB {
constructor() {
    const serviceA = inject(ServiceA, { optional: true }); // null
}
}</code></pre>
<p>注: 它的值是 null 而不是 undefined&nbsp;哦,Angular 更中意使用 null 而不是 undefined。</p>
<p>@inject 的写法</p>
<pre class="language-javascript highlighter-hljs"><code>@Injectable()
class ServiceB {
constructor(@Inject(ServiceA) @Optional() serviceA: ServiceA | null) {
    console.log(serviceA);
}
}</code></pre>
<p>用到了 @Optional decorator。decorator 的写法已经逐渐被淘汰了,建议统一用 inject 函数就好了,下面的教程我也不会再提了。</p>
<p>Provider.deps 的写法</p>
<pre class="language-javascript highlighter-hljs"><code>const injector = Injector.create({
providers: [
    {
      provide: ServiceB,
      useFactory: (serviceA: ServiceA | null) =&gt; {
      return new ServiceB(serviceA);
      },
      deps: [], // 关键
    },
],
});</code></pre>
<p>这个写法比较冷门,而且 deps 的类型是 any[],没有翻文档很难想到它是这么写的。另外&nbsp;Optional 和 @Optional 是同一个 Optional 哦。</p>
<p>Provider.deps 的写法也已经逐渐被淘汰了,建议统一用 inject 函数就好了,下面的教程我也不会再提了。</p>
<p>&nbsp;</p>
<h2>循环引用</h2>
<p>Angular DI 不支持循环引用。</p>
<pre class="language-javascript highlighter-hljs"><code>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: ,
});
const serviceB = injector.get(ServiceB);</code></pre>
<p>直接报错 circular dependency。</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202212/641294-20221207130248869-1213916141.png"></p>
<h3>破解之法</h3>
<p>有些循环引用是设计问题,我们应该要修改设计,但绝大部分情况是因为依赖注入的局限。</p>
<p>依赖注入要求透过 constructor 作为注入入口,但很多时候我们并不需要在&nbsp;constructor 阶段使用到依赖,更多的是在某些方法中才使用到依赖。</p>
<p>这种情况下循环依赖在设计上,其实并没有问题。</p>
<p>我们可以透过先注入 Injector 的方式来破解。</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceA {
injector = inject(Injector);
method() {
    const serviceB = injector.get(ServiceB);
}
}</code></pre>
<p>ServerA 不在 constructor 阶段注入 ServiceB,就不会有循环依赖了,</p>
<p>取而代之的是注入当前的 Injector,在 method 被调用时才去注入 ServiceB。</p>
<p>注:下面这个写法是错误的</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceA {
method() {
    const serviceB = inject(ServiceB);
}
}</code></pre>
<p>因为 inject 没有在 injection context 中,只有 constructor 阶段才处于 injection context 内。</p>
<p>我们可以透过 runInInjectionContext 来处理。</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceA {
injector = inject(Injector);
method() {
    runInInjectionContext(this.injector, () =&gt; {
      const serviceB = inject(ServiceB);
    });
}
}</code></pre>
<p>这样就 ok 了。</p>
<p>&nbsp;</p>
<h2>Singleton 单列模式</h2>
<p>Injector.get 创建 value 后会把 value 缓存起来,用在 class 的话,这叫单列模式。</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceA {}

const injector = Injector.create({
providers: ,
});
const serviceA1 = injector.get(ServiceA);
const serviceA2 = injector.get(ServiceA);
console.log(serviceA1 === serviceA2); // true</code></pre>
<p>下面 useFactory 方法只会被调用一次</p>
<pre class="language-javascript highlighter-hljs"><code>const injector = Injector.create({
providers: [{ provide: ServiceA, useFactory: () =&gt; new ServiceA() }],
});
const serviceA1 = injector.get(ServiceA);
const serviceA1 = injector.get(ServiceA);</code></pre>
<p>这个是 Angular DI 的设计,如果我们不想要单列,希望每一次都跑 factory 方法....不太容易,下一 part 会教。</p>
<p>&nbsp;</p>
<h2>Hierarchical Injector</h2>
<p>Angular 的 Injector 有 Prototype 的概念(parent child 原型链)</p>
<pre class="language-javascript highlighter-hljs"><code>class ParentService {}
class ChildService {}

const parentInjector = Injector.create({
name: 'Parent Injector',
providers: ,
});

const childInjector = Injector.create({
name: 'Child Injector',
parent: parentInjector, // 连接上 parent
providers: ,
});</code></pre>
<p>在 Injecter.create 时声明它的 parent 就可以把 injector 串联起来了。</p>
<p>继承的作用自然是原型链查找咯。</p>
<pre class="language-javascript highlighter-hljs"><code>const parentService = childInjector.get(ParentService);</code></pre>
<p>透过 childInjector 也可以获取到 ParentService。这就是它的基本玩法。</p>
<h3>当 Hierarchical 遇上 Override Provider 和单列模式</h3>
<p>上面有提过 Injector 两个特性</p>
<ol>
<li>
<p>Injector 只会实例化一次 class。后续的注入会返回同一个实例。(所谓的单列模式)</p>
</li>
<li>
<p>当出现 same Provider 时,后一个会覆盖掉前一个 (除非声明 multi: true)</p>
</li>
</ol>
<p>而这两个特性遇上继承时会有一些些变化,看例子:</p>
<h4>each Injector have own scope</h4>
<pre class="language-javascript highlighter-hljs"><code>class ParentService {}

const parentInjector = Injector.create({
name: 'Parent Injector',
providers: ,
});

const childInjector = Injector.create({
name: 'Child Injector',
parent: parentInjector,
providers: , // override ParentService
});

const parentService1 = parentInjector.get(ParentService);
const parentService2 = childInjector.get(ParentService);
console.log(parentService1 === parentService2); // false</code></pre>
<p>每一个 Injector 都有自己的 scope。它和原型链的查找非常相似。当 childInjector 拥有属于自己的 Provider 时。它就不会去查找 ParentInjector 了。&nbsp;</p>
<p>childInjector.get(ParentService) 会产生一个新的 ParentService 实例。这也是我们避开 Singleton 的唯一办法。</p>
<p>此外, 当声明 multi: true,也不会出现 double service</p>
<pre class="language-javascript highlighter-hljs"><code>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); // </code></pre>
<p>同样是因为当 childInjector 有属于自己的 Provider 时,它就完全不理会 ParentInjector 了。</p>
<h3>self, skipSelf 限制查找</h3>
<pre class="language-javascript highlighter-hljs"><code>class ParentService {}
class ChildService {}

const parentInjector = Injector.create({
name: 'Parent Injector',
providers: ,
});

const childInjector = Injector.create({
name: 'Child Injector',
parent: parentInjector,
providers: ,
});

const parentService1 = childInjector.get(ParentService); // ok
const parentService2 = childInjector.get(ParentService, undefined, {
self: true,
}); // 报错</code></pre>
<p>由于声明了 self: true,childInjector 只能在自己的作用域查找 Provider,所以无法获取到 ParentService 也就报错了。</p>
<p>同理 skipSelf 就是不找自己的作用域,只找祖先的 Injector。</p>
<pre class="language-javascript highlighter-hljs"><code>const childService = childInjector.get(ChildService, undefined, {
skipSelf: true,
}); // 报错</code></pre>
<p>&nbsp;</p>
<h2>@Injectable() and InjectionToken as Provider</h2>
<p>特别声明:</p>
<blockquote>
<p>下面例子中会用到&nbsp;providedIn: 'any',这个东西已经在 Angular v15 废弃了,不过目前 v20 依然能用。</p>
<p>相关资讯:&nbsp;Docs –&nbsp;Deprecated APIs and features</p>
<p>本篇为了不涉及其它概念又想体现 "@Injectable() and InjectionToken as Provider“,所以才用了这个&nbsp;providedIn: 'any'。</p>
<p>虽然 providedIn: 'any' 是废弃了,但是 providedIn 没有废弃,比如 providedIn: 'root' | 'platform' 都还在。</p>
<p>所以你依然可以照着它的概念去理解。</p>
</blockquote>
<p>上面所有例子中,我们提供 providers 给 Injector 的方式,对代码管理和&nbsp;tree shaking&nbsp;是扣分的。</p>
<pre class="language-javascript highlighter-hljs"><code>// 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: ,
});

// app.ts
import { injector } from './injector.ts';
import { ServiceB } from './service-b.ts';

injector.get(ServiceB);</code></pre>
<p>第一,我们必须把所有可能会用到的 Provider 通通 pass 给 Injector,这个很麻烦丫,不小心漏掉一个怎么办,代码管理扣分。</p>
<p>第二,我在 app.ts 只用到了 ServiceB,而且 ServiceB 本身并不依赖 ServiceA,所以整个项目 ServiceA 是应该被 shaking 掉了,</p>
<p>但由于 injector.ts 需要 import 所有可能被用到的 Provider,导致无论如何 ServiceA 都不会被 shaking 掉。tree shaking 扣分。</p>
<p>为了解决上述的问题,Angular 搞了另一种提供 Provider 给 Injector 的方式。</p>
<h3>@Injectable() as Provider</h3>
<p>下面这样会报错</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceA {}

const injector = Injector.create({
providers: [],
});

console.log(injector.get(ServiceA)); // Error: No provider for ServiceA!</code></pre>
<p>因为 providers 是空的。</p>
<p>我们加上 @Injectable</p>
<pre class="language-javascript highlighter-hljs"><code>@Injectable({
providedIn: 'any',
})
class ServiceA {}

const injector = Injector.create({
providers: [],
});

console.log(injector.get(ServiceA)); // OK</code></pre>
<p>这样就不会报错了。</p>
<p>上面我们有提到过,@Injectable 除了可以配合 Angular&nbsp;Compilation 搞黑魔法,它的另一个主要作用便是 as Provider。</p>
<p>为什么 providedIn: 'any' 后,injector 不需要 providers 也可以 get 到 ServiceA 呢?</p>
<h4>Injector 源码逛一逛</h4>
<p>Injector.create 方法的源码在&nbsp;injector.ts。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250525151758047-1280519826.png"></p>
<p>看注释理解。</p>
<p>createInjector 函数源码在&nbsp;create_injector.ts</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250525151950865-929267775.png"></p>
<p>createInjectorWithoutInjectorInstances 函数</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250525152133740-1936784302.png"></p>
<p>关键就是创建了 R3Injector 对象。</p>
<p>class R3Injector 的源码在&nbsp;r3_injector.ts</p>
<p>R3Injector 继承自 EnvironmentInjector</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250525152341286-1083440911.png"></p>
<p>EnvironmentInjector 实现了抽象的 Injector 接口,我们平时用的就是 Injector 接口。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250525152422189-1519366931.png"></p>
<p>R3Injector 内有两个很重要的属性</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250525153314534-451110581.png"></p>
<p>一个是 records,它是一个 Map 用来装所有的 providers。</p>
<p>另一个是 parent injector。</p>
<p>有了这两个属性,Injector.get 就可以查找 providers 和祖先 providers 了。</p>
<p>那如果 records 里找不到 Provider 呢?</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250525153634179-1350772324.png"></p>
<p>这个 injectable definition 指的是,经过 compile + @Injectable() 后 ServiceA 的 ɵprov static property。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250525153829831-869668783.png"></p>
<p>当 records 找不到,Injector 会去拿 ServiceA.ɵprov injectable definition。</p>
<p>然后检查它的 scope</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250525153921137-1086477266.png"></p>
<p>providedIn: 'any' 表示 in scope(注: 除了 any 其实还可以放其它值,比如 'root' 和 'platform',这个以后章节会教)</p>
<p>接着调 factory 出来执行就拿到 provider value 了。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250525154009989-726085263.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250525154124666-9562647.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250525154209905-845869238.png"></p>
<p>总结,injector 有 2 种方式可以找到 provider,第一个是去 records 找 provider,另一个是去 definition 找 provider。</p>
<h3>InjectionToken as Provider</h3>
<pre class="language-javascript highlighter-hljs"><code>@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'</code></pre>
<p>和 @Injectable 同样原理,只是 @Injectable 用于 class,InjectionToken 用于其它类型。</p>
<p>&nbsp;</p>
<h2>Destroy Injector</h2>
<p>Injector 是有生命周期的,我们可以随时 destroy 掉它。</p>
<pre class="language-javascript highlighter-hljs"><code>class ServiceA {}
const injector = Injector.create({ providers: });

injector.destroy(); // destroy injector
const serviceA = injector.get(ServiceA); // get ServiceA after destroy</code></pre>
<p>效果</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202506/641294-20250609154312593-1343972246.png"></p>
<p>injector destroyed&nbsp;之后就不能再使用了。</p>
<h3>DestroyRef</h3>
<p>我们可以透过 DestroyRef 对象来监听 Injector destroy。</p>
<pre class="language-javascript highlighter-hljs"><code>const destroyRef = injector.get(DestroyRef);
const removeListener = destroyRef.onDestroy(() =&gt; console.log('injector destroyed')); // 当 Injector.destroy() 时会触发这个 callback

// removeListener(); 如果想移除监听,可以调用这个返回函数。</code></pre>
<p>你可能会好奇,DestroyRef 是 Provider 吗?为什么我们可以 injector.get 到它。</p>
<p>相关源码在&nbsp;r3_injector.ts</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202506/641294-20250609160046061-504952644.png"></p>
<p>R3Injector 在 get 的时候,第一步会做一个 hasOwnProperty 检查。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202506/641294-20250609160415142-1404894168.png"></p>
<p>如果有相关 property 的话 (它应该要是一个方法),就会调用它,并且把 injector 传进去。</p>
<p>DestroyRef 的这个方法会把传入的 injector 直接返回出去。</p>
<p>也就是说</p>
<pre class="language-javascript highlighter-hljs"><code>console.log(injector === destroyRef as any); // true</code></pre>
<p>destroyRef 其实就是 injector 来的。</p>
<p>所以我们也可以这样子写</p>
<pre class="language-javascript highlighter-hljs"><code>const injector = Injector.create({ providers: }) as ɵR3Injector;
const removeListener = injector.onDestroy(() =&gt; console.log('injector destroyed'));</code></pre>
<p>效果一模一样,只是需要强转类型去 ɵR3Injector 而已 (注:建议还是使用 DestroyRef 会顺风水一点)。</p>
<p>&nbsp;</p>
<h2>真实项目中 DI 的使用方式&nbsp;</h2>
<p>上面例子中,我们都是自己创建 Injector,但其实,在真实项目中,Angular 会替我们创建 Injector,我们不会用到 Injector.create 这个方法。</p>
<p>首先创建一个项目</p>
<div class="cnblogs_code">
<pre>ng new di --routing=<span style="color: rgba(0, 0, 255, 1)">false</span> --style=scss --skip-tests --ssr=<span style="color: rgba(0, 0, 255, 1)">false</span> --zoneless</pre>
</div>
<p>做一个 service-a.ts</p>
<pre class="language-javascript highlighter-hljs"><code>export class ServiceA {}</code></pre>
<p>在 Angular 项目中,我们的代码入口是组件,而组件本身又是 class,所以它天生就可以注入。</p>
<pre class="language-javascript highlighter-hljs"><code>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);
}
}</code></pre>
<p>我们直接在 App 组件 constructor 里透过 inject 函数注入 ServiceA。</p>
<p>结果报错了。因为呢,虽然 Angular 替我们创建了 Injector,但是 DI 还需要 Provider,我们还没有把 ServiceA 提供给 Injector,所以它找不到。</p>
<p>到 app.config.ts</p>
<pre class="language-javascript highlighter-hljs"><code>import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import { ServiceA } from './service-a';

export const appConfig: ApplicationConfig = {
providers: [
    ServiceA, // 1. 添加 Provider
    provideBrowserGlobalErrorListeners(),
    provideZonelessChangeDetection(),
]
};</code></pre>
<p>appConfig.providers 就是提供给 Injector 的 providers。</p>
<p>main.ts</p>
<pre class="language-javascript highlighter-hljs"><code>import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';

bootstrapApplication(App, appConfig)
.catch((err) =&gt; console.error(err));</code></pre>
<p>Angular 在 bootstrap application 的时候会把 App 组件和 appConfig 串联上。</p>
<p>于是 App 组件就可以 inject 到 appConfig 的 providers 了。</p>
<p>当然我们也可以用 @Injectable providedIn 的方式提供 Provider 给 Injector。</p>
<pre class="language-javascript highlighter-hljs"><code>import { Injectable } from "@angular/core";

@Injectable({
providedIn: 'root', // 'any' 废弃了,通常是用 'root'
})
export class ServiceA {}</code></pre>
<p>这样就可以不需要写在 appConfig 了。&nbsp;</p>
<p>&nbsp;</p>
<h2>总结</h2>
<p>Angular 的 DI 和其它后端 (e.g. ASP.NET Core) 的 DI 大同小异,几个小小区别:</p>
<ol>
<li>
<p>not only for class,任何类型都可以使用 DI。</p>
</li>
<li>
<p>单列模式</p>
</li>
<li>
<p>原型链</p>
</li>
</ol>
<p>目前为止,我们大概学习了 50% 关于 Angular DI 的知识,另外 50% 会在后面的章节教。</p>
<p>为什么不一起教?因为太复杂了。当 DI 配上组件会有另一番天地,我们必须先对组件有基础的了解才能把 DI 加进去。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<h2>目录</h2>
<p>上一篇&nbsp;Angular 20+ 高阶教程 – Angular Compiler (AKA ngc) Quick View</p>
<p>下一篇&nbsp;Angular 20+ 高阶教程 – Component 组件 の Angular Component vs Web Component</p>
<p>想查看目录,请移步&nbsp;Angular 20+ 高阶教程 – 目录</p>
<p>喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding&nbsp;😊💻</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div><br><br>
来源:https://www.cnblogs.com/keatkeat/p/16953629.html
頁: [1]
查看完整版本: Angular 20+ 高阶教程 – Dependency Injection 依赖注入