如何用最简单的方式解释依赖注入?依赖注入是如何实现解耦的?
依赖注入实例系列
核心:所要依赖的项变化时,不用在constructor的地方改,直接在provider改就行了。
——即组件不用关心引用哪个服务,由提供商控制。(控制反转)
文档:多级注入器中,模板的逻辑结构,在@Component中提供服务和修改服务的可见性不是重点。
依赖注入的三种方式
Angular 中有两个注入器层次结构:
ModuleInjector 层次结构 —— 使用 @NgModule() 或 @Injectable() 注解在此层次结构中配置 ModuleInjector。2. > ElementInjector 层次结构 —— 在每个 DOM 元素上隐式创建。除非您在 @Directive() 或 @Component() 的 providers 属性中进行配置,否则默认情况下,ElementInjector 为空。
- 在服务本身的
@[Injectable](https://angular.cn/api/core/Injectable)()装饰器中。 - 在 NgModule 的
@[NgModule](https://angular.cn/api/core/NgModule)()装饰器中。
NgModule 级的提供商可以在 @NgModule() providers 元数据中指定,也可以在 @Injectable() 的 providedIn 选项中指定某个模块类(主要的区别是如果 NgModule 没有用到该服务,那么它就是可以被摇树优化掉的)
- 在组件的
@[Component](https://angular.cn/api/core/Component)()装饰器中。
依赖注入和其它方式的区别
- 依赖注入和new的区别?
-
依赖注入方式和new方式的区别
如果需要通过传入参数并new一个类的方式,就不使用injectable,不作为依赖注入,直接在组件的constructor中new,而不是用private xxx依赖注入方式。
包括单元测试中也是,就不用TestBed了。
- 牺牲依赖注入,可用构造函数传参
- 或者:仍用依赖注入方式,给一个初始化的init方法初始成员变量
参考:Angular2 EXCEPTION No provider for String
单元测试中:
// 使用new,不用testBedbeforeEach(() => {assistant = new TableCheckboxAssistant(items, checkkey);});
为什么要依赖注入
- 组件只管注入依赖,不在组件中改动。
- 注册什么样的依赖,由注入器决定,例如是生产环境还是开发环境、http请求还是本地请求,这样避免在组件中改动。
- 为什么有不同的依赖?环境不同、同一方法在不同组件有不同实现方式,不可能在组件里注入多个依赖。
- 如果用new()的方式,无法区分用哪个依赖。
- 通过传入成员变量参数?就需要在组件中改动。
- 在服务内部区分?根据环境判断的尚能区分,如果是与组件选用相关的,无法区分。
- 单元测试的方便,使用本地请求的依赖。
// providersexport interface HeroService {getHeroById(id: number): Hero;}export class HeroServiceHttp implements HeroService {getHeroById(id: number): Hero {return this.http.get('xxxx', {id});}}export class HeroServiceLocal implements HeroService {getHeroById(id: number): Hero {return new Hero();}}providers: [{provide: HeroService,useClass: environment.production ? HeroServiceHttp : HeroServiceLocal}]
// 单元测试中的用法beforeEach(async(() => {TestBed.configureTestingModule({imports: [RouterTestingModule],providers: [{provide: HeroService,useClass: HeroServiceLocal}],declarations: [AppComponent],}).compileComponents();}));
你创建了一个 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 会在启动过程中为你创建全应用级注入器以及所需的其它注入器。你不用自己创建注入器。
- 当 Angular 创建组件类的新实例时,它会通过查看该组件类的构造函数,来决定该组件依赖哪些服务或其它依赖项。 比如
HeroListComponent的构造函数中需要HeroService: - 当 Angular 发现某个组件依赖某个服务时,它会首先检查是否该注入器中已经有了那个服务的任何现有实例。如果所请求的服务尚不存在,注入器就会使用以前注册的服务提供商来制作一个,并把它加入注入器中,然后把该服务返回给 Angular。
【类比:如果没有实例,new一个,并放在服务类(提供商)的静态属性上;如果静态属性上有,直接使用服务 类(提供商)的静态属性上的。】
- 当所有请求的服务已解析并返回时,Angular 可以用这些服务实例为参数,调用该组件的构造函数。
【类比:执行constructor,this.heroService = 注入器中的heroService】
根注入器
用根注入器将你的服务注册成为提供商。
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.
@Injectable({providedIn: 'root', // 元数据的值是root})
mock数据的服务 :这种方法在原型阶段有用,但是不够健壮、不利于维护。 一旦你想要测试该组件或想从远程服务器获得英雄列表,就不得不修改 HeroesListComponent 的实现,并且替换每一处使用了 HEROES 模拟数据的地方。
import { HEROES } from './mock-heroes';export class HeroListComponent {heroes = HEROES;}
2. 提供服务 Inject
在 @Injectable() 装饰器中提供元数据来把它注册到根注入器,Angular 会为 HeroService 创建一个单一的共享实例
@Injectable({providedIn: 'root',})
使用特定的 NgModule 注册提供商时,该服务的同一个实例将会对该 NgModule 中的所有组件可用
如果在
AppModule的@[NgModule](https://angular.cn/api/core/NgModule)()中配置应用级提供者,就会覆盖一个在@[Injectable](https://angular.cn/api/core/Injectable)()的root元数据中配置的提供者
@NgModule({providers: [BackendService,Logger],...})
- 组件中注册提供商,为该组件的每一个新实例提供该服务的一个新实例
@Component({selector: 'app-hero-list',templateUrl: './hero-list.component.html',providers: [ HeroService ]})
3.创建可摇树优化的提供商
只要在服务本身的 @[Injectable](https://angular.cn/api/core/Injectable)() 装饰器中指定,而不是在依赖该服务的 NgModule 或组件的元数据中指定,你就可以制作一个可摇树优化的提供商
import { Injectable } from '@angular/core';import { UserModule } from './user.module';@Injectable({providedIn: UserModule,})export class UserService {}
上面的例子展示的就是在模块中提供服务的首选方式。之所以推荐该方式,是因为当没有人注入它时,该服务就可以被摇树优化掉。如果没办法指定哪个模块该提供这个服务,你也可以在那个模块中为该服务声明一个提供商。(@NgModule中providers)
可摇树优化的提供者
4. 组件中注入
Angular 把 HeroService 注入到 HeroesComponent。
constructor(private heroService: HeroService) { }
这个参数同时做了两件事:1. 声明了一个私有 heroService 属性,2. 把它标记为一个 HeroService 的注入点。
当 Angular 创建 HeroesComponent 时,依赖注入系统就会把这个 heroService 参数设置为 HeroService 的单例对象。
5. 测试时的注入
本来要创建一个http的spy,但因为这是个依赖注入的实例,所以用jasmine.createSpyObj创建一个空的
beforeEach(() => {// TODO: spy on other methods toohttpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);heroService = new HeroService(<any> httpClientSpy);相当于:this.http = httpClientSpy,拥有了get方法});
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 中提供该服务将会引入此风险。
@Component({selector: 'app-villains-list',templateUrl: './villains-list.component.html',providers: [ VillainsService ]})
import { Injectable } from '@angular/core';import { HeroModule } from './hero.module';import { HEROES } from './mock-heroes';@Injectable({// we declare that this service should be created// by any injector that includes HeroModule.providedIn: HeroModule,})export class HeroService {getHeroes() { return HEROES; }}
多重编辑会话——组件中注入
但如果该服务是一个全应用范围的单例就不行了。 每个组件就都会共享同一个服务实例,每个组件也都会覆盖属于其他英雄的报税单。
要防止这一点,我们就要在 HeroTaxReturnComponent 元数据的 providers 属性中配置组件级的注入器,来提供该服务。src/app/hero-tax-return.component.ts (providers)
providers: [ HeroTaxReturnService ]
服务
可观察(Observable)的数据
Observable 是 RxJS 库中的一个关键类。
使用 RxJS 的 of() 函数来模拟从服务器返回数据。
订阅(subscribe)
subscribe 函数把这个英雄数组传给这个回调函数,类似于promise的then
组件与 heroService.delete() 返回的 Observable 还完全没有关联。 必须订阅它 ,例如删除服务,即使不跟上回调,也要加一个subscribe()
服务中的服务
重新打开 HeroService,并且导入 MessageService。
模板中绑定服务
Angular 只会绑定到组件的公共属性
这个 messageService 属性必须是公共属性,因为你将会在模板中绑定到它。
export class MessagesComponents implements OnInit {constructor(public messageService: MessageService) {}}
修饰符
解析修饰符分为三类:
- 如果 Angular 找不到您要的东西该怎么办,用
@[Optional](https://angular.cn/api/core/Optional)() - 从哪里开始寻找,用
@[SkipSelf](https://angular.cn/api/core/SkipSelf)() - 到哪里停止寻找,用
@[Host](https://angular.cn/api/core/Host)()和@[Self](https://angular.cn/api/core/Self)()@Optional
如果没找到,logger就为nullconstructor(@Optional() private logger: Logger) {if (this.logger) {this.logger.log(some_message);}}
@self
@[Self](https://angular.cn/api/core/Self)()的一个好例子是要注入某个服务,但只有当该服务在当前宿主元素上可用时才行。为了避免这种情况下出错,请将 @Self() 与 @Optional() 结合使用。@Component({selector: 'app-self-no-data',templateUrl: './self-no-data.component.html',styleUrls: ['./self-no-data.component.css']})export class SelfNoDataComponent {constructor(@Self() @Optional() public leaf: LeafService) { }}
@skipSelf
与self同理@host
和@self的区别是什么@[Host](https://angular.cn/api/core/Host)属性装饰器会禁止在宿主组件以上的搜索。宿主组件通常就是请求该依赖的那个组件。 不过,当该组件投影进某个父组件时,那个父组件就会变成宿主。
使用@Inject指定自定义提供者
自定义提供者让你可以为隐式依赖提供一个具体的实现,比如内置浏览器 API
InjectionToken
import { Inject, Injectable, InjectionToken } from '@angular/core';export const BROWSER_STORAGE = new InjectionToken<Storage>('Browser Storage', {providedIn: 'root',factory: () => localStorage});// 或者用provde - useValue,例如API:localhost@Injectable({providedIn: 'root'})export class BrowserStorageService {constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {}get(key: string) {this.storage.getItem(key);}}
DI提供者
useClass方式
注意:provide必须是内部真正写的import的service,useClass是依赖注入选择注入的
providers: [{provide: FieldRulesService,useClass: FieldRulesService // TODO: 标准规则引擎}
useExisting
useExisting和useClass的区别: 可以看demo,较好解释
useValue
非类依赖
并非所有的依赖都是类。 有时候你会希望注入字符串、函数或对象。
TypeScript 接口interface不是有效的令牌
// app.module.tsproviders: [UserService,{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]
使用InjectionToken
// app.config.tsimport { InjectionToken } from '@angular/core';export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');// 注入的对象providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]// app.component.tsconstructor(@Inject(APP_CONFIG) config: AppConfig) {this.title = config.title;}
Factory工厂提供者
动态的提供者。
(所有提供者最终形态都是工厂,一般用于token等基本类型的注入)
// 函数中直接new的方式创建,注入依赖或直接传基本类型// Logger, UserService是另外需要依赖的令牌let heroServiceFactory = (logger: Logger, userService: UserService) => {return new HeroService(logger, userService.user.isAuthorized);};export let heroServiceProvider ={ provide: HeroService,useFactory: heroServiceFactory,deps: [Logger, UserService]};import { Component } from '@angular/core';import { heroServiceProvider } from './hero.service.provider';@Component({selector: 'app-heroes',providers: [ heroServiceProvider ],template: `<h2>Heroes</h2><app-hero-list></app-hero-list>`})export class HeroesComponent { }
怎样根据@Input输入属性动态注入)
import { StandardCategoryService } from 'src/app/routes/standard-data/standard-category/domain/services/standard-category.service';import { SystemCategoryService } from 'src/app/routes/shared/services/system-category.service';import { HttpMessenger } from 'src/app/routes/shared/services/http-messenger';// 拟根据输入属性@Input() standardCId动态注入,但是此时得不到const createSystemCategoryServiceFactory = (standardCId) => {if (standardCId) {return (httpMessenger: HttpMessenger) => {return new StandardCategoryService(httpMessenger);};} else {return (httpMessenger: HttpMessenger) => {return new SystemCategoryService(httpMessenger);};}};export const getSystemCategoryServiceProvider = (standardCId) => {console.log(standardCId);return {provide: SystemCategoryService,useFactory: createSystemCategoryServiceFactory(standardCId),deps: [HttpMessenger]};};
创建多例
Using multiple instances of the same service
Create new instance of class that has dependencies, not understanding factory provider
injector.get
ngOnInit() {if (this.standardCId) {this.systemCategoryService = this.injector.get<SystemCategoryService | StandardCategoryService>(StandardCategoryService);} else {this.systemCategoryService = this.injector.get<SystemCategoryService | StandardCategoryService>(SystemCategoryService);}this.getTreeNodes();}
类-接口(DI实战) 抽象类不用作继承,只用作依赖注入的令牌
LoggerService 和 DateLoggerService本可以从 MinimalLogger 中继承。 它们也可以实现 MinimalLogger,而不用单独定义接口。 但它们没有。 MinimalLogger 在这里仅仅被用作一个 “依赖注入令牌”
// Class used as a "narrowing" interface that exposes a minimal logger// Other members of the actual implementation are invisibleexport abstract class MinimalLogger {logs: string[];logInfo: (msg: string) => void;}
export class xxx {prorivders: [{ provide: MinimalLogger, useExisting: LoggerService }]}
