Abstract
DI(Dependency and Injection)是一种重要的设计模式,并被用于Angular框架中。它的优势在于,让组件adaptable,让服务reusable。
概念图
基础示例
定义Dependency:
config.service.ts:
import { Injectable } from '@angular/core';
@Injectable()
export class ConfigService {
public data = ['test', 'DI'];
public showData() {
console.log('ConfigService:', this.data);
}
public setData(data: any[]) {
this.data = data;
}
}
暴露Provider:
a.component.ts:
import { ConfigService } from 'src/app/services/config.service';
@Component({
providers: [ConfigService],// 将需要的服务的提供者暴露出来
})
进行Inject:
a.component.ts:
export class AComponent implements OnInit {
constructor(private configService: ConfigService) {// 构造器中注入依赖
configService.showData();// 使用依赖
}
ngOnInit(): void {
this.configService.setData([]);
}
}
效果:
Provider Scope(提供者的作用域)
组件级别
在组件的@Component()中providers配置,如上:
@Component({
providers: [ConfigService],// 仅当前组件可注入
})
模块级别
xx.module.ts:
import { ConfigService } from './services/config.service';
@NgModule({
providers: [ConfigService],// 注入到模块中,该模块范围内的组件无需再配置providers
})
app级别
config.service.ts:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'// 相当于整个应用app内均可注入,比模块范围更大
})
export class ConfigService {}
插曲-循环依赖(Dependency Circle)
如果A服务注入了B服务,B服务注入了C服务,C服务再注入A服务,这样它们之间相互依赖就会产生循环依赖;会产生编译报错。解决办法是消除这种依赖关系,将共同依赖抽离到一个新的模块D中,A,B,C再分别注入D。
不同类型的Provider(useClass/useExisting/useFactory/useValue)
导读
一个完整的provider语法应该如下:
providers:[{provide: Logger, useClass: Logger}]// 第一个属性是token标识符,用来注入时作为选择名使用的;第二个属性可以是useClass/useExisting/useFactory/useValue四种类型,对应的值也不同,分别具有不同的场景使用意义。
官网demo
简写形式翻译:
providers: [ConfigService]
// 等价于
providers: [{ provide: ConfigService, useClass: ConfigService }]
useClass
上述例子即是,默认形式。
useExisting
使用一个已经存在的provider,即别名。
假设一个已有的
config.service.ts:
import { Injectable } from '@angular/core';
@Injectable()
export class ConfigService {
public data = ['test', 'DI'];
public showData() {
console.log('ConfigService:', this.data);
}
public setData(data: any[]) {
this.data = data;
}
}
和一个抽象的abstract-config.service.ts:
export class AbstractConfigService {
constructor() { }
public showData() {
}
public setData(data: any[]) {
}
}
注入到a.components.ts:
import { AbstractConfigService } from 'src/app/services/abstract-config.service';
import { ConfigService } from 'src/app/services/config.service';
@Component({
providers: [ConfigService, {provide: AbstractConfigService, useExisting: ConfigService}],// 这两种方式都能生效
})
export class AComponent implements OnInit {
constructor(private configService: ConfigService, private renameConfigService: AbstractConfigService) {// 构造器中注入依赖
configService.showData();// 使用依赖
renameConfigService.showData();// 使用依赖
}
ngOnInit(): void {
this.renameConfigService.setData([]);
this.configService.showData();// 使用依赖
this.renameConfigService.showData();// 使用依赖
}
}
效果:
与useClass的区别:
假若使用useClass注入到a.components.ts:
@Component({
providers: [ConfigService, { provide: AbstractConfigService, useClass: ConfigService }], // 将需要的服务的提供者暴露出来
})
ngOnInit(): void {
this.renameConfigService.setData([]);
console.log('configService: ');
this.configService.showData(); // 使用依赖
console.log('useClass renameConfigService: ');
this.renameConfigService.showData(); // 使用依赖
}
效果:
可以看出,区别在于注入时,useExisting使用的同一个实例,而useClass则是新建一个实例。
useFactory
该provider可以组合其他的依赖,从而构建出一个综合性的provider。
新建一个服务:
ng g s services/log
log.service.ts:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'// 根注入
})
export class LogService {
constructor() { }
public logStart(){
console.log('process start!')
}
public logEnd(){
console.log('process end!')
}
}
原有的configService:
config.service.ts:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ConfigService {
public data = ['test', 'DI'];
public showData() {
console.log('ConfigService:', this.data);
}
public setData(data: any[]) {
this.data = data;
}
}
新建一个logConfigService:
log-config.service.ts:
import { LogService } from './log.service';
import { ConfigService } from './config.service';
// 等下手动注入,所以不需要@Injectable
export class LogConfigService {
constructor(
private logService: LogService,
private configService: ConfigService
) {}
showData() {
this.logService.logStart();
this.configService.showData();
this.logService.logEnd();
}
}
// 提供工厂函数
export function logConfigFactory(logger: LogService, config: ConfigService) {
return new LogConfigService(logger, config);
}
注入到组件中:
a.components.ts:
import { ConfigService } from 'src/app/services/config.service';
import { LogConfigService, logConfigFactory } from 'src/app/services/log-config.service';
import { LogService } from 'src/app/services/log.service';
@Component({
providers: [{provide: LogConfigService,useFactory: logConfigFactory,deps: [LogService, ConfigService]}]// 需要按顺序指定依赖项,顺序为Factory函数入参顺序
})
export class AComponent implements OnInit {
constructor(private logConfig: LogConfigService) {
this.logConfig.showData();
}
}
效果:
useValue && InjectionToken
这个模式就是绑定一个静态值,个人觉得和自定义注入token放一起比较适用。
注入到组件中:
a.component.ts:
import { Component, Inject, InjectionToken, OnInit } from '@angular/core';
interface AppConfig {
msg: string;
}
export const APP_CONFIG = new InjectionToken<AppConfig>('app config');// 提供参数方便标识令牌
@Component({
selector: 'app-a',
templateUrl: './a.component.html',
styleUrls: ['./a.component.scss'],
providers: [{provide: APP_CONFIG,useValue: {msg: 'hello world!'}}]
})
export class AComponent {
constructor(@Inject(APP_CONFIG) config: AppConfig) {
console.log("msg: ",config.msg)
}
}
效果:
Injection context
这是手动注入依赖的方法,是一种不使用构造器自动注入的方式
eg: 路由拦截器
export const authGuard: CanActivateFn =
(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const router = inject(Router);
router.navigateByUrl('/home');
return false;
};
推荐@Injectable({ provideIn: })
原因是TreeShaking, 配置项那种providers写法注入的依赖不会触发TreeShaking。
注入层级
platform
root
module
component
范围依次缩小。
platformBrowserDynamic().bootstrapModule(AppModule).then(ref=>{...})
这行代码是platform injector里创建了一个ModuleInjector,并且作为项目的根模块,所以为root。即@Injectable({ provideIn: ‘root’})就是注入在AppModule里。区别于配置项providers写法,它可以TreeShaking。
Injector Error
在某个具体的组件中解析注入依赖时,会自底向上地去寻找,找到即停,找不到就会报错:
装饰器
@Optional()
该装饰器表示,解析某个依赖时,如果找不到provider,也不报错,而是改为注入一个null来代替。学过Java的可能会一眼就明白。
import { Optional } from '@angular/core';
constructor(@Optional() private logService: LogService) {
console.log(this.logService);
}
效果:
@Self()
该装饰器表示,只允许provider的来源是当前组件的,如果当前component范围内找不到provider时,就不再继续往上层寻找,直接报错。
import { Self } from '@angular/core';
@Component({
providers: [LogService]// 只有这里注入的provider才范围内有效
})
constructor(@Self() private logService: LogService) {
console.log(this.logService);
}
@SkipSelf()
这个和@Self相反,跳过当前组件直接往上层寻找provider。
@Host()
终止装饰器,表示当前组件中如果还找不到provider,也不再往父组件,以及父模块中寻找了,当前组件为解析路径的last stop。
留言
为什么Angular要搞这么麻烦的依赖注入模式,而不是直接import文件来管理依赖呢?
依赖注入给我们开发时提供了怎样的帮助呢,在React和Vue中没有这种复杂的设计时,在相同的场景下又是怎么用代码解决需求的?
想要原文代码的,欢迎来戳我的git仓:
https://gitee.com/gao-hui007/my-blogs/blob/master/docs/angular/DI.md