Webpack 实现 AngularJS 的延迟加载
随着你的单页应用扩大,其下载时间也越来越长。这对提高用户体验不会有好处(提示:但用户体验正是我们开发单页应用的原因)。更多的代码意味着更大的文件,直到代码压缩已经不能满足你的需求,你唯一能为你的用户做的就是不要再让他一次性下载整个应用。这时,延迟加载就派上用场了。不同于一次性下载所有文件,而是让用户只下载他现在需要的文件。
所以。如何让你的应用程序实现延迟加载?它基本上是分成两件事情。把你的模块拆分成小块,并实施一些机制,允许按需加载这些块。听起来似乎有很多工作量,不是吗?如果你使用Webpack的话,就不会这样。它支持开箱即用的代码分割特性。在这篇文章中我假定你熟悉Webpack,但如果你不会的话,这里有一篇介绍。为了长话短说,我们也将使用AngularUIRouter和ocLazyLoad。
代码可以在GitHub上。你可以随时fork它。
Webpack的配置
没什么特别的,真的。实际上从你可以直接从文档中复制然后粘贴,唯一的区别是采用了ng-annotate,以让我们的代码保持简洁,以及采用babel来使用一些ECMAScript2015的魔法特性。如果你对ES6感兴趣,可以看看这篇以前的帖子。虽然这些东西都是非常棒的,但是它们都不是实现延迟加载所必需的东西。
//webpack.config.js
varconfig={
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
'usestrict';
exportdefaultrequire('angular')
.module('lazyApp',[
require('angular-ui-router'),
require('oclazyload'),
require('./pages/home/home.routing').name,
require('./pages/messages/messages.routing').name,
]);
路由配置
所有的延迟加载都在路由配置中实现。正如我所说,我们正在使用AngularUIRouter,因为我们需要实现嵌套视图。我们有几个使用案例。我们可以加载整个模块(包括子状态控制器)或每个state加载一个控制器(不去考虑对父级state的依赖)。
加载整个模块
当用户输入/home路径,浏览器就会下载home模块。它包括两个控制器,针对home和home.about这两个state。我们通过state的配置对象中的resolve属性就可以实现延迟加载。得益于Webpack的require.ensure方法,我们可以把home模块创建成第一个代码块。它就叫做1.bundle.js。如果没有$ocLazyLoad.load,我们会发现得到一个错误Argument'HomeController'isnotafunction,gotundefined,因为在Angular的设计中,启动应用之后再加载文件的方式是不可行的。但是$ocLazyLoad.load使得我们可以在启动阶段注册一个模块,然后在它加载完之后再去使用它。
//home.routing.js
'usestrict';
functionhomeRouting($urlRouterProvider,$stateProvider){
$urlRouterProvider.otherwise('/home');
$stateProvider
.state('home',{
url:'/home',
template:require('./views/home.html'),
controller:'HomeControllerasvm',
resolve:{
loadHomeController:($q,$ocLazyLoad)=>{
return$q((resolve)=>{
require.ensure([],()=>{
//loadwholemodule
letmodule=require('./home');
$ocLazyLoad.load({name:'home'});
resolve(module.controller);
});
});
}
}
}).state('home.about',{
url:'/about',
template:require('./views/home.about.html'),
controller:'HomeAboutControllerasvm',
});
}
exportdefaultangular
.module('home.routing',[])
.config(homeRouting);
控制器被当作是模块的依赖。
//home.js
'usestrict';
exportdefaultangular
.module('home',[
require('./controllers/home.controller').name,
require('./controllers/home.about.controller').name
]);
仅加载控制器
我们所做的是向前迈出的第一步,那么我们接着进行下一步。这一次,将没有大的模块,只有精简的控制器。
//messages.routing.js
'usestrict';
functionmessagesRouting($stateProvider){
$stateProvider
.state('messages',{
url:'/messages',
template:require('./views/messages.html'),
controller:'MessagesControllerasvm',
resolve:{
loadMessagesController:($q,$ocLazyLoad)=>{
return$q((resolve)=>{
require.ensure([],()=>{
//loadonlycontrollermodule
letmodule=require('./controllers/messages.controller');
$ocLazyLoad.load({name:module.name});
resolve(module.controller);
})
});
}
}
}).state('messages.all',{
url:'/all',
template:require('./views/messages.all.html'),
controller:'MessagesAllControllerasvm',
resolve:{
loadMessagesAllController:($q,$ocLazyLoad)=>{
return$q((resolve)=>{
require.ensure([],()=>{
//loadonlycontrollermodule
letmodule=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)=>{
//lazyloadtheview
require.ensure([],()=>resolve(require('./views/messages.new.html')));
});
},
controller:'MessagesNewControllerasvm',
resolve:{
loadMessagesNewController:($q,$ocLazyLoad)=>{
return$q((resolve)=>{
require.ensure([],()=>{
//loadonlycontrollermodule
letmodule=require('./controllers/messages.new.controller');
$ocLazyLoad.load({name:module.name});
resolve(module.controller);
})
});
}
}
});
}
exportdefaultangular
.module('messages.routing',[])
.config(messagesRouting);
当心重复的依赖
让我们来看看messages.all.controller和messages.new.controller的内容。
//messages.all.controller.js
'usestrict';
classMessagesAllController{
constructor(msgStore){
this.msgs=msgStore.all();
}
}
exportdefaultangular
.module('messages.all.controller',[
require('commons/msg-store').name,
])
.controller('MessagesAllController',MessagesAllController);
//messages.all.controller.js
'usestrict';
classMessagesNewController{
constructor(msgStore){
this.text='';
this._msgStore=msgStore;
}
create(){
this._msgStore.add(this.text);
this.text='';
}
}
exportdefaultangular
.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
'usestrict';
exportdefaultrequire('angular')
.module('lazyApp',[
require('angular-ui-router'),
require('oclazyload'),
//msgStoreasglobaldependency
require('commons/msg-store').name,
require('./pages/home/home.routing').name,
require('./pages/messages/messages.routing').name,
]);
单元测试的技巧
把msgStore改成是全局依赖并不意味着你应该从控制器中删除它。如果你这样做了,在你编写测试的时候,如果没有模拟这一个依赖,那么它就无法正常工作了。因为在单元测试中,你只会加载这一个控制器而非整个应用模块。
//messages.all.controller.spec.js
'usestrict';
describe('MessagesAllController',()=>{
varcontroller,
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('savesmsgStore.all()inmsgs',()=>{
expect(msgStoreMock.all).toHaveBeenCalled();
expect(controller.msgs).toEqual(['foo',]);
});
});
以上内容是小编给大家分享的Webpack实现AngularJS的延迟加载,希望对大家有所帮助!