How should I make configurable modules in Angular2

后端 未结 1 640
情深已故
情深已故 2020-12-09 22:41

I\'m wondering what would be the best way to set up configurable modules in angular2. In angular1 this was done usually via providers. With them being changed quite a bit, h

相关标签:
1条回答
  • 2020-12-09 23:32

    There are several recipes, which can be used separately or together.

    Configuration service

    Having a service to provide necessary configuration in key/value form is usually desirable.

    There may be more than one configuration service to configure one application entity, e.g. someConfig for generic user-defined configuration, and someDefaultConfig for all default values that are supposed to be possibly changed. For example, someConfig may contain auth credentials that are always user-defined, and someDefaultConfig may contain default hook callbacks, deep settings for auth providers, etc. The most simple way to implement this is to merge configuration objects with Object.assign.

    Mandatory configuration service

    A recipe that requires the user to define configuration service explicitly, it basically uses DI to designate that some module won't work without proper configuration.

    AngularJS

    // third-party module
    // will fail if someConfig wasn't defined by the user
    angular.module('some', []).factory('someService', (someConfig) => { ... })
    
    // user-defined module
    angular.module('app', ['some']).constant('someConfig', { foo: 'foo' });
    

    Angular

    // third-party module
    export const SOME_CONFIG = new InjectionToken('someConfig');
    
    @Injectable
    class SomeService {
      constructor(@Inject(SOME_CONFIG) someConfig) { ... }
    }
    
    @NgModule({ providers: [SomeService] })
    export class SomeModule {}
    
    // user-defined module
    @NgModule({
      imports: [SomeModule],
      providers: [{ provide: SOME_CONFIG, useValue: { foo: 'foo' } }]
    )
    export class AppModule {}
    

    Optional configuration service with overridable empty value

    This is a slight variation of the previous recipe, the only difference is that there is empty default value that won't make the application to fail if configuration service wasn't defined by a user:

    AngularJS

    // third-party module
    angular.module('some', [])
    .constant('someConfig', {})
    ...
    

    Angular

    // third-party module
    @NgModule({ providers: [..., { provide: SOME_CONFIG, useValue: {} }] })
    export class SomeModule {}
    ...
    

    Optional configuration service

    Alternatively, configuration service can be made totally optional for injection.

    AngularJS

    // third-party module
    angular.module('some', []).factory('someService', ($injector) => {
      const someConfig = $injector.has('someConfig') ? $injector.get('someConfig') : {};
      ...
    })
    ...
    

    Angular

    // third-party module
    export const SOME_CONFIG = new InjectionToken('someConfig');
    
    @Injectable
    class SomeService {
      constructor(@Inject(SOME_CONFIG) @Optional() someConfig) {
        this.someConfig = someConfig !== null ? someConfig : {};
        ...
      }
    }
    
    @NgModule({ providers: [SomeService] })
    export class SomeModule {}
    ...
    

    forRoot method

    forRoot static module method is a convention that is followed by Angular router module and numerous third-party modules. As explained in the guide, the method returns an object that implements ModuleWithProviders.

    Basically it gives an opportunity to define module providers dynamically, based on forRoot(...) arguments. This can be considered an alternative to AngularJS config and provider units that don't exist in Angular.

    AngularJS

    // third-party module
    angular.module('some', [])
    .constant('someDefaultConfig', { bar: 'bar' })
    .provider('someService', function (someDefaultConfig) {
      let someMergedConfig;
    
      this.configure = (config) => {
        someMergedConfig = Object.assign({}, someDefaultConfig, config);
      };
      this.$get = ...
    });
    
    // user-defined module
    angular.module('app', ['some']).config((someServiceProvider) => {
      someServiceProvider.configure({ foo: 'foo' });
    });
    

    Angular

    // third-party module
    export const SOME_CONFIG = new InjectionToken('someConfig');
    export const SOME_DEFAULT_CONFIG = new InjectionToken('someDefaultConfig');
    
    @Injectable
    class SomeService {
      constructor(
        @Inject(SOME_CONFIG) someConfig,
        @Inject(SOME_DEFAULT_CONFIG) someDefaultConfig
      ) {
        this.someMergedConfig = Object.assign({}, someDefaultConfig, someConfig);
        ...
      }
    }
    
    @NgModule({ providers: [
      SomeService,
      { provide: SOME_DEFAULT_CONFIG, useValue { bar: 'bar' } }
    ] })
    export class SomeModule {
      static forRoot(config): ModuleWithProviders {
        return {
          ngModule: SomeModule,
          providers: [{ provide: SOME_CONFIG, useValue: config }]
        };
      }
    }
    
    // user-defined module
    @NgModule({ imports: [SomeModule.forRoot({ foo: 'foo' })] })
    export class AppModule {}
    

    APP_INITIALIZER multi-provider

    Angular APP_INITIALIZER multi-provider allows to provide asynchronous initialization routines for the application.

    APP_INITIALIZER shares some similarities with AngularJS config phase. APP_INITIALIZER routines are susceptible to race conditions, similarly to config and run blocks in AngularJS. For instance, Router is available for injection in root component but not in APP_INITIALIZER, due to circular dependency on another APP_INITIALIZER.

    Synchronous initialization routine

    AngularJS

    ...
    // user-defined module
    angular.module('app', ['some']).config((someServiceProvider) => {
      someServiceProvider.configure({ foo: 'foo' });
    });
    

    Angular

    ...
    // user-defined module
    export function someAppInitializer(someService: SomeService) {
      return () => {
        someService.configure({ foo: 'foo' });
      };
    }
    
    @NgModule({
      imports: [SomeModule],
      providers: [{
        provide: APP_INITIALIZER,
        multi: true,
        useFactory: someAppInitializer,
        deps: [SomeService]
      }]
    })
    export class AppModule {}
    

    Asynchronous initialization routine

    Initialization may involve fetching configuration from remote source to configure services; something that is not possible with single AngularJS application. This requires to have another application that initializes and bootstraps main module. This scenario is naturally handled by APP_INITIALIZER.

    AngularJS

    ...
    // user-defined module
    angular.module('app', ['some']);
    
    angular.module('appInitializer', [])
    .factory('initializer', ($document, $http) => {
      return $http.get('data.json')
      .then((result) => result.data)
      .then((data) => {
        $document.ready(() => {
          angular.bootstrap($document.find('body'), ['app', (someServiceProvider) => {
            someServiceProvider.configure(data);
          }]);
        });
      });
    });
    
    angular.injector(['ng', 'appInitializer'])
    .get('initializer')
    .catch((err) => console.error(err));
    

    Angular

    ...
    // user-defined module
    export function someAppInitializer(http: HttpClient, someService: SomeService) {
      return () => {
        return http.get('data.json').toPromise()
        .then(data => {
          someService.configure(data);
        });
      };
    }
    
    @NgModule({
      imports: [SomeModule],
      providers: [{
        provide: APP_INITIALIZER,
        multi: true,
        useFactory: someAppInitializer,
        deps: [HttpClient, SomeService]
      }]
    })
    export class AppModule {}
    
    0 讨论(0)
提交回复
热议问题