Angular Multi Providers 和 APP_INITIALIZER

2019-11-05 15:49:40 浏览数 (1)

有些时候,我们希望在 Angular 应用程序启动的时候,执行一些初始化操作。针对这种场景,我们可以利用 APP_INITIALIZER 这个内置的 Token 来定义 multi provider,从而实现自定义系统初始化的逻辑。不过在介绍 APP_INITIALIZER 之前,我们先来介绍一下 multi provider 的相关知识。

multi provider 的使用

Multi provider 让我们可以使用相同的 Token 去注册多个 Provider ,具体如下:

代码语言:javascript复制
const SOME_TOKEN: InjectionToken<Array<string>> = new InjectionToken(
  "SomeToken"
);

const injector = Injector.create([
  { provide: SOME_TOKEN, useValue: "dependency one", multi: true },
  { provide: SOME_TOKEN, useValue: "dependency two", multi: true }
]);
const dependencies = injector.get(SOME_TOKEN);
console.log(dependencies); // ['dependency one', 'dependency two']

上面例子中,我们使用 multi: true 告诉 Angular 的依赖注入系统,我们设置的 provider 是 multi provider。正如之前所说,我们可以使用相同的 token 值,注册不同的 provider。当我们使用对应的 token 去获取依赖项时,我们获取的是已注册的依赖对象列表。

multi provider 的作用

首先我们先来分析一下,若没有设置 multi: true 属性时,使用同一个 token 注册 provider 时,会出现什么问题 ?

代码语言:javascript复制
class Engine { }
class TurboEngine { }

const injector = Injector.create([
      { provide: Engine, useClass: Engine, deps: [] },
      { provide: Engine, useValue: TurboEngine, deps: [] }
]);
const engine = injector.get(Engine); // engine instanceof TurboEngine -> true

这说明如果使用同一个 token 注册 provider,后面注册的 provider 将会覆盖前面已注册的 provider。此外,Angular 使用 multi provider 的这种机制,为我们提供可插拔的钩子(pluggable hooks) 。另外需要注意的是,multi provider 是不能和普通的 provider 混用

APP_INITIALIZER 简介

APP_INITIALIZER 的定义如下:

代码语言:javascript复制
/**
 * A function that will be executed when an application is initialized.
 */
export const APP_INITIALIZER = new InjectionToken<Array<() => void>>('Application   Initializer');

通过以上的定义,我们知道 APP_INITIALIZER Token 所对应的依赖对象是数组对象,数组中保存的元素是函数对象。这表示我们可以定义多个初始化逻辑,那么现在问题来了,我们自定义的初始化逻辑是什么被运行呢?

在 Angular 内部定义了一个 ApplicationInitStatus 类:

代码语言:javascript复制
// packages/core/src/application_init.ts
@Injectable()
export class ApplicationInitStatus {
  // TODO(issue/24571): remove '!'.
  private resolve !: Function;
  // TODO(issue/24571): remove '!'.
  private reject !: Function;
  private initialized = false;
  public readonly donePromise: Promise<any>;
  public readonly done = false;

  constructor(@Inject(APP_INITIALIZER) @Optional() private appInits: (() => any)[]) {
    this.donePromise = new Promise((res, rej) => {
      this.resolve = res;
      this.reject = rej;
    });
  }

  /** @internal */
  runInitializers() {
    if (this.initialized) {
      return;
    }

    const asyncInitPromises: Promise<any>[] = [];

    const complete = () => {
      (this as{done: boolean}).done = true;
      this.resolve();
    };

    if (this.appInits) {
      for (let i = 0; i < this.appInits.length; i  ) {
        const initResult = this.appInits[i]();
        if (isPromise(initResult)) {
          asyncInitPromises.push(initResult);
        }
      }
    }

    Promise.all(asyncInitPromises).then(() => { complete(); })
        .catch(e => { this.reject(e); });

    if (asyncInitPromises.length === 0) {
      complete();
    }
    this.initialized = true;
  }
}

