原文连接:http://michalzalecki.com/lazy-load-angularjs-with-webpack/html
随着你的单页应用扩大,其下载时间也愈来愈长。这对提升用户体验不会有好处(提示:但用户体验正是咱们开发单页应用的缘由)。更多的代码意味着更大的文件,直到代码压缩已经不能知足你的需求,你惟一能为你的用户作的就是不要再让他一次性下载整个应用。这时,延迟加载就派上用场了。不一样于一次性下载全部文件,而是让用户只下载他如今须要的文件。node
因此。如何让你的应用程序实现延迟加载?它基本上是分红两件事情。把你的模块拆分红小块,并实施一些机制,容许按需加载这些块。听起来彷佛有不少工做量,不是吗?若是你使用 Webpack 的话,就不会这样。它支持开箱即用的代码分割特性。在这篇文章中我假定你熟悉 Webpack,但若是你不会的话,这里有一篇介绍 。为了长话短说,咱们也将使用 AngularUI Router 和 ocLazyLoad 。webpack
代码能够在 GitHub 上。你能够随时 fork 它。git
没什么特别的,真的。实际上从你能够直接从文档中复制而后粘贴,惟一的区别是采用了 ng-annotate
,以让咱们的代码保持简洁,以及采用 babel
来使用一些 ECMAScript 2015 的魔法特性。若是你对 ES6 感兴趣,能够看看这篇之前的帖子 。虽然这些东西都是很是棒的,可是它们都不是实现延迟加载所必需的东西。angularjs
// 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
内,这是在每个页面上都须要强制下载的。正如你所看到的,咱们不会加载任何复杂的东西,除了全局的依赖。不一样于加载控制器,咱们只加载路由配置。github
// 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 的依赖)。web
当用户输入 /home
路径,浏览器就会下载 home 模块。它包括两个控制器,针对 home
和 home.about
这两个state。咱们经过 state 的配置对象中的 resolve
属性就能够实现延迟加载。得益于 Webpack 的 require.ensure
方法,咱们能够把 home 模块建立成第一个代码块。它就叫作 1.bundle.js
。若是没有 $ocLazyLoad.load
,咱们会发现获得一个错误 Argument 'HomeController' is not a function, got undefined
,由于在 Angular 的设计中,启动应用以后再加载文件的方式是不可行的。 可是 $ocLazyLoad.load
使得咱们能够在启动阶段注册一个模块,而后在它加载完以后再去使用它。bootstrap
// 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 ]);
咱们所作的是向前迈出的第一步,那么咱们接着进行下一步。这一次,将没有大的模块,只有精简的控制器。babel
// 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); }) }); } } }) ...
我相信在这里没有什么特别的,规则能够保持不变。
如今,让咱们暂时放开控制器而去关注一下视图。正如你可能已经注意到的,咱们把视图嵌入到了路由配置里面。若是咱们没有把里面全部的路由配置放进 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.controller
和 messages.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]); }); });