Angular 2 OpaqueToken & InjectionToken

7

在 Angular 2 中,provider 的 token 的类型可以是字符串或 Type 类型。我们可以根据实际应用场景,选用不同的类型。假设我们有一个服务类 DataService,并且我们想要在组件中注入该类的实例,我们可以这样使用:

@Component({
  selector: 'my-component',
  providers: [
    { provide: DataService, useClass: DataService }
  ]
})
class MyComponent {
  constructor(private dataService: DataService) { }
}

Type 类型

// Type类型 - @angular/core/src/type.ts
export const Type = Function;

export function isType(v: any): v is Type<any> {
  return typeof v === 'function';
}

export interface Type<T> extends Function { new (...args: any[]): T; }

这是非常酷炫的事情,只要我们知道依赖对象的类型,我们就可以方便地注入对应类型的实例对象。但是有时候,我们需要注入的是普通的JavaScript对象,而不是Type 类型的对象。比如,我们需要注入一个config对象:

const CONFIG = {
  apiUrl: 'http://my.api.com',
  theme: 'suicid-squad',
  title: 'My awesome app'
};

有时候我们需要注入一个原始数据类型的数值,如字符串或布尔值:

const FEATURE_ENABLED = true;

在这种情况下,我们是不能使用 String 或 Boolean 类型,因为如果使用这些类型,我们只能获得类型对应的默认值。想解决这个问题,但我们又不想引入一种新的类型来表示原始数据类型的值。这时我们可以考虑使用字符串作为 token,而不用引入新的类型:

let featureEnabledToken = 'featureEnable';
let configToken = 'config';

providers: [
  { provide: featureEnabledToken, useValue: FEATURE_ENABLED },
  { provide: configToken, useValue: CONFIG }
]

使用字符串作为 token 设置完 providers 后,我们就可以使用 @Inject 装饰器注入相应依赖:

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

class MyComponent {
  constructor(
      @Inject(featureEnabledToken) private featureEnabled,
    @Inject(configToken) private config
  )
}

使用字符串作为 Token 存在的问题

让我们回顾一下之前的例子,config 是一个很通用的名字,这样的话就可能在项目中留下隐患。因为若在项目中也存在同样名称的 provider,那么后面声明的 provider 将会覆盖之前声明的 provider。

假设在项目中,我们引入了第三方脚本库。该库的 provides 的配置信息如下:

export const THIRDPARTYLIBPROVIDERS = [
  { provide: 'config', useClass: ThirdParyConfig }
];

实际使用时,我们可能这样做:

import THIRDPARTYLIBPROVIDERS from './third-party-lib';

providers = [
  DataService,
  THIRDPARTYLIBPROVIDERS
];

到目前为止,一切都能正常工作。但我们是不知道 THIRDPARTYLIBPROVIDERS 内部的具体情况,除非我们已经阅读了第三方库的官方文档或源码。在未知的情况下,我们可能这样使用:

providers = [
  DataService,
  THIRDPARTYLIBPROVIDERS,
  { provide: configToken, useValue: CONFIG }
];

此时第三方库就不能正常工作了。因为它获取不到它所依赖的配置对象,因为它被我们自定义的 provider 替换了。

救世主 - OpaqueToken

为了解决上述问题,Angular 2 引入了 OpaqueToken,它允许我们创建基于字符串的 Token 类。创建 OpaqueToken 对象很简单,只需导入 Opaque 类。这样的话,上面提到的第三方类库,可以调整为:

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

const CONFIG_TOKEN = new OpaqueToken('config');

export const THIRDPARTYLIBPROVIDERS = [
  { provide: CONFIG_TOKEN, useClass: ThirdPartyConfig }
];

而之前提到的冲突问题,也可以按照下面的方式解决。

import { OpaqueToken } from '@angular/core';
import THIRDPARTYLIBPROVIDERS from './third-party-lib';

const MY_CONFIG_TOKEN = new OpaqueToken('config');

providers = [
  DataService,
  THIRDPARTYLIBPROVIDERS,
  { provide: MY_CONFIG_TOKEN, useValue: CONFIG }
]

OpaqueToken 的工作原理

// OpaqueToken - @angular/core/src/di/injection_token.ts
export class OpaqueToken {
  constructor(protected _desc: string) {}
  toString(): string { return `Token ${this._desc};` }
}

通过查看 OpaqueToken 类,我们可以发现,尽管是使用相同的字符串去创建 OpaqueToken 实例对象,但每次都是返回一个新的实例,从而保证了全局的唯一性。

const TOKEN_A = new OpaqueToken('token');
const TOKEN_B = new OpaqueToken('token');

TOKEN_A === TOKEN_B // false

救世主 - OpaqueToken 不给力了

让我们看一下示例中 DataService Provider 配置信息:

const API_URL = new OpaqueToken('apiUrl');

