前言

之前我们也学习过了 angular 框架的依赖注入的一些基本使用方式,今天将更加深入的学习下依赖注入

用 @Optional 来让依赖是可选的,以及使用 @Host 来限定搜索方式

依赖可以注册在组件树的任何层级上。 当组件请求某个依赖时,Angular 会从该组件的注入器找起,沿着注入器树向上,直到找到了第一个满足要求的提供商。如果没找到依赖,Angular 就会抛出一个错误。

某些情况下,你需要限制搜索,或容忍依赖项的缺失。 你可以使用组件构造函数参数上的 @Host 和 @Optional 这两个限定装饰器来修改 Angular 的搜索行为。

  • @Optional 属性装饰器告诉 Angular 当找不到依赖时就返回 null。
  • @Host 属性装饰器会禁止在宿主组件以上的搜索。宿主组件通常就是请求该依赖的那个组件。 不过,当该组件投影进某个父组件时,那个父组件就会变成宿主。
  constructor(
      @Host() // 查找HeroCacheService实例限制在该组件或者它的父组件以下
      private heroCache: HeroCacheService,

      @Host()
      @Optional() // 限制在该组件的父组件内查找,如果没有找到则返回null,也不会报错
      private loggerService: LoggerService
  )

使用 @Inject 指定自定义提供商

自定义提供商让你可以为隐式依赖提供一个具体的实现,比如内置浏览器 API。下面的例子使用 InjectionToken 来提供 localStorage,将其作为 BrowserStorageService 的依赖项。

import { Inject, Injectable, InjectionToken } from '@angular/core';

export const BROWSER_STORAGE = new InjectionToken<Storage>('Browser Storage', {
  providedIn: 'root',
  factory: () => localStorage
});

@Injectable({
  providedIn: 'root'
})
export class BrowserStorageService {
  constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {}

  get(key: string) {
    this.storage.getItem(key);
  }

  set(key: string, value: string) {
    this.storage.setItem(key, value);
  }

  remove(key: string) {
    this.storage.removeItem(key);
  }

  clear() {
    this.storage.clear();
  }
}

factory 函数返回 window 对象上的 localStorage 属性。Inject 装饰器修饰一个构造函数参数,用于为某个依赖提供自定义提供商。

使用 @Self 和 @SkipSelf 来修改提供商的搜索方式

注入器也可以通过构造函数的参数装饰器来指定范围。下面的例子就在 Component 类的 providers 中使用浏览器的 sessionStorage API 覆盖了 BROWSER_STORAGE 令牌。同一个 BrowserStorageService 在构造函数中使用 @Self 和 @SkipSelf 装饰器注入了两次,来分别指定由哪个注入器来提供依赖。

import { Component, OnInit, Self, SkipSelf } from '@angular/core';
import { BROWSER_STORAGE, BrowserStorageService } from './storage.service';

@Component({
  selector: 'app-storage',
  template: `
    Open the inspector to see the local/session storage keys:

    <h3>Session Storage</h3>
    <button (click)="setSession()">Set Session Storage</button>

    <h3>Local Storage</h3>
    <button (click)="setLocal()">Set Local Storage</button>
  `,
  providers: [
    BrowserStorageService,
    { provide: BROWSER_STORAGE, useFactory: () => sessionStorage }
  ]
})
export class StorageComponent implements OnInit {

  constructor(
    @Self() private sessionStorageService: BrowserStorageService, // BrowserStorageService会读取该组件提供的 BROWSER_STORAGE
    @SkipSelf() private localStorageService: BrowserStorageService, // BrowserStorageService跳过该组件提供的 BROWSER_STORAGE 则默认读取自己组件的依赖
  ) { }

  ngOnInit() {
  }

  setSession() {
    this.sessionStorageService.set('hero', 'Dr Nice - Session');
  }

  setLocal() {
    this.localStorageService.set('hero', 'Dr Nice - Local');
  }
}

使用 @Self 装饰器时,注入器只在该组件的注入器中查找提供商。@SkipSelf 装饰器可以让你跳过局部注入器,并在注入器树中向上查找,以发现哪个提供商满足该依赖。 sessionStorageService 实例使用浏览器的 sessionStorage 来跟 BrowserStorageService 打交道,而 localStorageService 跳过了局部注入器,使用根注入器提供的 BrowserStorageService,它使用浏览器的 localStorage API。

使用提供商来定义依赖

为了从依赖注入器中获取服务,你必须传给它一个令牌。 Angular 通常会通过指定构造函数参数以及参数的类型来处理它。 参数的类型可以用作注入器的查阅令牌。 Angular 会把该令牌传给注入器,并把它的结果赋给相应的参数。

constructor(logger: LoggerService) {
  logger.logInfo('Creating HeroBiosComponent');
}

Angular 会要求注入器提供与 LoggerService 相关的服务,并把返回的值赋给 logger 参数。

