[译] 通过 Webpack 实现 AngularJS 的延迟加载

逻辑玄冰探
• 阅读 11467

原文链接:http://michalzalecki.com/lazy-load-angularjs-with-webpack/

随着你的单页应用扩大,其下载时间也越来越长。这对提高用户体验不会有好处(提示:但用户体验正是我们开发单页应用的原因)。更多的代码意味着更大的文件,直到代码压缩已经不能满足你的需求,你唯一能为你的用户做的就是不要再让他一次性下载整个应用。这时,延迟加载就派上用场了。不同于一次性下载所有文件,而是让用户只下载他现在需要的文件。

所以。如何让你的应用程序实现延迟加载?它基本上是分成两件事情。把你的模块拆分成小块,并实施一些机制,允许按需加载这些块。听起来似乎有很多工作量,不是吗?如果你使用 Webpack 的话,就不会这样。它支持开箱即用的代码分割特性。在这篇文章中我假定你熟悉 Webpack,但如果你不会的话,这里有一篇介绍 。为了长话短说,我们也将使用 AngularUI RouterocLazyLoad

代码可以在 GitHub 上。你可以随时 fork 它。

Webpack 的配置

没什么特别的,真的。实际上从你可以直接从文档中复制然后粘贴,唯一的区别是采用了 ng-annotate ,以让我们的代码保持简洁,以及采用 babel 来使用一些 ECMAScript 2015 的魔法特性。如果你对 ES6 感兴趣,可以看看这篇以前的帖子 。虽然这些东西都是非常棒的,但是它们都不是实现延迟加载所必需的东西。

// webpack.config.js
var config = {
  entry: {
    app: ['./src/core/bootstrap.js'],
  },
  output: {
    path:     __dirname + '/build/',
    filename: 'bundle.js',
  },
  resolve: {
    root: __dirname + '/src/',
  },
  module: {
    noParse: [],
    loaders: [
      { test: /\.js$/, exclude: /node_modules/,
        loader: 'ng-annotate!babel' },
      { test: /\.html$/, loader: 'raw' },
    ]
  }
};

module.exports = config;

应用

应用模块是主文件,它必须被包括在 bundle.js 内,这是在每一个页面上都需要强制下载的。正如你所看到的,我们不会加载任何复杂的东西,除了全局的依赖。不同于加载控制器,我们只加载路由配置。

// app.js
'use strict';

export default require('angular')
  .module('lazyApp', [
    require('angular-ui-router'),
    require('oclazyload'),
    require('./pages/home/home.routing').name,
    require('./pages/messages/messages.routing').name,
  ]);

路由配置

所有的延迟加载都在路由配置中实现。正如我所说,我们正在使用 AngularUI Router ,因为我们需要实现嵌套视图。我们有几个使用案例。我们可以加载整个模块(包括子状态控制器)或每个 state 加载一个控制器(不去考虑对父级 state 的依赖)。

加载整个模块

当用户输入 /home 路径,浏览器就会下载 home 模块。它包括两个控制器,针对 homehome.about 这两个state。我们通过 state 的配置对象中的 resolve 属性就可以实现延迟加载。得益于 Webpack 的 require.ensure 方法,我们可以把 home 模块创建成第一个代码块。它就叫做 1.bundle.js 。如果没有 $ocLazyLoad.load,我们会发现得到一个错误 Argument 'HomeController' is not a function, got undefined,因为在 Angular 的设计中,启动应用之后再加载文件的方式是不可行的。 但是 $ocLazyLoad.load 使得我们可以在启动阶段注册一个模块,然后在它加载完之后再去使用它。

// home.routing.js
'use strict';

function homeRouting($urlRouterProvider, $stateProvider) {
  $urlRouterProvider.otherwise('/home');

  $stateProvider
    .state('home', {
      url: '/home',
      template: require('./views/home.html'),
      controller: 'HomeController as vm',
      resolve: {
        loadHomeController: ($q, $ocLazyLoad) => {
          return $q((resolve) => {
            require.ensure([], () => {
              // load whole module
              let module = require('./home');
              $ocLazyLoad.load({name: 'home'});
              resolve(module.controller);
            });
          });
        }
      }
    }).state('home.about', {
      url: '/about',
      template: require('./views/home.about.html'),
      controller: 'HomeAboutController as vm',
    });
}

export default angular
  .module('home.routing', [])
  .config(homeRouting);

控制器被当作是模块的依赖。

// home.js
'use strict';

export default angular
  .module('home', [
    require('./controllers/home.controller').name,
    require('./controllers/home.about.controller').name
  ]);

仅加载控制器

我们所做的是向前迈出的第一步,那么我们接着进行下一步。这一次,将没有大的模块,只有精简的控制器。

// messages.routing.js
'use strict';

