qrcode

马上订阅,开启修仙之旅

Angular Multi Providers 和 APP_INITIALIZER

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

multi provider 的使用

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

1
2
3
4
5
6
7
8
9
10
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 时,会出现什么问题 ?

1
2
3
4
5
6
7
8
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 的定义如下:

1
2
3
4
/**
* 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 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 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 循环来遍历系统定义的初始化函数:

1
2
3
4
5
6
7
8
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 会等待所有的异步任务都执行完成才认为初始化完成:

1
2
3
4
5
6
7
 Promise.all(asyncInitPromises).then(() => { complete(); })
.catch(e => { this.reject(e); });

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

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

APP_INITIALIZER 实战

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

1
2
3
4
5
6
7
8
9
10
11
12
13
{
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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 的内部执行过程,感兴趣的同学,可以进一步研究一下。


欢迎小伙伴们订阅前端修仙之路,及时阅读 Angular、RxJS、TypeScript 和 Node.js 最新文章。

qrcode