一、什么是依赖注入?
首先,用最通俗的语言来说,某公司财务类要给员工发放工资,它需要人事类提供一个薪资标准,财务类想要完成工资发放,就必须实例化人事类,,也就是说财务类依赖于人事类;
官方回答: 依赖注入 Dependency Injection
(简称DI)是用来创建对象及其依赖的其它对象的一种方式。 当依赖注入系统创建某个对象实例时,会负责提供该对象所依赖的对象(称为该对象的依赖)。
以下面分页示例讲解:
有一个page数据类,依赖于page基类,下面有三个组件,分别是文章列表、员工列表、视频列表组件,因为三个都是列表,所以肯定用到分页,三个组件从数据库中获取当前数据的总条数,然后计算出分页数,然后重新设置分页信息,假如文章列表让其一页显示20条数据,员工列表一页显示30条数据,视频列表一页显示15条数据,这时我们就应该给每一个列表都提供一个数据提供商,也就是说三个列表都是基于page数据类,但是由于每个组件设置分页数都不一样,所以必须把page数据类作为一个单独的数据提供商提供给每一个列表,他们的作用域是互不相干的;
二、angular是如何实现依赖注入的?
1. 注入器
每一个组件都有一个注入器实例,负责注入组件依赖的对象,注入器是angular提供的一个服务类,一般注入器会通过组件中的构造函数将组件中所需的对象注入进组件。(injector.get()
和构造函数中注入对比见下文)
constructor(private translate: TranslateService,
private activatedRoute : ActivatedRoute,
private router: Router,
private http: Http) {}
注意
:
1. 在angular中存在一个与组件树类似的注入器树;
2. 注入器冒泡——在自己注入器中没有找到服务实例,就回去父组件注入器或模板注入器中查找,直到跟注入器都没有找到,angular就会报错
2. 提供器(provider)
用于配置注入器,注入器通过它来创建被依赖对象的实例,Provider把标识映射到工厂方法中,被依赖的对象就是通过该方法创建的。
2.1 基本方式
providers: [LoginUserInfoService]
基本方法中,可以直接在providers
中写入需要的服务,相当于使用new实例化一个对象,这样的话就可以在其他的服务或组件中使用该服务;
2.2 useClass
providers:
[{ provide: UserInfoService, useClass:LoginUserInfoService }]
使用useClass
可以根据需要,选择注入非默认的服务。provide指定了提供器的token, useClass
表示实例化属性为new。
2.3 useFactory
providers: [
{
provide: APP_INITIALIZER,
useFactory: appInitializerFactory, //appInitializerFactory为函数(自定义),可以根据不同的条件,使用不同的服务
deps: [TranslateService, Injector],
multi: true
}
使用useFactory
可以根据不同的条件,使用不同的服务。在其他地方调用APP_INITIALIZER
时就会使用不同的服务,展示不同的结果。其中,deps
是Factory
中需要注入的内容。
三、injector.get()和在构造函数中注入有什么区别?
export function appInitializerFactory(translate: TranslateService, injector: Injector) {
return () => new Promise<any>((resolve: any) => {
const locationInitialized = injector.get(LOCATION_INITIALIZED, Promise.resolve(null));
locationInitialized.then(() => {
const lang = translate.getBrowserLang() === 'zh' ? 'zh' : 'en';
translate.setDefaultLang(lang);
translate.use(lang).subscribe(() => {
console.info(`Successfully initialized '${lang}' language.'`);
}, err => {
console.error(`Problem with '${lang}' language initialization.'`);
}, () => {
resolve(null);
});
});
});
}
使用构造函数注入的时候明确的声明了那些服务是需要注入的,这样通过依赖检查可以直接查找该组建所需的依赖项。
使用Injector
注入是为了解决一些动态注入的需求,此时Angular不知道你想要具体注入哪些依赖项,所以其实是注入了所有服务的DI容器(注意不是所有服务),
首选应该考虑使用构造函数注入,当有动态注入需求的时候再考虑使用Injector
四、单例服务
单例服务是指在应用中只存在一个实例的服务;
下面看一个单例服务示例:
新建项目, 并创建两个组件(parent
,child
),再创建一个logger服务;
将app.component.html
和app.component.ts
改成如下所示:
<h1>{{ title }}</h1>
<ul>
<li *ngFor="let log of loggerService.getLogges()">{{ log }}</li>
</ul>
<button (click)="addLog()">AddLogAppModule</button>
<hr />
<app-parent></app-parent>
import { Component } from '@angular/core';
import {LoggerService} from './logger.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'test-teach';
constructor(private loggerService: LoggerService) {}
addLog() {
this.loggerService.addLog('add log from appComponent successfully');
}
}
将logger.service.ts
修改为如下:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class LoggerService {
private logges: string[] = [];
constructor() { }
addLog(log: string) {
this.logges.push(log);
}
getLogges() {
return this.logges;
}
}
同时,将parent.component.html
和parent.component.ts
修改为如下:
<ul>
<li *ngFor="let log of loggerService.getLogges()">{{ log }}</li>
</ul>
<button (click)="addLog()">AddLogParentt</button>
<app-child></app-child>
import { Component, OnInit } from '@angular/core';
import { LoggerService } from '../logger.service';
@Component({
selector: 'app-parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.scss']
})
export class ParentComponent implements OnInit {
constructor(private loggerService: LoggerService) {}
ngOnInit() {}
addLog() {
this.loggerService.addLog('add log from parent component');
}
}
将child.component.html
和child.component.ts
修改为如下:
<p>child works!</p>
<ul>
<li *ngFor="let log of loggerService.getLogges()">{{ log }}</li>
</ul>
<button (click)="addLog()">AddLog</button>
import { Component, OnInit } from '@angular/core';
import { LoggerService } from '../logger.service';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.scss']
})
export class ChildComponent implements OnInit {
constructor(private loggerService: LoggerService) {}
ngOnInit() {}
addLog() {
this.loggerService.addLog('add log from child component');
}
}
运行效果如下:
可以看出,无论我们点击那一个btn,都会同步消息,因此,appModule
、parent
和child
用的都是一个服务实例;
- 组件层级关系是
appComponent
>parent
>child
- 程序运行时,发现
child
中需要一个loggerService
服务,就在child
对应的注入器中找,没找到就去父注入器中查找,若无找到再次冒泡到父注入器中,也即是前面提到的注入器冒泡
那如何实现非单例服务了??
非单例服务也即是阻止注入器冒泡,因此只需要在需要的地方提供一个服务提供商,如下在parent
中提供服务提供商providers: [LoggerService]
,
可以看出,appCompoent
和parent
以及child
之间的消息是不同步的;
常见干预注入器冒泡方法:
@Self()
:只允许在自己的注入器中查找服务@Optional()
:使服务可选,也就是找到就用,没找到也不报错(没有该装饰器的情况下,没有找到是会报错的,就像上面的例子那样)@SkipSelf()
:跳过自己的注入器,向父级注入器中查找@Host()
:在其宿主组件注入器中查找服务实例
constructor(@Self() private translate: TranslateService) {}
具体的用法可以自己增加尝试,这里不再做说明;
五、摇树优化
摇树优化是指编译器选项,是把为用到的代码从最终生成的包中移除,前提是服务商是可摇树优化的 ,从而减小打包体积;
使用 providedIn: 'root’
在@Injectable()
中providedIn
元数据属性有两种参数,一种是 ‘root
’注入器级别,也即是AppModule
注入器,可以在整个工程中使用,不用再次导入;另一种是 ‘ngModule
’级别,假如你只想让其在LoginModule
中使用该服务,只需如此定义即可: providedIn: 'LoginModule'
懒加载 providedIn: ‘root’ 解决方案
如果我们在懒加载中使用providedIn: 'root'
,虽然 ‘root’是AppModule
,但是如果该组件只是在懒性组件或服务中注入,那么就只会在延迟加载的bundle中;如果又额外将服务注入到其他正常加载的模块中,那么该服务会自动绑定到main
的bundle中;
简单来讲:
1、如果服务仅被注入到懒加载模块,它将捆绑在懒加载包中
2、如果服务又被注入到正常模块中,它将捆绑在主包中
但是他也会导致一个新问题出现:—(循环依赖)
我们可以通过创建一个 LazyServiceModule
来避免这个问题,它将是 LazyModule
的一个子模块,并将被用作我们想要提供的所有懒加载服务的“锚”,这样写的优点:
1. 它防止我们将懒加载的服务注入应用程序的正常加载模块
2. 只有当服务被真正注入其他惰性组件时,它才会打包到服务中
那怎么样的提供商是可以要输优化的呢?
只要在服务本身的@Injectable()
装饰器中指定,而不是在依赖该服务的@NgModule()
或组件的provides
数组中指定,就可以制作一个可摇树优化的提供商;
创建可摇树优化服务方法:
@Injectable({
providedIn: 'root', //root级别服务,root注入器就是 AppModule 注入器
})
export class Service {
}
如下方法不能摇树优化:
import { Injectable, NgModule } from '@angular/core';
@Injectable()
export class Service {
doSomething(): void {
}
}
@NgModule({
providers: [Service],
})
export class ServiceModule {
}
在我们项目中多使用的是这种提供商方式,虽然最后实现相同,但是假如此服务定义在LoginModule
中,想在LogModule
中使用该服务,就只能将LoginMoudle
引入到LogMoudle
中,而使用providedIn: 'root'
就不同考虑这些,并且打包阶段若未使用到此服务,angular会自动摇掉该服务,减小打包体积;