As your single-page app grows, so does its download time. This won’t do much good for user experience (hint: but user experience is why we develop single-page apps). More code means bigger files, and until code compression no longer meets your needs, the only thing you can do for your users is stop asking them to download the entire application at once. This is where lazy loading comes in handy. Instead of downloading all files at once, the user is allowed to download only the files he needs now.
So. How to make your application lazy loading? It's basically split into two things. Split your module into small chunks and implement some mechanism that allows these chunks to be loaded on demand. Sounds like a lot of work, doesn't it? Not if you use Webpack. It supports code splitting features out of the box. In this article I assume you are familiar with Webpack, but if you are not, here is an introduction. To keep a long story short, we will also use AngularUI Router and ocLazyLoad.
The code is available on GitHub. You can fork it at any time.
Webpack configuration
Nothing special, really. You could actually just copy and paste from the documentation, the only difference is the use of ng-annotate to keep our code clean, and babel to use some of the ECMAScript 2015 magic. If you are interested in ES6, you can check out this previous post . While these things are all great, none of them are necessary to implement lazy loading.
// 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;
Apply
The application module is the main file, which must be included in bundle.js, which is required to be downloaded on every page. As you can see, we're not loading anything complicated except global dependencies. Unlike loading the controller, we only load the routing configuration.
// 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, ]);
Routing configuration
All lazy loading is implemented in route configuration. As I said, we are using AngularUI Router because we need to implement nested views. We have several use cases. We can load the entire module (including child state controllers) or load a controller per state (regardless of dependencies on parent states).
Load the entire module
When the user enters the /home path, the browser will download the home module. It includes two controllers, for the two states home and home.about. We can implement lazy loading through the resolve attribute in the state configuration object. Thanks to Webpack's require.ensure method, we can create the home module as the first code block. It's called 1.bundle.js . Without $ocLazyLoad.load , we will find that we get an error Argument 'HomeController' is not a function, got undefined , because in Angular's design, it is not feasible to load files after starting the application. But $ocLazyLoad.load allows us to register a module during the startup phase and then use it after it is loaded.
// 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);
Controllers are treated as module dependencies.
// home.js 'use strict'; export default angular .module('home', [ require('./controllers/home.controller').name, require('./controllers/home.about.controller').name ]);
Load controller only
What we are doing is the first step forward, then let’s move on to the next step. This time, there will be no big modules, just streamlined controllers.
// 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); }) }); } } })
I believe there is nothing special here and the rules can remain the same.
Loading Views
Now, let’s let go of the controller for a moment and focus on the view. As you may have noticed, we embed the view into the routing configuration. This wouldn't be a problem if we didn't put all the routing configuration inside bundle.js , but now we need to. This case is not about lazy loading route configuration but view, so when we use Webpack to implement it, this will be very simple.
// 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);
Beware of duplicate dependencies
Let’s take a look at the contents of messages.all.controller and 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);
The source of our problem is require('commons/msg-store').name . It requires the msgStore service to realize message sharing between controllers. This service is present in both packages. There is one in messages.all.controller and another in messages.new.controller. Now, it has no room for optimization. How to solve it? Just add msgStore as a dependency of the application module. While this isn't perfect, in most cases it's enough.
// 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', ]); controller = $controller('MessagesAllController', { msgStore: msgStoreMock }); })); it('saves msgStore.all() in msgs', () => { expect(msgStoreMock.all).toHaveBeenCalled(); expect(controller.msgs).toEqual(['foo', ]); }); });
以上内容是小编给大家分享的Webpack 实现 AngularJS 的延迟加载,希望对大家有所帮助!