深入浅析Angular中怎么使用依赖注入

本篇文章给大家介绍一下依赖注入在 Angular 中的应用,希望对大家有所帮助!

深入浅析Angular中怎么使用依赖注入

本文通过实际案例,带大家了解依赖注入Angular中的应用和部分实现原理,其中包括

  • useFactoryuseClassuseValueuseExisting 不同提供商的应用场景

  • ModuleInjectorElementInjector不同层次注入器的意义

  • @Injectable()@NgModule()中定义provider的区别

  • @Optional()@Self()@SkipSelf()@Host() 修饰符的使用

  • muti(多提供商)的应用场景

【相关教程推荐:《angular教程》】

如果你还不清楚什么是依赖注入,可以先看下这篇文章详解依赖注入

useFactory、useClass、useValue 和 useExisting 不同类型provider的应用场景

下面,我们通过实际例子,来对几个提供商的使用场景进行说明。

useFactory 工厂提供商

某天,咱们接到一个需求:实现一个本地存储的功能,并将其注入Angular应用中,使其可以在系统中全局使用

首先编写服务类storage.service.ts,实现其存储功能

// storage.service.ts
export class StorageService {
  get(key: string) {
    return JSON.parse(localStorage.getItem(key) || '{}') || {};
  }

  set(key: string, value: ITokenModel | null): boolean {
    localStorage.setItem(key, JSON.stringify(value));
    return true;
  }

  remove(key: string) {
    localStorage.removeItem(key);
  }
}

如果你马上在user.component.ts中尝试使用

// user.component.ts
@Component({
  selector: 'app-user',
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.css']
})
export class CourseCardComponent  {
  constructor(private storageService: StorageService) {
    ...
  }
  ...
}

你应该会看到这样的一个错误:

NullInjectorError: No provider for StorageService!

显而易见,我们并没有将StorageService添加到 Angular的依赖注入系统中。Angular无法获取StorageService依赖项的Provider,也就无法实例化这个类,更没法调用类中的方法。

接下来,我们本着缺撒补撒的理念,手动添加一个Provider。修改storage.service.ts文件如下

// storage.service.ts
export class StorageService {
  get(key: string) {
    return JSON.parse(localStorage.getItem(key) || '{}') || {};
  }

  set(key: string, value: any) {
    localStorage.setItem(key, JSON.stringify(value));
  }

  remove(key: string) {
    localStorage.removeItem(key);
  }
}

// 添加工厂函数,实例化StorageService
export storageServiceProviderFactory(): StorageService {
  return new StorageService();
}

通过上述代码,我们已经有了Provider。那么接下来的问题,就是如果让Angular每次扫描到StorageService这个依赖项的时候,让其去执行storageServiceProviderFactory方法,来创建实例

这就引出来了下一个知识点InjectionToken

在一个服务类中,我们常常需要添加多个依赖项,来保证服务的可用。而InjectionToken是各个依赖项的唯一标识,它让Angular的依赖注入系统能准确的找到各个依赖项的Provider

接下来,我们手动添加一个InjectionToken

// storage.service.ts
import { InjectionToken } from '@angular/core';

export class StorageService {
  get(key: string) {
    return JSON.parse(localStorage.getItem(key) || '{}') || {};
  }

  set(key: string, value: any) {
    localStorage.setItem(key, JSON.stringify(value));
  }

  remove(key: string) {
    localStorage.removeItem(key);
  }
}

export storageServiceProviderFactory(): StorageService {
  return new StorageService();
}

