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

用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 的方式运行测试服务器,并打开页面,打开几个页面以后,网络请求如下:

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

延时加载模式

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

这下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 的功能。

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

这下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模式编译后的大小:

结果出乎意外,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的特性,为解决大小的问题提供了前提。

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

(0)

相关推荐

  • AngularJS 实现按需异步加载实例代码

    AngularJS 通过路由支持多视图应用, 可以根据路由动态加载所需的视图, 在 AngularJS 的文档中有详细的介绍, 网上也有不少教程, 就不用介绍了! 随着视图的不断增加,js文件会越来越多,而 AngularJS 默认需要把全部的js都一次性加载,使用起来非常不便, 因此按需加载模块的需求会越来越强,不过,AngularJS 并没有实现按需加载. 习惯了 seajs 的异步加载方式,也想着 angular 也能同样使用异步加载,但是事实不随人愿. angularjs 和 requi

  • 详解angularjs利用ui-route异步加载组件

    ui-route相比于angularjs的原生视图路由更好地支持了路由嵌套,状态转移等等.随着视图不断增加,打包的js体积也会越来越大,比如我在应用里面用到了wangeditor里面单独依赖的jquery就300多k.异步加载各个组件就很有必要.在这里我就以ui-route为框架来进行异步加载说明. 首先看一下路由加载文件 angular.module('webtrn-sns').config(['$stateProvider', function ($stateProvider) { $sta

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

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

  • 浅析JS异步加载进度条

    展现效果: 1) 当点击Load的时候,模拟执行异步加载. 浏览器被遮挡. 进度条出现. 实现思路: 1.当用户点击load button执行异步请求. 调用方法 出现加载条 2.怎么实现进度条呢? 1) 在document.body 新增一个div.覆盖浏览器. 设置背景会灰色. z-index = 999. 加载的时候让用户无法修改界面值 2) 在document.body 新增一个动态的div. 代码实现: Main.html: <!DOCTYPE html> <html>

  • JavaScript异步加载浅析

    前言 关于JavaScript脚本加载的问题,相信大家碰到很多.主要在几个点-- 1> 同步脚本和异步脚本带来的文件加载.文件依赖及执行顺序问题 2> 同步脚本和异步脚本带来的性能优化问题 深入理解脚本加载相关的方方面面问题,不仅利于解决实际问题,更加利于对性能优化的把握并执行.   先看随便一个script标签代码-- 复制代码 代码如下: <script src="js/myApp.js"></script> 如果放在<head>上面

  • JavaScript异步加载问题总结

    同步加载的问题 默认的js是同步加载的,这里的"加载"可以理解成是解析.执行,而不是"下载",在最新版本的浏览器中,浏览器对于代码请求的资源都是瀑布式的加载,而不是阻塞式的,但是js的执行总是阻塞的.这会引起什么问题呢?如果我的index页面要加载一些js,但是其中的某个请求迟迟得不到响应,于是阻塞了后面的js代码的执行(同步加载),同时页面渲染也不能继续(如果js引入是在head标签后). <script type="text/javascript

  • Android实现从缓存中读取图片与异步加载功能类

    本文实例讲述了Android实现从缓存中读取图片与异步加载功能类.分享给大家供大家参考,具体如下: 在新浪微博的微博列表中的图片,为了加速其显示也为了加快程序的响应,可以参考该图片异步加载类实现. public class AsyncImageLoader { //SoftReference是软引用,是为了更好的为了系统回收变量 private HashMap<String, SoftReference<Drawable>> imageCache; public AsyncImag

  • jquery使用EasyUI Tree异步加载JSON数据(生成树)

    这几天因为工作需要,要做一个支持无限级的菜单. 我也是菜鸟一只,能想到的东西不多,所以用了Easy UI的tree组件. 不得不说,easyui确实很强大. 因为是无限级菜单,数据量可能有点大,所以考虑采用异步加载. 但是因为后台默认传来的数据是 一个实体,所以又在后台进行了JSON字符串拼接. 最后,在网上找了N多代码,然后又去问了好几个群里的网友,终于搞出来这个小东西. 一.HTML部分代码 <div id="categoryChooseDiv" title="请选

  • 关于 jQuery Easyui异步加载tree的问题解析

    想要实现从本地中加载json文件,通过事件来动态的插入到ul中时,遇到了一小bug html中代码是这样的 <ul class="easyui-tree" id="tt"></ul> js中的代码 $(".next-menu:nth-child(1) a").click(function() { var $IDstr = $(this).attr("id"), $treeIDNum = parseInt

  • jquery zTree异步加载、模糊搜索简单实例分享

    本文实例为大家讲解了jquery zTree树插件的基本使用方法,具体内容如下 一.节点模糊搜索功能:搜索成功后,自动高亮显示并定位.展开搜索到的节点. 二.节点异步加载:1.点击展开时加载数据:2.选中节点时加载数据. 前台代码如下: <script type="text/javascript"> //ztree设置 var setting = { view: { fontCss: getFontCss }, check: { enable: true }, data:

  • jQuery使用zTree插件实现树形菜单和异步加载

    本文实例讲解了jQuery使用zTree插件实现树形菜单和异步加载,并且可以进行编辑,分享给大家供大家参考,具体内容如下 效果图: 一.HTML代码 <html> <head runat="server"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title></title> <l

随机推荐