Avoiding Injection Collisions: OpaqueToken

Since Angular allows the use of tokens as identifiers to its dependency injection system, one of the potential issues is using the same token to represent different entities. If, for example, the string 'token' is used to inject an entity, it's possible that something totally unrelated also uses 'token' to inject a different entity. When it comes time for Angular to resolve one of these entities, it might be resolving the wrong one. This behavior might happen rarely or be easy to resolve when it happens within a small team - but when it comes to multiple teams working separately on the same codebase or 3rd party modules from different sources are integrated these collisions become a bigger issue.

Consider this example where the main application is a consumer of two modules: one that provides an email service and another that provides a logging service.

app/email/email.service.ts

  1. export const apiConfig = 'api-config';
  2. @Injectable()
  3. export class EmailService {
  4. constructor(@Inject(apiConfig) public apiConfig) { }
  5. }

app/email/email.module.ts

  1. @NgModule({
  2. providers: [ EmailService ],
  3. })
  4. export class EmailModule { }

The email service api requires some configuration settings, identified by the string api-config, to be provided by the DI system. This module should be flexible enough so that it can be used by different modules in different applications. This means that those settings should be determined by the application characteristics and therefore provided by the AppModule where the EmailModule is imported.

app/logger/logger.service.ts

  1. export const apiConfig = 'api-config';
  2. @Injectable()
  3. export class LoggerService {
  4. constructor(@Inject(apiConfig) public apiConfig) { }
  5. }

app/logger/logger.module.ts

  1. @NgModule({
  2. providers: [ LoggerService ],
  3. })
  4. export class LoggerModule { }

The other service, LoggerModule, was created by a different team than the one that created EmailModule, and it that also requires a configuration object. Not surprisingly, they decided to use the same token for their configuration object, the string api-config. In an effort to avoid a collision between the two tokens with the same name, we could try to rename the imports as shown below. In an effort to avoid a collision between the two tokens with the same name, we could try to rename the imports as shown below.

app/app.module.ts

  1. import { apiConfig as emailApiConfig } from './email/index';
  2. import { apiConfig as loggerApiConfig } from './logger/index';
  3. @NgModule({
  4. ...
  5. providers: [
  6. { provide: emailApiConfig, useValue: { apiKey: 'email-key', context: 'registration' } },
  7. { provide: loggerApiConfig, useValue: { apiKey: 'logger-key' } },
  8. ],
  9. ...
  10. })
  11. export class AppModule { }

View Example

When the application runs, it encounters a collision problem resulting in both modules getting the same value for their configuration, in this case { apiKey: 'logger-key' }. When it comes time for the main application to specify those settings, Angular overwrites the first emailApiConfig value with the loggerApiConfig value, since that was provided last. In this case, module implementation details are leaking out to the parent module. Not only that, those details were obfuscated through the module exports and this can lead to problematic debugging. This is where Angular's OpaqueToken comes into play.

OpaqueToken

OpaqueTokens are unique and immutable values which allow developers to avoid collisions of dependency injection token ids.

  1. import { OpaqueToken } from '@angular/core';
  2. const name = 'token';
  3. const token1 = new OpaqueToken(name);
  4. const token2 = new OpaqueToken(name);
  5. console.log(token1 === token2); // false

Here, regardless of whether or not the same value is passed to the constructor of the token, it will not result in identical symbols.

app/email/email.module.ts

  1. export const apiConfig = new OpaqueToken('api-config');
  2. @Injectable()
  3. export class EmailService {
  4. constructor(@Inject(apiConfig) public apiConfig: EmailConfig) { }
  5. }
  1. export const apiConfig = new OpaqueToken('api-config');
  2. @Injectable()
  3. export class LoggerService {
  4. constructor(@Inject(apiConfig) public apiConfig: LoggerConfig) { }
  5. }

View Example

After turning the identifying tokens into OpaqueTokens without changing anything else, the collision is avoided. Every service gets the correct configuration object from the root module and Angular is now able to differentiate two tokens that uses the same string.

原文: https://angular-2-training-book.rangle.io/handout/di/angular2/avoiding_collisions_opaque_token.html