// 添加StorageServiced的InjectionToken
export const STORAGE_SERVICE_TOKEN = new InjectionToken<StorageService>(&#39;AUTH_STORE_TOKEN&#39;);

ok,我们已经有了StorageServiceProviderInjectionToken

接下来,我们需要一个配置,让Angular依赖注入系统能够对其进行识别,在扫描到StorageService(Dependency)的时候,根据STORAGE_SERVICE_TOKEN(InjectionToken)去找到对应的storageServiceProviderFactory(Provider),然后创建这个依赖项的实例。如下,我们在module中的@NgModule()装饰器中进行配置:

// user.module.ts
@NgModule({
  imports: [
    ...
  ],
  declarations: [
    ...
  ],
  providers: [
    {
      provide: STORAGE_SERVICE_TOKEN, // 与依赖项关联的InjectionToken,用于控制工厂函数的调用
      useFactory: storageServiceProviderFactory, // 当需要创建并注入依赖项时,调用该工厂函数
      deps: [] // 如果StorageService还有其他依赖项,这里添加
    }
  ]
})
export class UserModule { }

到这里,我们完成了依赖的实现。最后,还需要让Angular知道在哪里注入Angular提供了 @Inject装饰器来识别

// user.component.ts
@Component({
  selector: &#39;app-user&#39;,
  templateUrl: &#39;./user.component.html&#39;,
  styleUrls: [&#39;./user.component.css&#39;]
})
export class CourseCardComponent  {
  constructor(@Inject(STORAGE_SERVICE_TOKEN) private storageService: StorageService) {
    ...
  }
  ...
}

到此,我们便可以在user.component.ts调用StorageService里面的方法了

useClass 类提供商

emm...你是否觉得上述的写法过于复杂了,而在实际开发中,我们大多数场景是无需手动创建ProviderInjectionToken的。如下:

// user.component.ts
@Component({
  selector: &#39;app-user&#39;,
  templateUrl: &#39;./user.component.html&#39;,
  styleUrls: [&#39;./user.component.css&#39;]
})
export class CourseCardComponent  {
  constructor(private storageService: StorageService) {
    ...
  }
  ...
}

// storage.service.ts
@Injectable({ providedIn: &#39;root&#39; })
export class StorageService {}

// user.module.ts
@NgModule({
  imports: [
    ...
  ],
  declarations: [
    ...
  ],
  providers: [StorageService]
})
export class UserModule { }

下面,我们来对上述的这种简写模式进行剖析。

user.component.ts,我们舍弃了@Inject装饰器,直接添加依赖项private storageService: StorageService,这得益于AngularInjectionToken的设计。

InjectionToken不一定必须是一个InjectionToken object,只要保证它在运行时环境中能够识别对应的唯一依赖项即可。所以,在这里,你可以用类名即运行时中的构造函数名称来作为依赖项InjectionToken。省略创建InjectionToken这一步骤。

// user.module.ts
@NgModule({
  imports: [
    ...
  ],
  declarations: [
    ...
  ],
  providers: [{
    provide: StorageService, // 使用构造函数名作为InjectionToken
    useFactory: storageServiceProviderFactory,
    deps: []
  }]
})
export class UserModule { }

注意:由于Angular依赖注入系统是在运行时环境中根据InjectionToken识别依赖项,进行依赖注入的。所以这里不能使用interface名称作为InjectionToken,因为其只存在于Typescript语言的编译期,并不存在于运行时中。而对于类名来说,其在运行时环境中以构造函数名体现,可以使用。

接下来,我们可以使用useClass替换useFactory,其实也能达到创建实例的效果,如下:

...
providers: [{
  provide: StorageService,
  useClass: StorageService,
  deps: []
}]
...

当使用useClass时,Angular会将后面的值当作一个构造函数,在运行时环境中,直接执行new指令进行实例化,这也无需我们再手动创建 Provider

当然,基于Angular依赖注入设计,我们可以写得更简单

...
providers: [StorageService]
...

直接写入类名providers数组中,Angular会识别其是一个构造函数,然后检查函数内部,创建一个工厂函数去查找其构造函数中的依赖项,最后再实例化

useClass还有一个特性是,Angular会根据依赖项Typescript中的类型定义,作为其运行时InjectionToken去自动查找Provider。所以,我们也无需使用@Inject装饰器来告诉Angular在哪里注入了

你可以简写如下

  ...
  // 无需手动注入 :constructor(@Inject(StorageService) private storageService: StorageService)
  constructor(private storageService: StorageService) {
    ...
  }
  ...

这也就是我们平常开发中,最常见的一种写法。

useValue 值提供商

完成本地存储服务的实现后,我们又收到了一个新需求,研发老大希望提供一个配置文件,来存储StorageService的一些默认行为

我们先创建一个配置

const storageConfig = {
  suffix: &#39;app_&#39; // 添加一个存储key的前缀
  expires: 24 * 3600 * 100 // 过期时间,毫秒戳
}

useValue则可以 cover 住这种场景。其可以是一个普通变量,也可以是一个对象形式。

配置方法如下:

// config.ts
export interface STORAGE_CONFIG = {
  suffix: string;
  expires: number;
}

export const STORAGE_CONFIG_TOKEN = new InjectionToken<STORAGE_CONFIG>(&#39;storage-config&#39;);
export const storageConfig = {
  suffix: &#39;app_&#39; // 添加一个存储key的前缀
  expires: 24 * 3600 * 100 // 过期时间,毫秒戳
}

// user.module.ts
@NgModule({
  ...
  providers: [
    StorageService,
    {
      provide: STORAGE_CONFIG_TOKEN,
      useValue: storageConfig
    }
  ],
  ...
})
export class UserModule {}

user.component.ts组件中,直接使用配置对象

// user.component.ts
@Component({
  selector: &#39;app-user&#39;,
  templateUrl: &#39;./user.component.html&#39;,
  styleUrls: [&#39;./user.component.css&#39;]
})
export class CourseCardComponent  {
  constructor(private storageService: StorageService, @Inject(STORAGE_CONFIG_TOKEN) private storageConfig: StorageConfig) {
    ...
  }

  getKey(): void {
    const { suffix } = this.storageConfig;
    console.log(this.storageService.get(suffix + &#39;demo&#39;));
  }
}

useExisting 别名提供商

如果我们需要基于一个已存在的provider来创建一个新的provider,或需要重命名一个已存在的provider时,可以用useExisting属性来处理。比如:创建一个angular的表单控件,其在一个表单中会存在多个,每个表单控件存储不同的值。我们可以基于已有的表单控件provider来创建

// new-input.component.ts
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from &#39;@angular/forms&#39;;

@Component({
  selector: &#39;new-input&#39;,
  exportAs: &#39;newInput&#39;,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => NewInputComponent), // 这里的NewInputComponent已经声明了,但还没有被定义。无法直接使用,使用forwardRef可以创建一个间接引用,Angular在后续在解析该引用
      multi: true
    }
  ]
})
export class NewInputComponent implements ControlValueAccessor {
  ...
}

ModuleInjector 和 ElementInjector 层级注入器的意义

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

  • ModuleInjector —— 使用 @NgModule() 或 @Injectable() 的方式在模块中注入

  • ElementInjector —— 在 @Directive() 或 @Component() 的 providers 属性中进行配置

我们通过一个实际例子来解释两种注入器的应用场景,比如:设计一个展示用户信息的卡片组件

ModuleInjector 模块注入器

我们使用user-card.component.ts来显示组件,用UserService来存取该用户的信息

// user-card.component.ts
@Component({
  selector: &#39;user-card.component.ts&#39;,
  templateUrl: &#39;./user-card.component.html&#39;,
  styleUrls: [&#39;./user-card.component.less&#39;]
})
export class UserCardComponent {
  ...
}

// user.service.ts
@Injectable({
  providedIn: "root"
})
export class UserService {
  ...
}

上述代码是通过@Injectable添加到根模块中,root即根模块的别名。其等价于下面的代码

// user.service.ts
export class UserService {
  ...
}

// app.module.ts
@NgModule({
  ...
  providers: [UserService], // 通过providers添加
})
export class AppModule {}

当然,如果你觉得UserService只会在UserModule模块下使用的话,你大可不必将其添加到根模块中,添加到所在模块即可

// user.service.ts
@Injectable({
  providedIn: UserModule
})
export class UserService {
  ...
}

如果你足够细心,会发现上述例子中,我们既可以通过在当前service文件中的@Injectable({ provideIn: xxx })定义provider,也可以在其所属module中的@NgModule({ providers: [xxx] })定义。那么,他们有什么区别呢?

@Injectable()@NgModule()除了使用方式不同外,还有一个很大的区别是:

使用 @Injectable() 的 providedIn 属性优于 @NgModule() 的 providers 数组,因为使用 @Injectable() 的 providedIn 时,优化工具可以进行摇树优化 Tree Shaking,从而删除你的应用程序中未使用的服务,以减小捆绑包尺寸。

我们通过一个例子来解释上面的概述。随着业务的增长,我们扩展了UserService1UserService2两个服务,但由于某些原因,UserService2一直未被使用。

如果通过@NgModule()providers引入依赖项,我们需要在user.module.ts文件中引入对应的user1.service.tsuser2.service.ts文件,然后在providers数组中添加UserService1UserService2引用。而由于UserService2所在文件在module文件中被引用,导致Angular中的tree shaker错误的认为这个UserService2已经被使用了。无法进行摇树优化。代码示例如下:

// user.module.ts
import UserService1 from &#39;./user1.service.ts&#39;;
import UserService2 from &#39;./user2.service.ts&#39;;
@NgModule({
  ...
  providers: [UserService1, UserService2], // 通过providers添加
})
export class UserModule {}

那么,如果通过@Injectable({providedIn: UserModule})这种方式,我们实际是在服务类自身文件中引用了use.module.ts,并为其定义了一个provider。无需在UserModule中在重复定义,也就不需要在引入user2.service.ts文件了。所以,当UserService2没有被依赖时,即可被优化掉。代码示例如下:

// user2.service.ts
import UserModule from &#39;./user.module.ts&#39;;
@Injectable({
  providedIn: UserModule
})
export class UserService2 {
  ...
}

ElementInjector 组件注入器

在了解完ModuleInjector后,我们继续通过刚才的例子讲述ElementInjector

最初,我们系统中的用户只有一个,我们也只需要一个组件和一个UserService来存取这个用户的信息即可

// user-card.component.ts
@Component({
  selector: &#39;user-card.component.ts&#39;,
  templateUrl: &#39;./user-card.component.html&#39;,
  styleUrls: [&#39;./user-card.component.less&#39;]
})
export class UserCardComponent {
  ...
}

// user.service.ts
@Injectable({
  providedIn: "root"
})
export class UserService {
  ...
}

注意:上述代码将UserService被添加到根模块中,它仅会被实例化一次。

如果这时候系统中有多个用户,每个用户卡片组件里的UserService需存取对应用户的信息。如果还是按照上述的方法,UserService只会生成一个实例。那么就可能出现,张三存了数据后,李四去取数据,取到的是张三的结果。

那么,我们有办法实例化多个UserService,让每个用户的数据存取操作隔离开么?

答案是有的。我们需要在user.component.ts文件中使用ElementInjector,将UserServiceprovider添加即可。如下:

// user-card.component.ts
@Component({
  selector: &#39;user-card.component.ts&#39;,
  templateUrl: &#39;./user-card.component.html&#39;,
  styleUrls: [&#39;./user-card.component.less&#39;],
  providers: [UserService]
})
export class UserCardComponent {
  ...
}

通过上述代码,每个用户卡片组件都会实例化一个UserService,来存取各自的用户信息。

如果要解释上述的现象,就需要说到AngularComponents and Module Hierarchical Dependency Injection

在组件中使用依赖项时,Angular会优先在该组件的providers中寻找,判断该依赖项是否有匹配的provider。如果有,则直接实例化。如果没有,则查找父组件的providers,如果还是没有,则继续找父级的父级,直到根组件(app.component.ts)。如果在根组件中找到了匹配的provider,会先判断其是否有存在的实例,如果有,则直接返回该实例。如果没有,则执行实例化操作。如果根组件仍未找到,则开始从原组件所在的module开始查找,如果原组件所在module不存在,则继续查找父级module,直到根模块(app.module.ts)。最后,仍未找到则报错No provider for xxx

@Optional()、@Self()、@SkipSelf()、@Host() 修饰符的使用

Angular应用中,当依赖项寻找provider时,我们可以通过一些修饰符来对搜索结果进行容错处理限制搜索的范围

@Optional()

通过@Optional()装饰服务,表明让该服务可选。即如果在程序中,没有找到服务匹配的provider,也不会程序崩溃,报错No provider for xxx,而是返回null

export class UserCardComponent {
  constructor(@Optional private userService: UserService) {}
}

@Self()

使用@Self()Angular仅查看当前组件或指令的ElementInjector

如下,Angular只会在当前UserCardComponentproviders中搜索匹配的provider,如果未匹配,则直接报错。No provider for UserService

// user-card.component.ts
@Component({
  selector: &#39;user-card.component.ts&#39;,
  templateUrl: &#39;./user-card.component.html&#39;,
  styleUrls: [&#39;./user-card.component.less&#39;],
  providers: [UserService],
})
export class UserCardComponent {
  constructor(@Self() private userService?: UserService) {}
}

@SkipSelf()

@SkipSelf()@Self()相反。使用@SkipSelf()Angular在父ElementInjector中而不是当前ElementInjector中开始搜索服务.

// 子组件 user-card.component.ts
@Component({
  selector: &#39;user-card.component.ts&#39;,
  templateUrl: &#39;./user-card.component.html&#39;,
  styleUrls: [&#39;./user-card.component.less&#39;],
  providers: [UserService], // not work
})
export class UserCardComponent {
  constructor(@SkipSelf() private userService?: UserService) {}
}

// 父组件 parent-card.component.ts
@Component({
  selector: &#39;parent-card.component.ts&#39;,
  templateUrl: &#39;./parent-card.component.html&#39;,
  styleUrls: [&#39;./parent-card.component.less&#39;],
  providers: [
    {
      provide: UserService,
      useClass: ParentUserService, // work
    },
  ],
})
export class ParentCardComponent {
  constructor() {}
}

@Host()

@Host()使你可以在搜索provider时将当前组件指定为注入器树的最后一站。这和@Self()类似,即使树的更上级有一个服务实例,Angular也不会继续寻找。

multi 多服务提供商

某些场景下,我们需要一个InjectionToken初始化多个provider。比如:在使用拦截器的时候,我们希望在default.interceptor.ts之前添加一个 用于 token 校验的JWTInterceptor

...
const NET_PROVIDES = [
  { provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true },
  { provide: HTTP_INTERCEPTORS, useClass: JWTInterceptor, multi: true }
];
...

multi: 为false时,provider的值会被覆盖;设置为true,将生成多个provider并与唯一InjectionToken HTTP_INTERCEPTORS关联。最后可以通过HTTP_INTERCEPTORS获取所有provider的值

参考链接

Angular Dependency Injection: Complete Guide

Angular 中的依赖注入

更多编程相关知识,请访问:编程教学!!

以上就是深入浅析Angular中怎么使用依赖注入的详细内容,更多请关注其它相关文章!