ionic3.x LazyLDoad 实现方法的探究

什么是 Lazy Load

LazyLoad 就是懒加载,Angular2 对懒加载提供了很好的支持,只需要在根模块的路由上写一个

1
2
3
4
// app.module.ts
export const ROUTES: Routes = [
{ path: 'reports', loadChildren: '../reports/reports.module#ReportsModule' }
];

只要提供 loadChildren 字段应用初始化的时候就不会加载 report 这个模块,而是会在用到它的时候才加载它,这会缩减应用初始化时需要加载的文件大小,以此来提升应用的启动速度。

LazyLoad Page 在 ionic3.x 中的用法

step1:

为需要懒加载的组件添加 IonicPage 装饰器,例如这个 HomePage 组件

1
2
3
4
5
6
// home-page.ts
import { Component } from '@angular/core';
import { IonicPage } from 'ionic-angular';
@IonicPage()
@Component(... )
export class HomePage { ... }

step2:

为这个组件添加一个 IonicPageModule

1
2
3
4
5
6
7
8
9
// home-page.module.ts
import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { HomePage } from './home';
@NgModule({
declarations: [HomePage],
imports: [IonicPageModule.forChild(HomePage)],
})
export class HomePageModule { }

Angular 4.x Lazy Load 的实现方式

1. 使用 SystemJS 实现(不使用 webpack 的方式)

1
2
3
4
5
6
7
8
// packages/core/src/linker/system_js_ng_module_factory_loader.ts
private loadAndCompile(path: string): Promise<NgModuleFactory<any>> {
// ...
return System.import(module) // 采用 System.import 来加载
.then((module: any) => module[exportName])
.then((type: any) => checkNotEmpty(type, module, exportName))
.then((type: any) => this._compiler.compileModuleAsync(type));
}

如果应用是基于 SystemJS 而不是使用 angular-cli 来构建的,这里直接调用 System.import 来异步加载所需要的模块。

2. 使用 @ngtools/webpack 实现

webpack 其实是支持把 System.import() 作为一个代码切分点,但仅支持某些动态情景(如 System.import('routes/' + module + '.js')),webpack会根据 'routes/' + module + '.js' 里的 routes/.js 等静态信息推测所有情景,并生成对应的 context module 负责加载这些动态模块。不过对于纯表达式(如 System.import(module)),没有任何已知信息,webpack 就没有办法切分代码了,而这也就是我们遇到的情况。既然 webpack 自身无法预处理,那就是由开发者来告诉 webpack 如何处理,这就是 @ngtools/webpack 的处理思路,它同过分析 loadChildren 的值,抽取需要懒加载的模块信息,打包这些模块,并生成对应的 context module,建立映射对象,映射对象大概是下面这个样子:

1
2
3
4
5
6
var map = {
"./lazy/lazy.module": [
267, // module id
0 // index
]
};

动将这些文件打包成单独的代码块,并用即使执行函数对它们进行包装,System.import 会被webpack 替换为 __webpack_require__ 方法。在运行时通过这个方法来加载所需要的模块。

Ionic 3.x 基于 ionic-cli 的实现方式

deep link system 是一个用于在 ionic App 中进行导航的系统,他会维护 app 内部的页面的名称和组件的对应关系,

首先创建一个 NgModule

1
2
3
4
5
6
7
8
9
10
11
12
13
// my-page.module.ts
@NgModule({
declarations: [
MyPage
],
imports: [
IonicPageModule.forChild(MyPage)
],
entryComponents: [
MyPage
]
})
export class MyPageModule {}

然后为页面组件添加 @ionicPage 装饰器,

1
2
3
4
5
6
// my-page.ts
@IonicPage()
@Component({
templateUrl: 'main.html'
})
export class MyPage {}

ionic-cli 会自动为这个页面创建一个 deep link,链接的名称默认与组件名称一致,我们在 App 中可以使用 ‘MyPage’ 字符串进行页面导航。

1
2
3
4
5
6
7
8
9
10
11
@Component({
templateUrl: 'another-page.html'
})
export class AnotherPage {
constructor(public navCtrl: NavController) {}

goToMyPage() {
// go to the MyPage component
this.navCtrl.push('MyPage'); // use a string
}
}

懒加载正是基于这种方式实现的,下面来说一下过程

构建过程

  1. 遍历所有的 ts 文件

  2. 解析 ts 文件的语法树,获取语法树中的 class declaration 节点

  3. 遍历 class declaration 节点上的 decorators 节点取得 IonicPage 节点

  4. 解析 IonicPage 装饰器的元数据,(一个页面中只允许存在一个 Ionicpage 装饰器)

  5. 根据被 IonicPage 所在的文件名,按照 ionic-cli 定义的规则获得 Component 对应的 PageModule 文件路径

  6. 如果是 AOT 模式则将 Module 的文件名修改为 .ngfactory.ts

  7. 将路径信息和 Module 的元数据(DeepLinkDecoratorData 类型)添加到 deepLinkConfigEntries 数组中

  8. 将这个数组中的信息转换成以下格式的字符串,并缓存

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    links: [
    {
    loadChildren: '../pages/xyz/xyz-home/xyz-home.module#XyzPageModule',
    name: 'XyzPage',
    segment: 'xyz-home',
    priority: 'low',
    defaultHistory: []
    }
    ]
    }
  9. 将这段代码插入到 app.module.ts 中

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
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {IonicApp, IonicModule} from 'ionic-angular';

import {DrifterApp} from './app.component';
import {DECLARATIONS} from './declarations';
import {ENTRY_COMPONENTS} from './entry-component';
import {PROVIDER} from './providers';
import {ComponentsModule} from '../components/components.module';
import {PipesModule} from '../pipes/pipes.module';

@NgModule({
declarations: DECLARATIONS,
imports: [
BrowserModule,
IonicModule.forRoot(DrifterApp, {
backButtonText: '',
iconMode: 'ios',
mode: 'ios',
},// 插入到这个位置后面
{
links: [
{
loadChildren: '../pages/xyz/xyz-home/xyz-home.module#XyzPageModule',
name: 'XyzPage',
segment: 'xyz-home',
priority: 'low',
defaultHistory: []
}
]
}),
ComponentsModule,
PipesModule
],
bootstrap: [IonicApp],
entryComponents: ENTRY_COMPONENTS,
providers: PROVIDER
})

export class AppModule {
}

这个对象被当做 IonicModule.forRoot 的第三个参数 deepLinkConfig 传入
,这个参数在后面会被当做一个 ValueProvider (DeepLinkConfigToken:OpaqueToken) 加入到 IonicModule

10.在 webpack 阶段,猜测:使用了和 @ngtools/webpack 工具类似的方法,讲需要懒加载的代码切分成几个单独的文件,以供运行时动态加载。

运行时

Ionic3.x 中页面路由采用了一种全新的方式,使用了一个叫做 DeepLinker 的服务来维护页面的路由信息

具体的操作如下:
当我们调用 NavController.push 方法,并且传入一个字符串(Page 的名称)的时候,ionic 就会加载这个页面组件

  1. push 会触发 NavControllerBase_loadLazyLoading 的调用,调用时会将页面名称以及参数等信息传入该方法

  2. _loadLazyLoading 方法中会调用一个叫做 convertToViews 的方法,这个方法会将传入的 PageName 转换为一个可以使用的 Component。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    NavControllerBase.prototype._loadLazyLoading = function (ti) {
    var _this = this;
    var /** @type {?} */ insertViews = ti.insertViews;
    if (insertViews) {
    (void 0) /* assert */;
    return convertToViews(this._linker, insertViews).then(function (viewControllers) {
    ...
    });
    }
    return Promise.resolve();
    };
  3. 如果传入的是 PageName 字符串,会调用 getComponent 函数将字符串对应的 Component 所在的 js 文件加载进来。

  4. getComponent 函数使用 deeplinker 中的 getComponentFromName 方法获取这个页面对应的路径。路径保存在上文提到的 linker 对象的 loadChildren 属性中。

  5. 使用 SystemJS 提供的 System.import 方法来动态加载这些文件,webpack2.x 支持 System.import 的模块,它会将 System.import 替换为 __webpack_require__ (但据说在 Webpack3.x 中已经废弃了对 System.import 的支持),同时被单独打包的模块会被即使执行函数表达式(IIFE)包裹,__webpack_require__ 这个方法执行后会创建一个 JSONP 请求,也就是在 Header 中插入一个 script 标签使浏览器请求需要的脚本,并立即执行该脚本,这样就得到了需要的模块。

Next Step

  1. 上述内容只研究了页面的懒加载方式,至于 Pipe,Directive 以及其他非页面的 Component 的懒加载实现方式还有待实践和研究。
  2. 构建时代码切分的处理还没有找到,看生成的代码中有着和 @ngtools/webpack 生成的代码中相似的结构
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
// src lazy 代码块
var map = {
"../pages/bottle/add-bottle/add-bottle.module": [
406,
10
]
};
function webpackAsyncContext(req) {
var ids = map[req];
if(!ids)
return Promise.reject(new Error("Cannot find module '" + req + "'."));
return __webpack_require__.e(ids[1]).then(function() {
return __webpack_require__(ids[0]);
});
};
webpackAsyncContext.keys = function webpackAsyncContextKeys() {
return Object.keys(map);
};
webpackAsyncContext.id = 155;
module.exports = webpackAsyncContext;


//////////////////
// WEBPACK FOOTER
// ./src lazy
// module id = 155
// module chunks = 12

所以推测ionic-cli 同样是实现了和 @ngtools/webpack 类似的能力。

参见

webpack 2 的新特性

DOC IonicModule

Provider 的种类以及它们的类型定义

Deep link system in Ionic Apps