Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何解决 Angular 2 中 Token 命名冲突 #3

Open
semlinker opened this issue Mar 3, 2017 · 0 comments
Open

如何解决 Angular 2 中 Token 命名冲突 #3

semlinker opened this issue Mar 3, 2017 · 0 comments

Comments

@semlinker
Copy link
Owner

字符串Token VS Type类型Token

在 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。

参考资源
opaque-tokens-in-angular-2

@semlinker semlinker changed the title 如何解决 Angular 2 中 Provider 命名冲突 如何解决 Angular 2 中 Token 命名冲突 Mar 5, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant