Angular依赖注入 DI详解

Abstract

DI(Dependency and Injection)是一种重要的设计模式,并被用于Angular框架中。它的优势在于,让组件adaptable,让服务reusable。

概念图

injector-injects.png

基础示例

定义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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值