在 ApplicationInitStatus 类内部,通过 Angular 的 DI 机制注入了 APP_INITIALIZER 对应的依赖对象。此外在该类内部定义了一个 runInitializers() 方法,因为 APP_INITIALIZER 对应的依赖对象类型是 Array<() => void> ,所以在 runInitializers() 方法内部,通过 for 循环来遍历系统定义的初始化函数:

代码语言:javascript复制
if (this.appInits) {
   for (let i = 0; i < this.appInits.length; i  ) {
     const initResult = this.appInits[i]();
       if (isPromise(initResult)) {
          asyncInitPromises.push(initResult);
       }
   }
}

通过以上代码可知,当我们定义的初始化函数执行后返回的是一个 Promise 对象时,它会被保存到 asyncInitPromises: Promise<any>[] 数组对象中,此后 Angular 会等待所有的异步任务都执行完成才认为初始化完成:

代码语言:javascript复制
 Promise.all(asyncInitPromises).then(() => { complete(); })
     .catch(e => { this.reject(e); });

if (asyncInitPromises.length === 0) {
   complete();
}
this.initialized = true;

介绍完 APP_INITIALIZER,接下来我们来介绍如何使用它。

APP_INITIALIZER 实战

这里我们来自定义一个初始化函数,该函数会让应用的启动时间过程延迟 2 s:

代码语言:javascript复制
{
  provide: APP_INITIALIZER,
  useFactory: () => {
    return () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve();
        },  2000);
      });
    }
  },
  multi: true
}

需要注意的事,上面的示例只是为了演示需要。在工作中使用的是 Ionic 框架,在框架内部也是通过 APP_INITIALIZER 定义 multi provider 实现自定义初始化操作,眼见为实(Ionic 4.0.0 beta3):

代码语言:javascript复制
// ionic-4.0.0-beta.3/angular/src/ionic-module.ts
@NgModule({
  declarations: DECLARATIONS,
  exports: DECLARATIONS,
  providers: [p.AngularDelegate, p.ModalController, p.PopoverController],
  imports: [CommonModule]
})
export class IonicModule {
  static forRoot(config?: IonicConfig): ModuleWithProviders {
    return {
      ngModule: IonicModule,
      providers: [
        {
          provide: APP_INITIALIZER,
          useFactory: appInitialize,
          multi: true,
          deps: [
            ConfigToken
          ]
        },
        ...PROVIDERS
      ]
    };
  }
}

最后我们来看一下 appInitialize 函数:

代码语言:javascript复制
export function appInitialize(config: Config) {
  return () => {
    const win: IonicWindow = window as any;
    if (typeof win !== 'undefined') {
      const Ionic = win.Ionic = win.Ionic || {};

      Ionic.config = config;

      Ionic.ael = (elm, eventName, cb, opts) => {
        if (elm.__zone_symbol__addEventListener) {
          elm.__zone_symbol__addEventListener(eventName, cb, opts);
        } else {
          elm.addEventListener(eventName, cb, opts);
        }
      };

      Ionic.rel = (elm, eventName, cb, opts) => {
        if (elm.__zone_symbol__removeEventListener) {
          elm.__zone_symbol__removeEventListener(eventName, cb, opts);
        } else {
          elm.removeEventListener(eventName, cb, opts);
        }
      };

      Ionic.raf = (cb: any) => {
        if (win.__zone_symbol__requestAnimationFrame) {
          win.__zone_symbol__requestAnimationFrame(cb);
        } else {
          win.requestAnimationFrame(cb);
        }
      };

      // define all of Ionic's custom elements
      defineCustomElements(win);
    }
  };
}

在 appInitialize() 方法内部,主要执行以下的操作:

  • 设置全局的 Ionic 对象及初始化 Ionic 对象内部的 config 属性;
  • 定义ael(addEventListener)、rel(removeEventListener)、raf(requestAnimationFrame)方法;
  • 定义 Ionic 内部所有的自定义元素。

总结

本文首先介绍了 multi provider 的使用和作用,然后介绍了如何利用 APP_INITIALIZER 这个内置的 Token 来定义 multi provider,从而实现自定义系统初始化的逻辑。此外,还通过 Angular 源码分析了 APP_INITIALIZER 的内部执行过程,感兴趣的同学,可以进一步研究一下。

0 人点赞