function messagesRouting($stateProvider) {
  $stateProvider
    .state('messages', {
      url: '/messages',
      template: require('./views/messages.html'),
      controller: 'MessagesController as vm',
      resolve: {
        loadMessagesController: ($q, $ocLazyLoad) => {
          return $q((resolve) => {
            require.ensure([], () => {
              // load only controller module
              let module = require('./controllers/messages.controller');
              $ocLazyLoad.load({name: module.name});
              resolve(module.controller);
            })
          });
        }
      }
    }).state('messages.all', {
      url: '/all',
      template: require('./views/messages.all.html'),
      controller: 'MessagesAllController as vm',
      resolve: {
        loadMessagesAllController: ($q, $ocLazyLoad) => {
          return $q((resolve) => {
            require.ensure([], () => {
              // load only controller module
              let module = require('./controllers/messages.all.controller');
              $ocLazyLoad.load({name: module.name});
              resolve(module.controller);
            })
          });
        }
      }
    })
    ...

我相信在这里没有什么特别的,规则可以保持不变。

加载视图(Views)

现在,让我们暂时放开控制器而去关注一下视图。正如你可能已经注意到的,我们把视图嵌入到了路由配置里面。如果我们没有把里面所有的路由配置放进 bundle.js,这就不会是一个问题,但现在我们需要这么做。这个案例不是要延迟加载路由配置而是视图,那么当我们使用 Webpack 来实现的时候,这会非常简单。

// messages.routing.js
  ...
  .state('messages.new', {
        url: '/new',
        templateProvider: ($q) => {
          return $q((resolve) => {
            // lazy load the view
            require.ensure([], () => resolve(require('./views/messages.new.html')));
          });
        },
        controller: 'MessagesNewController as vm',
        resolve: {
          loadMessagesNewController: ($q, $ocLazyLoad) => {
            return $q((resolve) => {
              require.ensure([], () => {
                // load only controller module
                let module = require('./controllers/messages.new.controller');
                $ocLazyLoad.load({name: module.name});
                resolve(module.controller);
              })
            });
          }
        }
      });
  }

  export default angular
    .module('messages.routing', [])
    .config(messagesRouting);

当心重复的依赖

让我们来看看 messages.all.controllermessages.new.controller 的内容。

// messages.all.controller.js
'use strict';

class MessagesAllController {
  constructor(msgStore) {
    this.msgs = msgStore.all();
  }
}

export default angular
  .module('messages.all.controller', [
    require('commons/msg-store').name,
  ])
  .controller('MessagesAllController', MessagesAllController);
// messages.all.controller.js
'use strict';

class MessagesNewController {
  constructor(msgStore) {
    this.text = '';
    this._msgStore = msgStore;
  }
  create() {
    this._msgStore.add(this.text);
    this.text = '';
  }
}

export default angular
  .module('messages.new.controller', [
    require('commons/msg-store').name,
  ])
  .controller('MessagesNewController', MessagesNewController);

我们的问题的根源是 require('commons/msg-store').name 。它需要 msgStore 这一个服务,来实现控制器之间的消息共享。此服务在两个包中都存在。在 messages.all.controller 中有一个,在 messages.new.controller 中又有一个。现在,它已经没有任何优化的空间。如何解决呢?只需要把 msgStore 添加为应用模块的依赖。虽然这还不够完美,在大多数情况下,这已经足够了。

// app.js
'use strict';

export default require('angular')
  .module('lazyApp', [
    require('angular-ui-router'),
    require('oclazyload'),
    // msgStore as global dependency
    require('commons/msg-store').name,
    require('./pages/home/home.routing').name,
    require('./pages/messages/messages.routing').name,
  ]);

单元测试的技巧

msgStore 改成是全局依赖并不意味着你应该从控制器中删除它。如果你这样做了,在你编写测试的时候,如果没有模拟这一个依赖,那么它就无法正常工作了。因为在单元测试中,你只会加载这一个控制器而非整个应用模块。

// messages.all.controller.spec.js
'use strict';

describe('MessagesAllController', () => {

  var controller,
      msgStoreMock;

  beforeEach(angular.mock.module(require('./messages.all.controller').name));
  beforeEach(inject(($controller) => {
    msgStoreMock = require('commons/msg-store/msg-store.service.mock');
    spyOn(msgStoreMock, 'all').and.returnValue(['foo', 8]);
    controller = $controller('MessagesAllController', { msgStore: msgStoreMock });
  }));

  it('saves msgStore.all() in msgs', () => {
    expect(msgStoreMock.all).toHaveBeenCalled();
    expect(controller.msgs).toEqual(['foo', 8]);
  });

});
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
Java获得今日零时零分零秒的时间(Date型)
publicDatezeroTime()throwsParseException{    DatetimenewDate();    SimpleDateFormatsimpnewSimpleDateFormat("yyyyMMdd00:00:00");    SimpleDateFormatsimp2newS
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这