浅析Angular2子模块以及异步加载


Posted in Javascript onApril 24, 2017

用Angular2开发一个大型的应用,我们通常都需要分模块进行开发。例如将某一个功能的相关页面和功能放在一个模块里面,这样既可以实现系统的松耦合,给开发和后期的维护带来很大的便利。同时,对于子模块,我们还可以使用延时加载,这样可以减少初始加载的文件的大小。在这篇文章中,我们就来看看在Angular2框架下怎么实现子模块及其延时加载。

可以在这里查看本文使用的实例 。该实例基于上篇文章Angular2使用Guard和Resolve进行验证和权限控制 所用的实例,并在它基础上添加了一个lazy的模块,以及将现有的todo模块配置成延时加载方式。

为了体现启用延时加载前后的包的大小变化,以及启用压缩后的变化,在这个教程里面,使用了angular-cli创建项目脚手架,并用它来进行测试和打包。有关angular-cli的使用请查看 官网 。在这篇文章我们使用的angular-cli的版本是1.0.0-beta.21。如果你使用的是别的版本,可能结果就会不一样。甚至有些错误,我们在最后会说明当前版本angular-cli的bug。

模块设计

在开发Angular2应用时,像组件设计、路由设计以外,对于一个较大型的应用,我们还需要设计模块。例如,将一个应用分成几个功能模块,以及有哪些公用模块。公用模块里面应该放公用的service类,例如权限验证、登录、获取用户信息、全局的错误处理、工具类等,还有封装的指令或组件。而在某一个功能模块里面,只处理这个模块里面的业务,尽量不和其他模块交互。

拿之前教程中的TodoList应用来说,只有home页面和2个todo页面,我们把todo相关的功能放在一个子模块里面,为了演示,又加了一个简单的名字叫lazy的模块。我们将把todo模块和lazy模块配置成延时加载的模块。

子模块开发

接下来再看看子模块的开发。其实在之前的例子中,就把todo相关的组件放在了一个模块里面。但是却没有强调子模块开发需要注意的地方,甚至有些配置可能没有采用子模块的方式进行配置。这里,我们就主要说明一下需要注意的地方,如果要查看完整的代码,请参考 实例源代码 。

子模块路由

首先需要注意的是路由。在之前的例子中,我们把todo相关的路由定义在一个文件中,然后在app的路由定义中把所有路由合并到一起。 todo.routes.ts 的内容如下:

// 省略import
export const TodoRoutes: Route[] = [
  {
    path: 'todo',
    canActivateChild: [MyTodoGuard],
    children: [
      { path: 'list', component: TodoListComponent, resolve: { todos: MyTodoResolver } },
      { path: 'detail/:id', component: TodoDetailComponent, canDeactivate: [ CanLeaveTodoDetailGuard ] }
    ]
  }
];

然后在 app.routes.ts 中定义一个路由模块:

const routes: Routes = [
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },
  ...TodoRoutes // 这里就是将TodoRoutes列表里的内容合并到routes
];
@NgModule({
 imports: [ RouterModule.forRoot(routes) ],
 exports: [ RouterModule ]
})
export classAppRoutingModule{ }

最后,在AppModule里面引入这个路由模块。

这种方式实现的路由无法实现子模块的延时加载,要实现延时加载,首先要将todo模块的路由修改成子路由模块,也就是要修改 todo.routes.ts

// 省略import
export const TodoRoutes: Route[] = [
  {
    path: 'todo',
    canActivateChild: [MyTodoGuard],
    children: [
      { path: 'list', component: TodoListComponent, resolve: { todos: MyTodoResolver } },
      { path: 'detail/:id', component: TodoDetailComponent, canDeactivate: [ CanLeaveTodoDetailGuard ] }
    ]
  }
];
// 通过下面的方式定义了一个子路由模块
@NgModule({
 imports: [ RouterModule.forChild(TodoRoutes) ],
 exports: [ RouterModule ]
})
export classTodoRoutingModule{ }

这里,我们定义了一个子路由模块, TodoRoutingModule ,它使用 RouterModule.forChild(TodoRoutes) 来创建。跟整个App的路由模块比较的话,主路由模块使用 RouterModule.forRoot(routes) 来定义。

定义好了子路由模块,我们就在子模块里面引入它既可:

