Webpack implements lazy loading of AngularJS

  • 2021-01-19 22:00:50
  • OfStack

As your single page app grows, it takes longer to download. This is not good for the user experience (hint: but the user experience is why we developed single-page apps). More code means bigger files, and until code compression is no longer sufficient, the only thing you can do for your users is stop making them download the entire application all at once. This is where lazy loading comes in. Instead of downloading all the files at once, the user downloads only the files he needs right now.

So. How to make your application lazy to load? It basically breaks down into two things. Break up your modules into small chunks and implement mechanisms that allow these blocks to be loaded on demand. Sounds like a lot of work, doesn't it? Not if you use Webpack. It supports out-of-the-box code splitting features. In this article I assume that you are familiar with Webpack, but if you are not, here is an introduction. To make a long story short, we will also use AngularUI, Router and ocLazyLoad.

The code is available on GitHub. You can always fork it.

Webpack configuration

Nothing special, really. In fact, you can copy and paste from the document directly, the only difference is that ng-annotate is used to keep our code clean, and babel is used to use some of the magic features of ECMAScript 2015. If you're interested in ES6, check out this previous post. While all of these things are great, they are not 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;

application

The application module is the main file, which must be included in bundle. js, which is a mandatory download on every page. As you can see, we don't load anything complicated except global dependencies. Unlike the load controller, we only load the route 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,
]);

The routing configuration

All lazy loading is implemented in the routing configuration. As I said, we are using ES34en ES35en because we need to implement nested views. We have several use cases. We can load the entire module (including child state controllers) or one controller per state (regardless of the dependency on the parent state).

Load the whole module

When the user enters the /home path, the browser downloads the home module. It includes two controllers for home and home.about are the two state. We can implement lazy loading by using the resolve property 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. Argument 'HomeController' is not a function, got undefined, Angular a function, got undefined, Angular, Angular, Angular, Angular a function, got undefined, Angular, Angular, Angular, Angular, Angular, Angular, Angular, Angular a function, got undefined, Angular, Angular, Angular But $ocLazyLoad.load allows us to register a module at startup and then use it after it's 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've done is take the first step forward, so let's move on to the next step. This time, there will be no big modules, only a streamlined controller.


// 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 stay the same.

Load View (Views)

Now, let's leave the controller for a moment and focus on view 1. As you may have noticed, we embedded the view in the route configuration. This wouldn't be a problem if we didn't put all of the routing configuration in bundle.js, but now we need to. This case is not about lazy loading the routing configuration but the view, so when we implement it using ES87en, it 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 repetitive dependencies

Let's take a look at 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 one service, msgStore, to share messages between controllers. This service exists in both packages. There is one in messages.all.controller and another in messages.new.controller. Now, there is no room for optimization. What's the solution? Simply 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,
]);

Unit testing techniques

Changing msgStore to be a global dependency does not mean you should remove it from the controller. If you do this, it will not work properly if you do not simulate this dependency when you write the test. Because in unit tests, you only load the one controller and not the entire application module.


// 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', ]);
});
});

The above content is this site to share Webpack to achieve AngularJS delayed loading, I hope to help you!


Related articles: