Webpack 实现 AngularJS 的延迟加载


Posted in Javascript onMarch 02, 2016

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

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

代码可以在 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 模块。它包括两个控制器,针对 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 使得我们可以在启动阶段注册一个模块,然后在它加载完之后再去使用它。

// 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.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', ]);
controller = $controller('MessagesAllController', { msgStore: msgStoreMock });
}));
it('saves msgStore.all() in msgs', () => {
expect(msgStoreMock.all).toHaveBeenCalled();
expect(controller.msgs).toEqual(['foo', ]);
});
});

以上内容是小编给大家分享的Webpack 实现 AngularJS 的延迟加载,希望对大家有所帮助!

Javascript 相关文章推荐
js停止输出代码
Jul 20 Javascript
javascript jQuery插件练习
Dec 24 Javascript
基于jQuery的日期选择控件
Oct 27 Javascript
Fastest way to build an HTML string(拼装html字符串的最快方法)
Aug 20 Javascript
浅述节点的创建及常见功能的实现
Dec 15 Javascript
jQuery中 bind的用法简单介绍
Feb 13 Javascript
js自定义trim函数实现删除两端空格功能
Feb 09 Javascript
浅谈JS对象添加getter与setter的5种方法
Jun 09 Javascript
Vue-router的使用和出现空白页,路由对象属性详解
Sep 03 Javascript
Vue.js 父子组件通信的十种方式
Oct 30 Javascript
基于javascript实现碰撞检测
Mar 12 Javascript
js实现炫酷光感效果
Sep 05 Javascript
浅谈JS原型对象和原型链
Mar 02 #Javascript
jquery单击事件和双击事件冲突解决方案
Mar 02 #Javascript
jQuery实现只允许输入数字和小数点的方法
Mar 02 #Javascript
jQuery Mobile开发中日期插件Mobiscroll使用说明
Mar 02 #Javascript
javascript求日期差的方法
Mar 02 #Javascript
基于JavaScript实现表单密码的隐藏和显示出来
Mar 02 #Javascript
jQuery判断浏览器并动态调整select宽度的方法
Mar 02 #Javascript
You might like
PHP第一季视频教程(李炎恢+php100 不断更新)
2011/05/29 PHP
PHP表单递交控件名称含有点号(.)会被转化为下划线(_)的处理方法
2013/01/06 PHP
yii2超好用的日期组件和时间组件
2016/05/05 PHP
PHP7内核之Reference详解
2019/03/14 PHP
PHP简单实现图片格式转换(jpg转png,gif转png等)
2019/10/30 PHP
PHPExcel实现的读取多工作表操作示例
2020/04/14 PHP
自己整理的一个javascript日期处理函数
2010/10/16 Javascript
js中单引号与双引号冲突问题解决方法
2013/10/04 Javascript
js中arguments的用法(实例讲解)
2013/11/30 Javascript
JavaScript中实现依赖注入的思路分享
2015/01/15 Javascript
JavaScript将Web页面内容导出到Word及Excel的方法
2015/02/13 Javascript
jQuery实现高亮显示的方法
2015/03/10 Javascript
js实现匹配时换色的输入提示特效代码
2015/08/17 Javascript
angularjs 实现带查找筛选功能的select下拉框实例
2017/01/11 Javascript
Vue组件开发初探
2017/02/14 Javascript
JQuery查找子元素find()和遍历集合each的方法总结
2017/03/07 Javascript
用vue和node写的简易购物车实现
2017/04/25 Javascript
Vue.js在数组中插入重复数据的实现代码
2017/11/17 Javascript
JS设计模式之访问者模式定义与用法分析
2018/02/05 Javascript
laypage.js分页插件使用方法详解
2019/07/27 Javascript
[12:51]71泪洒现场!是DOTA2让经典重现
2014/03/24 DOTA
[46:55]LGD vs Liquid 2019国际邀请赛小组赛 BO2 第一场 8.16
2019/08/19 DOTA
pycharm 使用心得(四)显示行号
2014/06/05 Python
python批量修改文件名的实现代码
2014/09/01 Python
编程语言Python的发展史
2014/09/26 Python
通过代码实例展示Python中列表生成式的用法
2015/03/31 Python
Python编程判断这天是这一年第几天的方法示例
2017/04/18 Python
python对离散变量的one-hot编码方法
2018/07/11 Python
Flask框架路由和视图用法实例分析
2019/11/07 Python
为你的html5网页添加音效示例
2014/04/03 HTML / CSS
体育教师工作总结的自我评价
2013/10/10 职场文书
单位刻章介绍信范文
2014/01/11 职场文书
大学迎新晚会主持词
2014/03/24 职场文书
城市规划应届生推荐信
2014/09/08 职场文书
Jupyter notebook 更改文件打开的默认路径操作
2021/05/21 Python
apache虚拟主机配置的三种方式(小结)
2022/07/23 Servers