// 省略import
@NgModule({
 imports: [CommonModule, FormsModule, TodoRoutingModule ],
 declarations: [TodoListComponent, TodoDetailComponent, TodoItemComponent],
 providers: [TodoService, MyTodoResolver, MyTodoGuard, CanLeaveTodoDetailGuard]
})
export classTodoModule{}

这样,我们就定义好了一个子模块。当用户打开 /todo/list /todo/detail/* 时,这个子模块里面的相关页面就会展示,它也不会跟其他模块有任何交互。也就是说,进入和离开这个子模块,都是通过路由跳转实现。这个子模块也是完全独立的,可以独立开发,也可以很容易就用到其他应用里面。

延时加载子模块

下面,我们就可以通过修改路由的配置,使得todo模块实现延时加载。Angular的路由模块已经提供了 loadChildren 定义可以直接帮我们实现该功能。下面就是新的app路由定义

const routes: Routes = [
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },
  { path: 'todo', loadChildren: 'app/todo/todo.module#TodoModule' },
  { path: 'lazy', loadChildren: 'app/lazy/lazy.module#LazyModule' }
];

@NgModule({
 imports: [ RouterModule.forRoot(routes) ],
 exports: [ RouterModule ]
})
export classAppRoutingModule{ }

在这里,我们对于 todo 路径,交给 app/todo/todo.module 里面的 TodoModule 模块处理。而在 TodoModule 模块里,已经有一个子路由的定义。

最后,再修改 app.module.ts ,保证它里面不再引入 TodoModule 。如此一来,我们在主模块AppModule里面,没有引入 todo 模块的任何组件或服务。这样就能在完全脱离 TodoModule 模块的情况下,运行主模块的功能。当用户打开 /todo 里面的url时,就加载 app/lazy/lazy.module 里面的 LazyModule 模块,并交由它来处理响应的url。

总结一下,实现延时加载子模块,主要是要注意下面几点:

  1. 子模块的路由用 RouterModule.forChild(TodoRoutes) 方式定义。
  2. 主模块不要引入子模块,也不要引入子模块的任何组件或服务,否则子模块就会被打包进主模块里。
  3. 只有子模块才会用到的Service在子模块的 providers 里面定义,如果是主模块和子模块都会用到的Service就用公用模块的方式定义。要注意这个Service的实例只能有一个。

运行

接下来我们来看看运行的结果。(注意根据运行环境不同,文件大小会不一样)

不启用延时加载

首先,我们在 app.module.ts 引入 TodoModule ,这样 todo 模块不是延时加载的,只有 lazy 模块是延时加载的。我们使用 ng serve 的方式运行测试服务器,并打开页面,打开几个页面以后,网络请求如下:

浅析Angular2子模块以及异步加载 

从图中可以看到,有一个3.4M的main的js文件,下面的 1.chunk.js 的 lazy 模块延时加载的。打包的文件确实是非常的大,因为lazy模块非常简单,只是显示了一个字符串在模板里。所以它的大小也非常小,才5.8k。

延时加载模式

下面在把 TodoModule 模块从 app.module.ts 去掉,这样, todo 模块就是延时加载的,再看一下网络请求:

浅析Angular2子模块以及异步加载 

这下main文件变成了3.1M,lazy模块对应的js文件是 1.chunk.js ,还是5.8k,todo模块对应的文件 0.chunk.js 是324K。可以看见一个很简单的todo模块,里面有service, rosolver, guards, 还有3个组件,里面分别都有模块、css,虽然文件不少,但是他们的实现实际上都很小。只是一个模块的文件,在未压缩的情况下就有300多K,让我这个Angular2的忠实粉丝都无语。

延时加载-prod模式

一般我们在部署应用的时候,都会使用压缩、混淆、合并等方法来减少最终文件的大小。使用angular-cli工具,除了在编译的时候提供打包的功能,甚至在测试的时候,也可以启用压缩选项。我们可以运行 ng serve -pro 来使用 prod 模式来启动测试服务器。在启动的过程中,可以看到很多类似下面的日志:

WARNING in 0.005fea95566fdabe23df.chunk.js from UglifyJs
Dropping unused function scheduleMicroTask [/Users/mavlarn/mydev/blog/angular2-tutorial/angular2-routes-lazy-module-webpack/~/@angular/forms/src/facade/lang.js:21,0]

可以看出,angular-cli的 prod 模式下编译的时候,去除了很多不需要的代码,这就是angular的 Tree Shaking 的功能。

运行以后,网络请求如下:

浅析Angular2子模块以及异步加载 

这下main文件减少到了221K,lazy模块对应的js文件是 1.chunk.js ,只有1.0k,todo模块对应的文件 0.chunk.js 是17.9K。总共大小大概是240K左右,如果再使用GZip压缩,应该可以到6,70K左右。在官方文档里提到,一个Angular2的简单实例,通过Tree Shaking、压缩、GZip,最终下载的包大小有50K。我们这个实例毕竟稍微复杂,实现了大多数的通用功能,如路由、guard、resolver、表单,也是用到了Rxjs里的 Observable ,所以最终压缩后能有70K左右的话,也符合官方文档的说法。

编译后

最后,我们再使用 ng build --prod 来看看用prod模式编译后的大小:

浅析Angular2子模块以及异步加载 

结果出乎意外,main文件的大小比上面在prod模式下运行测试服务器大很多,达到800多K。应该是编译过程需要某些参数,或者是当前的angular-cli有什么bug。

再使用 ng build --prod --aot 编译,main文件的大小是446K。虽然小了一点,但是也不符合预期。

总结

先说延时加载,应该都知道可以减少第一次加载的文件的大小。特别是当某个模块使用了一些比较大第三方的js库,例如图形库等,那么,把这些模块独立出来,使用延时加载的方式,可以大大减少首次加载的时间。对于Angular2的应用来说,如果我们要定义 Component ,就从 @angular/core 里面引入 Component ,需要定义路由就从 @angular/router 里面引入`Router。所以,只要我们设计好了整个App的模块、组件、路由,我们就可以利用延时加载的功能使得首页文件尽可能的小。

使用模块化的开发,也能给我们的开发和维护带来很大的便利,项目越大越大,模块化和组件化带来的便利就越明显。

目前Angular2的天坑

在网上,经常可以看到一些文章说Angular1或者2的一些坑。实际上,大部分都是因为使用不当,或者没有按照最佳实践去使用,特别是Angular1。虽然Angular1有本质上的性能问题,但是,通过良好的整体设计、良好的 代码规范和质量,还是可以开发出很流畅的手机web应用。

但是,在准备这篇文章中的实例时,却遇到了几个严重的问题,让我这个Angular2的忠实粉丝也很无奈。

Angular 2.2.2及以上版本的BUG

我在实例中使用的Angular的版本是2.2.1,如果用的版本是2.2.2 ~ 2.3.0之间,在运行或编译的时候,可能会出现如下的错误:

ngCompiler.ReflectorHost is not a constructor
TypeError: ngCompiler.ReflectorHost is not a constructor

可以上Github查看该 issue 的情况。如果遇到这种问题,只能先使用2.2.1的版本。

Angular-Router

在这个实例中,延时加载的todo模块里面有一个service,我们使用Angular的依赖注入的功能自动初始化以及诸如这个服务的实例。但是,在3.1.2及以上的版本里面,这个服务会被创建多次,每次激活相关路由的时候,就会创建一次。而且,只有在延时加载的模式才会发生这种错误。相关 issue

TypeScript

在我之前的教程里,判断用户是否具有某种权限,使用了如下的方法:

hasRole(role: string): boolean {
  return this.account && this.account.roles.includes(role);
}

但是,更新了TypeScript以后,该方法就不存在了,原因可以查看 这个 .

所以改成了用 indexOf(role) > 0 来判断列表里是否存在一个字符串。

虽然目前Angular还不是十分稳定,有一些Bug,甚至TypeScript也不稳定,但是,相信这些问题都能够很快解决。而且随着框架越来越成熟,也会越来越稳定。

而且,Angular2+Typescript的开发方式也十分便利,Typescript的强类型检查能够帮助我们减少编码的错误,提高效率。而且,我们也可以很方便的查看框架的API,能省去很多查资料的时间。

Angular2的很多思想非常适用于开发大型的应用。如果开发过大型的Java项目,就会发现学习Angular2是一件非常容易的事情。Angular2引入了很多面向对象的框架的思想,而这些,都是在面向对象领域开发大型项目的多年开发经验。这些经验应用到前端开发,也能帮助我们更方便的开发和维护大型的前端项目。

虽然,Angular2的应用最终的打包文件非常大(我们这个实例即使压缩完后也有70K左右,但是如果用VUE的话会比这个小很多),但是随着Angular2的越来越稳定,各种开发工具越来越成熟,相信文件大小的问题也能够有一个比较好的解决方案。因为Angular2的AOT、Tree Shaking的特性,为解决大小的问题提供了前提。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
浅析js中2个等号与3个等号的区别
Aug 06 Javascript
js跨浏览器实现将字符串转化为xml对象的方法
Sep 25 Javascript
JavaScript中使用Substring删除字符串最后一个字符
Nov 03 Javascript
js如何调用qq互联api实现第三方登录
Mar 28 Javascript
Bootstrap每天必学之按钮
Nov 26 Javascript
jquery输入数字随机抽奖特效的简单实现代码
Jun 10 Javascript
原生js代码实现图片放大境效果
Oct 30 Javascript
jquery实现手机端单店铺购物车结算删除功能
Feb 22 Javascript
AngularJS动态绑定ng-options的ng-model实例代码
Jun 21 Javascript
jQuery UI 实例讲解 - 日期选择器(Datepicker)
Sep 18 jQuery
vue实现鼠标移过出现下拉二级菜单功能
Dec 12 Javascript
React冒泡和阻止冒泡的应用详解
Aug 18 Javascript
Angular2使用Guard和Resolve进行验证和权限控制
Apr 24 #Javascript
详解AngularJS 路由 resolve用法
Apr 24 #Javascript
详解AngularJS ui-sref的简单使用
Apr 24 #Javascript
详解在Angularjs中ui-sref和$state.go如何传递参数
Apr 24 #Javascript
JS实现获取图片大小和预览的方法完整实例【兼容IE和其它浏览器】
Apr 24 #Javascript
angular中实现控制器之间传递参数的方式
Apr 24 #Javascript
使用vue框架 Ajax获取数据列表并用BootStrap显示出来
Apr 24 #Javascript
You might like
PHP中限制IP段访问、禁止IP提交表单的代码
2011/04/23 PHP
PHP两种去掉数组重复值的方法比较
2014/06/19 PHP
用 Composer构建自己的 PHP 框架之基础准备
2014/10/30 PHP
PHP多维数组遍历方法(2种实现方法)
2015/12/10 PHP
PHP数据分析引擎计算余弦相似度算法示例
2017/08/08 PHP
Laravel框架创建路由的方法详解
2019/09/04 PHP
prototype 学习笔记整理
2009/07/17 Javascript
web开发人员学习jQuery的6大理由及jQuery的优势介绍
2013/01/03 Javascript
JS解决url传值出现中文乱码的另类办法
2013/04/08 Javascript
一览画面点击复选框后获取多个id值的方法
2016/05/30 Javascript
Javascript中的arguments对象
2016/06/20 Javascript
基于jQuery和Bootstrap框架实现仿知乎前端动态列表效果
2016/11/09 Javascript
vue.js实例todoList项目
2017/07/07 Javascript
Vue数组更新及过滤排序功能
2017/08/10 Javascript
vue 路由页面之间实现用手指进行滑动的方法
2018/02/23 Javascript
浅谈vue的几种绑定变量的值 防止其改变的方法
2018/03/01 Javascript
使用jquery-easyui的布局layout写后台管理页面的代码详解
2019/06/19 jQuery
JS中比Switch...Case更优雅的多条件判断写法
2019/09/05 Javascript
在Gnumeric下使用Python脚本操作表格的教程
2015/04/14 Python
基于循环神经网络(RNN)实现影评情感分类
2018/03/26 Python
Python图像处理之图像的缩放、旋转与翻转实现方法示例
2019/01/04 Python
详解python中__name__的意义以及作用
2019/08/07 Python
nginx+uwsgi+django环境搭建的方法步骤
2019/11/25 Python
浅谈keras使用预训练模型vgg16分类,损失和准确度不变
2020/07/02 Python
Django ORM判断查询结果是否为空,判断django中的orm为空实例
2020/07/09 Python
python3中TQDM库安装及使用详解
2020/11/18 Python
详解CSS3 Media Queries中媒体属性的使用
2016/02/29 HTML / CSS
如何获取某个日期是当月的最后一天
2013/12/05 面试题
文明餐桌行动实施方案
2014/02/19 职场文书
保研推荐信
2014/05/09 职场文书
学党史心得体会
2014/09/05 职场文书
学校机关党总支领导班子整改工作方案
2014/10/26 职场文书
小学生交通安全寄语
2015/02/27 职场文书
入党转正申请报告
2015/05/15 职场文书
第一书记观后感
2015/06/08 职场文书
教你使用Pandas直接核算Excel中快递费用
2021/05/12 Python