如果注入器已经缓存了与该令牌相关的服务实例,那么它就会直接提供此实例。 如果它没有,它就要使用与该令牌相关的提供商来创建一个。

定义提供商的几种方式

import { Component, Inject } from '@angular/core';

import { DateLoggerService } from './date-logger.service';
import { Hero }              from './hero';
import { HeroService }       from './hero.service';
import { LoggerService }     from './logger.service';
import { MinimalLogger }     from './minimal-logger.service';
import { RUNNERS_UP,
         runnersUpFactory }  from './runners-up';

@Component({
  selector: 'app-hero-of-the-month',
  templateUrl: './hero-of-the-month.component.html',
  providers: [
    { provide: Hero,          useValue:    someHero },
    { provide: TITLE,         useValue:   'Hero of the Month' },
    { provide: HeroService,   useClass:    HeroService },
    { provide: LoggerService, useClass:    DateLoggerService },
    { provide: MinimalLogger, useExisting: LoggerService },
    { provide: RUNNERS_UP,    useFactory:  runnersUpFactory(2), deps: [Hero, HeroService] }
  ]
})
export class HeroOfTheMonthComponent {
  logs: string[] = [];

  constructor(
      logger: MinimalLogger,
      public heroOfTheMonth: Hero,
      @Inject(RUNNERS_UP) public runnersUp: string,
      @Inject(TITLE) public title: string)
  {
    this.logs = logger.logs;
    logger.logInfo('starting up');
  }
}

【值提供商:useValue】
useValue 键让你可以为 DI 令牌关联一个固定的值。 使用该技巧来进行运行期常量设置,比如网站的基础地址和功能标志等。 你也可以在单元测试中使用值提供商,来用一个 Mock 数据来代替一个生产环境下的数据服务。

【类提供商:useClass】
useClass 提供的键让你可以创建并返回指定类的新实例。

你可以使用这类提供商来为公共类或默认类换上一个替代实现。比如,这个替代实现可以实现一种不同的策略来扩展默认类,或在测试环境中模拟真实类的行为

【别名提供商:useExisting】
useExisting 提供了一个键,让你可以把一个令牌映射成另一个令牌。实际上,第一个令牌就是第二个令牌所关联的服务的别名,这样就创建了访问同一个服务对象的两种途径。

{ provide: MinimalLogger, useExisting: LoggerService }

MinimalLogger 可以访问 LoggerService 服务

【工厂提供商:useFactory】
useFactory 提供了一个键,让你可以通过调用一个工厂函数来创建依赖实例

{ provide: RUNNERS_UP,    useFactory:  runnersUpFactory(2), deps: [Hero, HeroService] }

注入器通过调用你用 useFactory 键指定的工厂函数来提供该依赖的值。 注意,提供商的这种形态还有第三个键 deps,它指定了供 useFactory 函数使用的那些依赖。

runners-up.ts

export function runnersUpFactory(take: number) {
  return (winner: Hero, heroService: HeroService): string => {
    /* ... */
  };
};

提供替代令牌:'InjectionToken'

依赖对象可以是一个简单的值,比如日期,数字和字符串,或者一个无形的对象,比如数组和函数。

这样的对象没有应用程序接口,所以不能用一个类来表示。更适合表示它们的是:唯一的和符号性的令牌,一个 JavaScript 对象,拥有一个友好的名字,但不会与其它的同名令牌发生冲突。

InjectionToken 具有这些特征。在Hero of the Month例子中遇见它们两次,一个是 title 的值,一个是 runnersUp 工厂提供商。

{ provide: TITLE,         useValue:   'Hero of the Month' },
{ provide: RUNNERS_UP,    useFactory:  runnersUpFactory(2), deps: [Hero, HeroService] }

这样创建 TITLE 令牌:

import { InjectionToken } from '@angular/core';

export const TITLE = new InjectionToken<string>('title');

使用一个前向引用(forwardRef)来打破循环

在 TypeScript 里面,类声明的顺序是很重要的。如果一个类尚未定义,就不能引用它。

这通常不是一个问题,特别是当你遵循一个文件一个类规则的时候。 但是有时候循环引用可能不能避免。当一个类A 引用类 B,同时'B'引用'A'的时候,你就陷入困境了:它们中间的某一个必须要先定义。

Angular 的 forwardRef() 函数建立一个间接地引用,Angular 可以随后解析。

providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],

forwardRef

允许引用尚未定义的引用

class Door {
  lock: Lock;

  // Door attempts to inject Lock, despite it not being defined yet.
  // forwardRef makes this possible.
  constructor(@Inject(forwardRef(() => Lock)) lock: Lock) { this.lock = lock; }
}

// Only at this point Lock is defined.
class Lock {}
Copyright © frankshi.com 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-06-21 11:21:54

results matching ""

    No results matching ""