providers: [
  {
    provide: DataService,
    useFactory: (http, apiUrl) => {
      // create data service
    },
    deps: [
      Http,
      new Inject(API_URL)
    ]
  }
]

我们使用工厂函数创建 DataService 实例,DataService 依赖 http 和 apiUrl 对象,为了让 Angular 能够准确地注入依赖对象,我们使用 deps 属性声明依赖对象的类型。因为 Http 是 Type 类型的 Token,我们只需直接声明。但 API_URL 是 OpaqueToken 类的实例,不属于 Type 类型。因此我们需要使用 new Inject(API_URL) 方式声明依赖对象。(备注:new Inject()与在构造函数中使用 @Inject() 的方式声明依赖对象是等价的)。

上面的代码能够正常运行,但在实际开发过程中,开发者很容易忘记调用 new Inject()。为了解决这个问题,Angular 团队引入了 InjectionToken。

新救世主 - Angular 4.x InjectionToken

// InjectionToken - @angular/core/src/di/injection_token.ts

/**
* InjectionToken 继承于 OpaqueToken,同时支持泛型,用于描述依赖对象的类型
*
*/
export class InjectionToken<T> extends OpaqueToken {
  private _differentiate_from_OpaqueToken_structurally: any;
  constructor(desc: string) { super(desc);  }
  
  toString(): string { 
     return `InjectionToken ${this._desc};` 
  }
}

使用 InjectionToken 重写上面的示例:

// InjectionToken<T> - 使用泛型描述该Token所关联的依赖对象的类型
const API_URL = new InjectionToken<string>('apiUrl'); 

providers: [
  {
    provide: DataService,
    useFactory: (http, apiUrl) => {
      // create data service
    },
    deps: [
      Http,
      API_URL // no `new Inject()` needed!
    ]
  }
]

总结

我们可以通过 OpaqueToken 避免定义 Provider 时,出现 Token 命名冲突的问题。除此之外,使用 OpaqueToken 也为我们提供更好的异常信息。但如果我们使用的 Angular 4.x 以上的版本,我们最好使用 InjectionToken 替换原有的 OpaqueToken。

参考资源


如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

xaclincoln · 2017年04月02日

关于Type<T>类型,是不是只要T包括一个构造函数,Typescript会自动转化为对应的Type类型。例如
injector.get(AppModule) //此时AppModule为被自动识别为Type<AppModule>,这样Injector.get会返回一个强类型的实例,而不是一个any.不知道是否正确?

回复

semlinker 作者 · 2017年04月03日

@Injectable()
class Engine { }

@Injectable()
class Car {
constructor(public engine: Engine) { }
}

var injector = ReflectiveInjector.resolveAndCreate([Car, Engine]);
var car = injector.get(Car);
expect(car instanceof Car).toBe(true);
expect(car.engine instanceof Engine).toBe(true);

回复

semlinker 作者 · 2017年04月03日

export interface Type<T> extends Function { new (...args: any[]): T; }
=> Type<T> 是 TypeScript 中的泛型,在 @Output 装饰器中最常用的 EventEmitter,在创建 EventEmitter 实例的时候,需要定义 @Output() change: EventEmitter<number> = new EventEmitter<number>(),表示该 Observable 对象发出的值的类型都是数值类型,如果你设置其它值的话,就会抛出异常。
EventEmitter 的函数签名如下:
export class EventEmitter<T> extends Subject<T>

回复

ycpaladin · 2017年11月29日

我照上文中最后一个示例敲了一下,出现错误。环境是angular5。

Code:

const API_URL = new InjectionToken<string>('http://cnodejs.org');
@Component({
    selector: 'app-demo02',
    templateUrl: './demo02.component.html',
    styleUrls: ['./demo02.component.css'],
    providers: [
        {
            provide: DataService,
            useFactory(api) {
                console.log('api_url=>', api);
                return new DataService([
                    { name: 'kevin', age: 28 },
                    { name: 'ccw', age: 2 },
                    { name: 'lxy', age: 22 }
                ]);
            },
            deps: [API_URL]
        }
    ]
})

错误:

Error: StaticInjectorError[InjectionToken http://cnodejs.org]: 
  StaticInjectorError[InjectionToken http://cnodejs.org]: 
    NullInjectorError: No provider for InjectionToken http://cnodejs.org!
    at _NullInjector.get (core.js:923)
    at resolveToken (core.js:1211)
    at tryResolveToken (core.js:1153)
    at StaticInjector.get (core.js:1024)
    at resolveToken (core.js:1211)
    at tryResolveToken (core.js:1153)
    at StaticInjector.get (core.js:1024)
    at resolveNgModuleDep (core.js:10585)
    at NgModuleRef_.get (core.js:11806)
    at resolveDep (core.js:12302)

回复

载入中...