如何用最简单的方式解释依赖注入?依赖注入是如何实现解耦的?
依赖注入实例系列
核心:所要依赖的项变化时,不用在constructor的地方改,直接在provider改就行了。
——即组件不用关心引用哪个服务,由提供商控制。(控制反转)

文档:多级注入器中,模板的逻辑结构,在@Component中提供服务和修改服务的可见性不是重点。

依赖注入的三种方式

Angular 中有两个注入器层次结构:

  1. ModuleInjector 层次结构 —— 使用 @NgModule() 或 @Injectable() 注解在此层次结构中配置 ModuleInjector。2. > ElementInjector 层次结构 —— 在每个 DOM 元素上隐式创建。除非您在 @Directive() 或 @Component() 的 providers 属性中进行配置,否则默认情况下,ElementInjector 为空。

  1. 在服务本身的 @[Injectable](https://angular.cn/api/core/Injectable)() 装饰器中。
  2. 在 NgModule 的 @[NgModule](https://angular.cn/api/core/NgModule)() 装饰器中。

NgModule 级的提供商可以在 @NgModule() providers 元数据中指定,也可以在 @Injectable() 的 providedIn 选项中指定某个模块类(主要的区别是如果 NgModule 没有用到该服务,那么它就是可以被摇树优化掉的)

  1. 在组件的 @[Component](https://angular.cn/api/core/Component)() 装饰器中。

组件有属于自己的实例。例如每个服务存取自己的数据。

依赖注入和其它方式的区别

  • 依赖注入和new的区别?
  • [ ] 在模板中直接绑定服务?和vuex的区别?

    依赖注入方式和new方式的区别

  • 如果需要通过传入参数并new一个类的方式,就不使用injectable,不作为依赖注入,直接在组件的constructor中new,而不是用private xxx依赖注入方式。

包括单元测试中也是,就不用TestBed了。

  • 牺牲依赖注入,可用构造函数传参
  • 或者:仍用依赖注入方式,给一个初始化的init方法初始成员变量

参考:Angular2 EXCEPTION No provider for String
单元测试中:

  1. // 使用new,不用testBed
  2. beforeEach(() => {
  3. assistant = new TableCheckboxAssistant(items, checkkey);
  4. });

为什么要依赖注入

  1. 组件只管注入依赖,不在组件中改动。
  2. 注册什么样的依赖,由注入器决定,例如是生产环境还是开发环境、http请求还是本地请求,这样避免在组件中改动。
  3. 为什么有不同的依赖?环境不同、同一方法在不同组件有不同实现方式,不可能在组件里注入多个依赖。
  4. 如果用new()的方式,无法区分用哪个依赖。
  • 通过传入成员变量参数?就需要在组件中改动。
  • 在服务内部区分?根据环境判断的尚能区分,如果是与组件选用相关的,无法区分。
  1. 单元测试的方便,使用本地请求的依赖。
  1. // providers
  2. export interface HeroService {
  3. getHeroById(id: number): Hero;
  4. }
  5. export class HeroServiceHttp implements HeroService {
  6. getHeroById(id: number): Hero {
  7. return this.http.get('xxxx', {id});
  8. }
  9. }
  10. export class HeroServiceLocal implements HeroService {
  11. getHeroById(id: number): Hero {
  12. return new Hero();
  13. }
  14. }
  15. providers: [
  16. {
  17. provide: HeroService,
  18. useClass: environment.production ? HeroServiceHttp : HeroServiceLocal
  19. }
  20. ]
  1. // 单元测试中的用法
  2. beforeEach(async(() => {
  3. TestBed.configureTestingModule({
  4. imports: [
  5. RouterTestingModule
  6. ],
  7. providers: [
  8. {
  9. provide: HeroService,
  10. useClass: HeroServiceLocal
  11. }
  12. ],
  13. declarations: [
  14. AppComponent
  15. ],
  16. }).compileComponents();
  17. }));

你创建了一个 MessageService,以便在类之间实现松耦合通讯 服务:从服务器获取数据、验证用户输入(?)或直接把日志写到控制台

依赖注入

1. 注入

1. 创建阶段

依赖

是当类需要执行其功能时,所需要的服务或对象 。DI 是一种编码模式,其中的 会从 外部源 中请求获取 依赖 ,而不是自己创建它们。
在 Angular 中,DI 框架会在实例化该类时向其提供这个类所声明的依赖项

服务 hero.service.ts

hero.service.ts:the new service imports the Angular [_Injectable_](https://angular.cn/api/core/Injectable) symbol

服务中的类 HeroService

heroService:The _HeroService_ class is going to provide an injectable service

提供商 HeroService自身

provider: A provider is something that can create or deliver a service; in this case, it instantiates the _HeroService_ class to provide the service.
the _HeroService_ is registered as the provider of this service.
把服务类注册为该服务的提供商(provider),也就是说heroService被注册成为提供商。(对于服务,该提供商通常就是服务类本身。

注入器 [HeroService提供商,ServiceA提供商,ServiceB提供商、heroService实例, serviceA实例, serviceb实例…]

提供商 配置。

  • 该注入器负责创建 服务实例 ,负责在需要时选取提供商,并把它们注入到像 HeroListComponent 这样的类中。
  • 默认情况下,Angular CLI 命令 ng generate service 会通过给 @Injectable 装饰器添加元数据的形式,用根注入器将你的服务注册成为提供商。(一般不用全局单例root的方式)
  • Angular 会在启动过程中为你创建全应用级注入器以及所需的其它注入器。你不用自己创建注入器。
  1. 当 Angular 创建组件类的新实例时,它会通过查看该组件类的构造函数,来决定该组件依赖哪些服务或其它依赖项。 比如HeroListComponent 的构造函数中需要 HeroService
  2. 当 Angular 发现某个组件依赖某个服务时,它会首先检查是否该注入器中已经有了那个服务的任何现有实例。如果所请求的服务尚不存在,注入器就会使用以前注册的服务提供商来制作一个,并把它加入注入器中,然后把该服务返回给 Angular。

【类比:如果没有实例,new一个,并放在服务类(提供商)的静态属性上;如果静态属性上有,直接使用服务 类(提供商)的静态属性上的。】

  1. 当所有请求的服务已解析并返回时,Angular 可以用这些服务实例为参数,调用该组件的构造函数。

【类比:执行constructor,this.heroService = 注入器中的heroService】
服务和依赖注入 - 图1

根注入器

用根注入器将你的服务注册成为提供商。
Registering the provider in the @Injectable metadata also allows Angular to optimize an app by removing the service if it turns out not to be used after all.

  1. @Injectable({
  2. providedIn: 'root', // 元数据的值是root
  3. })

mock数据的服务 :这种方法在原型阶段有用,但是不够健壮、不利于维护。 一旦你想要测试该组件或想从远程服务器获得英雄列表,就不得不修改 HeroesListComponent 的实现,并且替换每一处使用了 HEROES 模拟数据的地方。

  1. import { HEROES } from './mock-heroes';
  2. export class HeroListComponent {
  3. heroes = HEROES;
  4. }

2. 提供服务 Inject

  1. 在 @Injectable() 装饰器中提供元数据来把它注册到根注入器,Angular 会为 HeroService 创建一个单一的共享实例

    1. @Injectable({
    2. providedIn: 'root',
    3. })
  2. 使用特定的 NgModule 注册提供商时,该服务的同一个实例将会对该 NgModule 中的所有组件可用

    如果在 AppModule@[NgModule](https://angular.cn/api/core/NgModule)() 中配置应用级提供者,就会覆盖一个在 @[Injectable](https://angular.cn/api/core/Injectable)()root 元数据中配置的提供者

  1. @NgModule({
  2. providers: [
  3. BackendService,
  4. Logger
  5. ],
  6. ...
  7. })
  1. 组件中注册提供商,为该组件的每一个新实例提供该服务的一个新实例
    1. @Component({
    2. selector: 'app-hero-list',
    3. templateUrl: './hero-list.component.html',
    4. providers: [ HeroService ]
    5. })

3.创建可摇树优化的提供商

只要在服务本身的 @[Injectable](https://angular.cn/api/core/Injectable)() 装饰器中指定,而不是在依赖该服务的 NgModule 或组件的元数据中指定,你就可以制作一个可摇树优化的提供商

  1. import { Injectable } from '@angular/core';
  2. import { UserModule } from './user.module';
  3. @Injectable({
  4. providedIn: UserModule,
  5. })
  6. export class UserService {
  7. }

上面的例子展示的就是在模块中提供服务的首选方式。之所以推荐该方式,是因为当没有人注入它时,该服务就可以被摇树优化掉。如果没办法指定哪个模块该提供这个服务,你也可以在那个模块中为该服务声明一个提供商。(@NgModule中providers)
可摇树优化的提供者

4. 组件中注入

Angular 把 HeroService 注入到 HeroesComponent。

  1. constructor(private heroService: HeroService) { }

这个参数同时做了两件事:1. 声明了一个私有 heroService 属性,2. 把它标记为一个 HeroService 的注入点。
当 Angular 创建 HeroesComponent 时,依赖注入系统就会把这个 heroService 参数设置为 HeroService 的单例对象。

5. 测试时的注入

本来要创建一个http的spy,但因为这是个依赖注入的实例,所以用jasmine.createSpyObj创建一个空的

  1. beforeEach(() => {
  2. // TODO: spy on other methods too
  3. httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
  4. heroService = new HeroService(<any> httpClientSpy);
  5. 相当于:this.http = httpClientSpy,拥有了get方法
  6. });

2. 多级注入

解析规则

  • 如果您已在不同级别注册了相同 DI 令牌的提供者,则 Angular 会用遇到的第一个来解析该依赖
  • @[Directive](https://angular.cn/api/core/Directive)() 具有 providers 属性,@[Component](https://angular.cn/api/core/Component)() 也同样如此。 这意味着指令和组件都可以使用 providers 属性来配置提供者

    viewProviders和providers的区别

    与内容投影有关。
    非重点。定义viewProviders依赖的子组件才能搜寻到这个依赖,投影搜寻不到,会继续向上寻找。

    服务隔离——NgModule中注入

    如果你在根模块 AppModule 中(也就是你注册 HeroesService 的地方)提供 VillainsService,就会让应用中的任何地方都能访问到 VillainsService,包括针对英雄的工作流。如果你稍后修改了 VillainsService,就可能破坏了英雄组件中的某些地方。在根模块 AppModule 中提供该服务将会引入此风险。
  1. @Component({
  2. selector: 'app-villains-list',
  3. templateUrl: './villains-list.component.html',
  4. providers: [ VillainsService ]
  5. })
  1. import { Injectable } from '@angular/core';
  2. import { HeroModule } from './hero.module';
  3. import { HEROES } from './mock-heroes';
  4. @Injectable({
  5. // we declare that this service should be created
  6. // by any injector that includes HeroModule.
  7. providedIn: HeroModule,
  8. })
  9. export class HeroService {
  10. getHeroes() { return HEROES; }
  11. }

多重编辑会话——组件中注入

但如果该服务是一个全应用范围的单例就不行了。 每个组件就都会共享同一个服务实例,每个组件也都会覆盖属于其他英雄的报税单。
要防止这一点,我们就要在 HeroTaxReturnComponent 元数据的 providers 属性中配置组件级的注入器,来提供该服务。
src/app/hero-tax-return.component.ts (providers)

  1. providers: [ HeroTaxReturnService ]

服务

可观察(Observable)的数据

ObservableRxJS 库中的一个关键类。
使用 RxJS 的 of() 函数来模拟从服务器返回数据。

订阅(subscribe)

subscribe 函数把这个英雄数组传给这个回调函数,类似于promise的then
组件与 heroService.delete() 返回的 Observable 还完全没有关联。 必须订阅它 ,例如删除服务,即使不跟上回调,也要加一个subscribe()

服务中的服务

重新打开 HeroService,并且导入 MessageService

模板中绑定服务

Angular 只会绑定到组件的公共属性
这个 messageService 属性必须是公共属性,因为你将会在模板中绑定到它。

  1. export class MessagesComponents implements OnInit {
  2. constructor(public messageService: MessageService) {}
  3. }

修饰符

解析修饰符分为三类:

  1. 如果 Angular 找不到您要的东西该怎么办,用 @[Optional](https://angular.cn/api/core/Optional)()
  2. 从哪里开始寻找,用 @[SkipSelf](https://angular.cn/api/core/SkipSelf)()
  3. 到哪里停止寻找,用 @[Host](https://angular.cn/api/core/Host)()@[Self](https://angular.cn/api/core/Self)()

    @Optional

    如果没找到,logger就为null
    1. constructor(@Optional() private logger: Logger) {
    2. if (this.logger) {
    3. this.logger.log(some_message);
    4. }
    5. }

    @self

    @[Self](https://angular.cn/api/core/Self)() 的一个好例子是要注入某个服务,但只有当该服务在当前宿主元素上可用时才行。为了避免这种情况下出错,请将 @Self() 与 @Optional() 结合使用。
    1. @Component({
    2. selector: 'app-self-no-data',
    3. templateUrl: './self-no-data.component.html',
    4. styleUrls: ['./self-no-data.component.css']
    5. })
    6. export class SelfNoDataComponent {
    7. constructor(@Self() @Optional() public leaf: LeafService) { }
    8. }

    @skipSelf

    与self同理

    @host

    和@self的区别是什么

    @[Host](https://angular.cn/api/core/Host) 属性装饰器会禁止在宿主组件以上的搜索。宿主组件通常就是请求该依赖的那个组件。 不过,当该组件投影进某个父组件时,那个父组件就会变成宿主。

使用@Inject指定自定义提供者

自定义提供者让你可以为隐式依赖提供一个具体的实现,比如内置浏览器 API
InjectionToken

  1. import { Inject, Injectable, InjectionToken } from '@angular/core';
  2. export const BROWSER_STORAGE = new InjectionToken<Storage>('Browser Storage', {
  3. providedIn: 'root',
  4. factory: () => localStorage
  5. });
  6. // 或者用provde - useValue,例如API:localhost
  7. @Injectable({
  8. providedIn: 'root'
  9. })
  10. export class BrowserStorageService {
  11. constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {}
  12. get(key: string) {
  13. this.storage.getItem(key);
  14. }
  15. }

DI提供者

useClass方式

注意:provide必须是内部真正写的import的service,useClass是依赖注入选择注入的

  1. providers: [
  2. {
  3. provide: FieldRulesService,
  4. useClass: FieldRulesService // TODO: 标准规则引擎
  5. }

useExisting

useExisting和useClass的区别: 可以看demo,较好解释

useValue

非类依赖

并非所有的依赖都是类。 有时候你会希望注入字符串、函数或对象。
TypeScript 接口interface不是有效的令牌

  1. // app.module.ts
  2. providers: [
  3. UserService,
  4. { provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
  5. ]

使用InjectionToken

  1. // app.config.ts
  2. import { InjectionToken } from '@angular/core';
  3. export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
  4. // 注入的对象
  5. providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]
  6. // app.component.ts
  7. constructor(@Inject(APP_CONFIG) config: AppConfig) {
  8. this.title = config.title;
  9. }

Factory工厂提供者

动态的提供者。

(所有提供者最终形态都是工厂,一般用于token等基本类型的注入)

  1. // 函数中直接new的方式创建,注入依赖或直接传基本类型
  2. // Logger, UserService是另外需要依赖的令牌
  3. let heroServiceFactory = (logger: Logger, userService: UserService) => {
  4. return new HeroService(logger, userService.user.isAuthorized);
  5. };
  6. export let heroServiceProvider =
  7. { provide: HeroService,
  8. useFactory: heroServiceFactory,
  9. deps: [Logger, UserService]
  10. };
  11. import { Component } from '@angular/core';
  12. import { heroServiceProvider } from './hero.service.provider';
  13. @Component({
  14. selector: 'app-heroes',
  15. providers: [ heroServiceProvider ],
  16. template: `
  17. <h2>Heroes</h2>
  18. <app-hero-list></app-hero-list>
  19. `
  20. })
  21. export class HeroesComponent { }

怎样根据@Input输入属性动态注入)

  1. import { StandardCategoryService } from 'src/app/routes/standard-data/standard-category/domain/services/standard-category.service';
  2. import { SystemCategoryService } from 'src/app/routes/shared/services/system-category.service';
  3. import { HttpMessenger } from 'src/app/routes/shared/services/http-messenger';
  4. // 拟根据输入属性@Input() standardCId动态注入,但是此时得不到
  5. const createSystemCategoryServiceFactory = (standardCId) => {
  6. if (standardCId) {
  7. return (httpMessenger: HttpMessenger) => {
  8. return new StandardCategoryService(httpMessenger);
  9. };
  10. } else {
  11. return (httpMessenger: HttpMessenger) => {
  12. return new SystemCategoryService(httpMessenger);
  13. };
  14. }
  15. };
  16. export const getSystemCategoryServiceProvider = (standardCId) => {
  17. console.log(standardCId);
  18. return {
  19. provide: SystemCategoryService,
  20. useFactory: createSystemCategoryServiceFactory(standardCId),
  21. deps: [HttpMessenger]
  22. };
  23. };

创建多例

Using multiple instances of the same service
Create new instance of class that has dependencies, not understanding factory provider

injector.get

  1. ngOnInit() {
  2. if (this.standardCId) {
  3. this.systemCategoryService = this.injector.get<SystemCategoryService | StandardCategoryService>(StandardCategoryService);
  4. } else {
  5. this.systemCategoryService = this.injector.get<SystemCategoryService | StandardCategoryService>(SystemCategoryService);
  6. }
  7. this.getTreeNodes();
  8. }

类-接口(DI实战) 抽象类不用作继承,只用作依赖注入的令牌

LoggerServiceDateLoggerService本可以从 MinimalLogger 中继承。 它们也可以实现 MinimalLogger,而不用单独定义接口。 但它们没有。 MinimalLogger 在这里仅仅被用作一个 “依赖注入令牌”

  1. // Class used as a "narrowing" interface that exposes a minimal logger
  2. // Other members of the actual implementation are invisible
  3. export abstract class MinimalLogger {
  4. logs: string[];
  5. logInfo: (msg: string) => void;
  6. }
  1. export class xxx {
  2. prorivders: [
  3. { provide: MinimalLogger, useExisting: LoggerService }
  4. ]
  5. }