个人从源码理解angular项目在JIT模式下的启动过程
通常一个angular项目会有一个个模块(Module)来管理各自的业务,并且必须有一个根模块(AppModule)作为应用的入口模块,整个应用都围绕AppModule展开。可以这么说,AppModule是一个angular项目的起点。
不过单从angular的启动过程来说,AppModule就是其工作的终点。整个angular框架的启动过程都是为了使AppModule可以工作而展开的。本文算是笔者单就阅读angular源码中的启动过程相关部分的总结,angular源码博大精深,有任何笔者理解不够或错误的地方还望包涵并指正。
源码中的一些类与个人的翻译
在Typescript的帮助下angular框架实现了究极抽象,其中有几个固定风格的命名,笔者个人的翻译如下:
- XXX-Injector: 注入器 ( 也就是到处注入的服务实例 )
- XXX-Factory: 工厂 ( 包括了 编译器工厂、平台工厂、模块工厂、组建工厂等,所有这些类都由工厂创建出来 )
- XXX-Ref: 引用 ( 工厂创建出的所有类都是一个引用,通过引用来进行控制 )
启动过程的实现目标
首先不直接查看angular源代码,而是从实际项目的启动代码入手,一般的实际项目通过这样的代码启动:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app';
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
根据涉及的两个方法的返回类型可以看出angular的启动整体来看分为两部曲:
- 创建平台引用 platformBrowserDynamic()
- 使用创建的平台引用进一步创建模块引用 bootstrapModule(AppModule)
接下来就要利用好TypeScript究极智能代码提示到处F12来进入angular的源代码了。
得到平台引用 PlatformRef
首先在 /packages/platform-browser-dynamic/src/platform-browser-dynamic.ts 下找到 platformBrowserDynamic 方法的定义:
// JIT下创建 平台工厂
export const platformBrowserDynamic = createPlatformFactory(
platformCoreDynamic, 'browserDynamic', INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS);
可见 platformBrowserDynamic 本身是一个平台工厂,直接执行平台工厂就可以得到一个平台引用。
根据传入的参数可以得出,创建好的平台名称为 browserDynamic,注入了一个名字很长的服务,并且依赖一个父级工厂叫做 platformCoreDynamic。
进入 INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS 发现有意思的几个注入器如下:
{ // 传入的是一个资源加载器 内含一个get方法用来创建XmlHttpRequest对象请求资源
provide: COMPILER_OPTIONS,
useValue: {providers: [{provide: ResourceLoader, useClass: ResourceLoaderImpl}]},
multi: true
},
{provide: PLATFORM_INITIALIZER, useValue: initDomAdapter, multi: true}, // 初始化DOM适配器 实例化一个类 包含所有DOM操作
{provide: PlatformLocation, useClass: BrowserPlatformLocation}, // 浏览器端定位服务 提供了一些从DOM中得到url、历史等信息的能力
{provide: DOCUMENT, useFactory: _document, deps: []}, // windows 的 document 对象
进入父平台 platformCoreDynamic 的实现:
// 作为根平台工厂的父平台 这个也使用平台工厂创建,但其父平台为 核心平台
export const platformCoreDynamic = createPlatformFactory(platformCore, 'coreDynamic', [
{provide: COMPILER_OPTIONS, useValue: {}, multi: true},
{provide: CompilerFactory, useClass: JitCompilerFactory},
]);
这个平台表面上也没做什么厉害的事情,不就是注入了一个编译选项 COMPILER_OPTIONS,还是空的,以及一个看名字很厉害实际上更厉害的 JitCompilerFactory,直接翻译为JIT编译器工厂,那就是用来生成编译器的东西。
然后继续往下进入爷爷平台 platformCore 的实现:
// 核心平台的服务商 比较厉害的就是 PlatformRef_
const _CORE_PLATFORM_PROVIDERS: Provider[] = [
{ provide: PLATFORM_ID, useValue: 'unknown' }, // 用于设置缺省平台名
PlatformRef_, // 核心模块引用
{ provide: PlatformRef, useExisting: PlatformRef_ }, // 模块引用就是核心模块引用
{ provide: Reflector, useFactory: _reflector, deps: [] }, // Reflect待研究
TestabilityRegistry, // 测试支持待研究
Console, // 日志服务
];
// 核心工厂无父平台 就直接传null 命名为 core
export const platformCore = createPlatformFactory(null, 'core', _CORE_PLATFORM_PROVIDERS);
亲戚关系就到爷爷这里为止了,现在进入 createPlatformFactory 看看平台工厂是怎么创建的(不是创建平台,而是创建工厂):
export function createPlatformFactory(
parentPlatformFactory: ((extraProviders?: Provider[]) => PlatformRef) | null, name: string, // 父级工厂
providers: Provider[] = []
): (extraProviders?: Provider[]) => PlatformRef {
const marker = new InjectionToken(`Platform: ${name}`);
return (extraProviders: Provider[] = []) => {
let platform = getPlatform();
// 保证只能创建一个平台
if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) {
if (parentPlatformFactory) {
// 有父级平台工厂的话使用父级并注入自己包含的注入器
parentPlatformFactory(providers.concat(extraProviders).concat({provide: marker, useValue: true}));
} else {
// 没有父级平台说明是爷爷平台了 那就直接创建平台
createPlatform(ReflectiveInjector.resolveAndCreate(providers.concat(extraProviders).concat({provide: marker, useValue: true})));
}
}
// 确保平台已被创建并包含maker信息并返回创建好的平台
return assertPlatform(marker);
};
}
export function assertPlatform(requiredToken: any): PlatformRef {
const platform = getPlatform();
if (!platform) { // 确保平台被创建
throw new Error('No platform exists!');
}
if (!platform.injector.get(requiredToken, null)) { // 这里是确保 marker 被注入
throw new Error('A platform with a different configuration has been created. Please destroy it first.');
}
return platform;
}
看来核心的创建平台的代码就在 createPlatform 里了:
// 最顶级的平台工厂会执行并创建出平台
export function createPlatform(injector: Injector): PlatformRef {
// 存在平台 && 未销毁 && 不允许多个平台
if (_platform && !_platform.destroyed && !_platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) {
throw new Error('There can be only one platform. Destroy the previous one to create a new one.');
}
// 得到这个平台引用
_platform = injector.get(PlatformRef);
// 得到服务商
const inits = injector.get(PLATFORM_INITIALIZER, null);
// 执行初始化
if (inits) inits.forEach((init: any) => init());
// 返回初始化完成的平台
return _platform;
}
看完 createPlatform,没错angular又继续踢皮球了,回忆创建爷爷平台时注入的那一堆服务中,就有个 PlatformRef,可不就是平台引用吗~
使用创建的平台引用启动根模块
上面的爷爷平台注入了一个平台引用,其实现是 PlatformRef_,其提供了启动模块的一些方法,将在两部曲的第二步中用到。
首先径直在 /packages/core/src/application_ref.ts 下的 PlatformRef_ 中找到 bootstrapModule 方法:
// 启动根模块就是调用这个
bootstrapModule<M>(
moduleType: Type<M>, // 模块类
compilerOptions: CompilerOptions|CompilerOptions[] = [] // 编译选项
): Promise<NgModuleRef<M>> {
return this._bootstrapModuleWithZone(moduleType, compilerOptions);
}
private _bootstrapModuleWithZone<M>(
moduleType: Type<M>, compilerOptions: CompilerOptions|CompilerOptions[] = [],
ngZone?: NgZone
): Promise<NgModuleRef<M>> {
const compilerFactory: CompilerFactory = this.injector.get(CompilerFactory); // 从注入器中得到编译器工厂
const compiler = compilerFactory.createCompiler( // 创建出编译器 传入的是编译选项 会创建一堆注入器 包括编译器 并会将编译器作为返回
Array.isArray(compilerOptions) ? compilerOptions : [compilerOptions]
);
return compiler.compileModuleAsync(moduleType) // 编译模块
.then((moduleFactory) => this._bootstrapModuleFactoryWithZone(moduleFactory, ngZone)); // 启动模块工厂并加入NgZone
}
里面涉及到了几个步骤:
- 从注入器中取出前面在爸爸平台注入的 CompilerFactory
- 使用 CompilerFactory 创建出编译器并传入编译选项(可以为空)
- 使用编译器异步编译传入的根模块,并在回调中执行一个名字很长的方法 _bootstrapModuleFactoryWithZone
其中 compileModuleAsync 即编译模块的细节涉及到了许多其他的东西,限于篇幅本文暂且不去解读它。
现在最后剩下了一个 _bootstrapModuleFactoryWithZone 方法。此方法做的事情主要是使用传入的模块工厂(由异步编译模块得到)创建出最终的模块来,并为其注入一个新建的NgZone实例:
private _bootstrapModuleFactoryWithZone<M>(moduleFactory: NgModuleFactory<M>, ngZone?: NgZone):
Promise<NgModuleRef<M>> {
// 创建新的NgZone实例
if (!ngZone) ngZone = new NgZone({enableLongStackTrace: isDevMode()});
return ngZone.run(() => {
const ngZoneInjector =
ReflectiveInjector.resolveAndCreate([{provide: NgZone, useValue: ngZone}], this.injector);
// 创建模块引用(注入上面的注入器) 注入的服务商包括 NgZone 和 传入的其他注入器
const moduleRef = <InternalNgModuleRef<M>>moduleFactory.create(ngZoneInjector);
const exceptionHandler: ErrorHandler = moduleRef.injector.get(ErrorHandler, null);
if (!exceptionHandler) {
throw new Error('No ErrorHandler. Is platform module (BrowserModule) included?');
}
moduleRef.onDestroy(() => remove(this._modules, moduleRef));
ngZone !.runOutsideAngular(() => ngZone !.onError.subscribe({next: (error: any) => {
exceptionHandler.handleError(error);
}}));
return _callAndReportToErrorHandler(exceptionHandler, ngZone !, () => {
const initStatus: ApplicationInitStatus = moduleRef.injector.get(ApplicationInitStatus);
// 执行初始化 细节待研究
initStatus.runInitializers();
return initStatus.donePromise.then(() => {
this._moduleDoBootstrap(moduleRef);
return moduleRef;
});
});
});
}
收回前面最后这两个字,现在皮球又踢给了 _moduleDoBootstrap 方法:
private _moduleDoBootstrap(moduleRef: InternalNgModuleRef<any>): void {
// 得到app引用
const appRef = moduleRef.injector.get(ApplicationRef) as ApplicationRef;
if (moduleRef._bootstrapComponents.length > 0) {
// 启动所有启动组件 至于启动组件哪里来待研究
moduleRef._bootstrapComponents.forEach(f => appRef.bootstrap(f));
} else if (moduleRef.instance.ngDoBootstrap) {
// 属于手动执行ngDoBootstrap方法的情况
moduleRef.instance.ngDoBootstrap(appRef);
} else {
// 既没有启动组件又没有手动执行启动,则抛出错误
throw new Error(
`The module ${stringify(moduleRef.instance.constructor)} was bootstrapped, but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. ` +
`Please define one of these.`);
}
// 现在模块也启动好了 连启动组件都初始化好了 把这个模块push到平台的模块列表中吧
this._modules.push(moduleRef);
}
这里是不是有点恍然大悟,模块启动好后,此方法中进一步操作了启动组件,也就是我们在AppModule中都要配置给bootstrap的入口组件:
bootstrap: [ AppComponent ]
至此模块引用也彻底创建好了,也就是说angular项目终于是启动成功了,当然其中模块以及组件编译过程还深不可测,值得细细研究。
总结
- 回顾angular项目的启动,分为平台的创建和模块的创建两步
- 可以认为平台就是一个服务,平台的创建做的事情就是创建一个对象,一个注入了一大堆服务的对象
- 一个angular应用只能有一个平台,或者说此平台被视为angular应用本身,由此平台来编译模块,管理服务等
- 模块的创建由于涉及到很多编译方面的内容本文还来不及去深入,不过已知的信息是:
- 模块由平台使用其注入的编译器工厂生产出一个编译器进行编译得到
- 完成模块的编译后要进一步启动模块,启动的方式是得到其应用引用(ApplicationRef)来启动其启动组件(AppComponent)
个人从源码理解angular项目在JIT模式下的启动过程的更多相关文章
- Apache Spark源码走读之15 -- Standalone部署模式下的容错性分析
欢迎转载,转载请注明出处,徽沪一郎. 概要 本文就standalone部署方式下的容错性问题做比较细致的分析,主要回答standalone部署方式下的包含哪些主要节点,当某一类节点出现问题时,系统是如 ...
- Apache Spark源码走读之19 -- standalone cluster模式下资源的申请与释放
欢迎转载,转载请注明出处,徽沪一郎. 概要 本文主要讲述在standalone cluster部署模式下,Spark Application在整个运行期间,资源(主要是cpu core和内存)的申请与 ...
- 基于SpringBoot的Environment源码理解实现分散配置
前提 org.springframework.core.env.Environment是当前应用运行环境的公开接口,主要包括应用程序运行环境的两个关键方面:配置文件(profiles)和属性.Envi ...
- AdvanceEast源码理解
目录 文章思路 源码理解 一. 标签点形式 按顺序排列四个点,逆时针旋转,且第一个点为左上角点(刚开始选择最左边的点, 二. 标签切边 三. loss计算 四. NMS 最后说明 文章思路 大神的gi ...
- IdentityServer4源码解析_1_项目结构
目录 IdentityServer4源码解析_1_项目结构 IdentityServer4源码解析_2_元数据接口 IdentityServer4源码解析_3_认证接口 IdentityServer4 ...
- 深入源码理解Spring整合MyBatis原理
写在前面 聊一聊MyBatis的核心概念.Spring相关的核心内容,主要结合源码理解Spring是如何整合MyBatis的.(结合右侧目录了解吧) MyBatis相关核心概念粗略回顾 SqlSess ...
- Caffe源码理解2:SyncedMemory CPU和GPU间的数据同步
目录 写在前面 成员变量的含义及作用 构造与析构 内存同步管理 参考 博客:blog.shinelee.me | 博客园 | CSDN 写在前面 在Caffe源码理解1中介绍了Blob类,其中的数据成 ...
- jedis的源码理解-基础篇
[jedis的源码理解-基础篇][http://my.oschina.net/u/944165/blog/127998] (关注实现关键功能的类) 基于jedis 2.2.0-SNAPSHOT ...
- VUEJS2.0源码理解--优
VUEJS2.0源码理解 http://jiongks.name/blog/vue-code-review/#pingback-112428
随机推荐
- 003-0.6632是float/Float/double/Double中的哪个?
应该是float,最后两个是包装类,这里应该安装基本类型去看待. 而java的浮点型默认是double型,如果希望生成一个float型的浮点数则需要在这个值的后面紧跟f和F.
- 拨开字符编码的迷雾--MySQL数据库字符编码
拨开字符编码迷雾系列文章链接: 拨开字符编码的迷雾--字符编码概述 拨开字符编码的迷雾--编译器如何处理文件编码 拨开字符编码的迷雾--字符编码转换 拨开字符编码的迷雾--MySQL数据库字符编码 1 ...
- 基于dijkstra算法求地铁站最短路径以及打印出所有的路径
拓展dijkstra算法,实现利用vector存储多条路径: #include <iostream> #include <vector> #include <stack& ...
- jQuery与js的length属性
js:length 属性可返回字符串中的字符数目. stringObject.length jQuery:length 属性包含 jQuery 对象中元素的数目. $(selector).length ...
- Json作为配置文件注意事项
错误描述 在一次开发中,使用了JSON数据作为配置文件,但反序列化时总是出错,开始还以为是转义字符的问题,因为存了一个绝对路径(D:\xx\xx.json),后来测试发现竟然发现是类嵌套的问题. 解决 ...
- 汇编指令-MRS(读)和MSR(写)指令操作CPSR寄存器和SPSR寄存器使用(1)
1.MSR和MRS指令介绍 MRS 指令: 对状态寄存器CPSR和SPSR进行读操作.通过读CPSR可以获得当前处理器的工作状态.读SPSR寄存器可以获得进入异常前的处理器状态(因为只有异常模式下有 ...
- Python终端输出打印彩色字体的方法
一 实现过程 终端的字符颜色是用转义序列控制的,是文本模式下的系统显示功能,和具体的语言无关. 转义序列是以ESC开头,即用\033来完成(ESC的ASCII码用十进制表示是27,用八进制表示就是0 ...
- hdu 3062 2-Sat入门
开始学习2-Sat,前面看了对称性解决2-sat的ppt,很有帮助. 题意:n对夫妻,夫妻需要出席一人,给出不相容的关系,求每对是否能完成出席方案. 思路:通过关系建图,Tarjan缩点,然后进行判断 ...
- MongoDB学习之路(三)
数据库 一个MongoDB可以建立多个数据库. MongoDB的默认数据库为"db",该数据库存储在data目录中. MongoDB的单个实例可以容纳多个独立的数据库,每一个都有自 ...
- Beta版本冲刺前期计划及安排
a. 介绍小组新加入的成员,Ta担任的角色. 吴东益:经讨论决定,Ta担任角色为开发人员 李志霖:在原先的团队中负责前端开发与界面设计.现经讨论决定,Ta此次担任角色为开发人员 由他们替代原来成员陈雄 ...