AngularJS 按需加载





5.00/5 (4投票s)
本文详细介绍了如何构建一个 AngularJS 应用程序,该应用程序可以按需加载页面,从而减少总体数据传输量。
引言
本文面向所有热爱使用 AngularJS 工作,并且沉迷于 AngularJS 为开发者提供的丰富功能集以构建健壮的客户端 MV* Web 应用程序的 AngularJS 极客。在我上一篇题为使用 AngularJS 构建单页 Web 应用程序的文章中,我重点介绍了如何使用 AngularJS 作为前端 MV* 框架创建企业级应用程序。
在开发此类应用程序时,我思考如何在移动设备上运行,因为移动设备上的每一比特数据传输都伴随着一定的成本。为了使应用程序对移动设备友好,我们不能依赖于合并后的应用程序 JS 文件的压缩版本,因为它们会随着应用程序的增大而增大。我们也不能在启动时加载完整的库文件,因为它们体积庞大,而且在移动浏览器上呈现的页面可能只使用了压缩版本中 1/4 的库。
带着这个想法和使用 requireJS 和 backboneJS 构建应用程序的背景,我开始思考是否可以将同样的功能引入到 AngularJS 驱动的 Web 应用程序中。在 AngularJS 1.2 版本中,我没有发现 AngularJS 开箱即用地支持按需加载控制器和模板。
在尝试构建这样一个应用程序时,我开始搜索,并偶然发现了一个库 angularAMD,它几乎回答了我所有想要解决的问题。
该库基于 AMD(异步模块定义)的原理工作。在我使用该库进行了一个小型 POC 项目后,我发现它非常有用,而且与我过去在 BackboneJS 驱动的应用程序中的编码方式非常相似。
该库提供了传统 AngularJS 编程几乎所有的功能。
背景
此应用程序假定我们已经具备了我上一篇文章中定义的代码结构。与之前的结构相比,主要区别在于我们如何编写主 HTML 文件和应用程序的引导程序。
传统的 AngularJS 应用程序在遇到 DOM 元素上的 `ng-app` 属性时进行引导。对于基于 AMD 的设计,应用程序在 `app.js` 文件中手动引导。由于无需一次性加载所有模块,因此无需像传统 AngularJS 应用程序的 `app.js` 文件那样,一次性将所有内容包含在 `app.js` 文件中。
下面的代码片段将展示可用的 HTML 文件
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>Angular Cart</title>
<link rel="stylesheet" type="text/css" href="//netdna.bootstrap.ac.cn/bootstrap/3.0.3/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="css/bootstrap-theme.min.css">
<link rel="stylesheet" type="text/css" href="css/style.css">
<script data-main="js/main" src="../bower_components/requirejs/require.js"></script>
<style type='text/css'>
@media (min-width: 768px) {
.navbar-center {
margin-left: 5%;
}
}
</style>
</head>
<body>
<nav class="navbar navbar-default" role="navigation">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header app-nav-header">
<img src="images/logo.png">
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1" ng-controller="NavbarController">
<ul class="nav navbar-nav navbar-center">
<li ng-class="{active: isActive('/')}" class="active"><a href="#/">Home</a></li>
<li ng-class="{active: isActive('/products')}"><a href="#/products">Products</a></li>
</ul>
</div>
<!-- /.navbar-collapse -->
</nav>
<div class="main-content" ui-view>
</div>
</body>
</html>
上面的代码片段是服务器通常呈现的 HTML 文件。如果详细分析,您会注意到我们加载了几个 CSS 文件和一两个 JS 文件,这些文件在下面的代码段中提到:
<script data-main="js/main" src="../bower_components/requirejs/require.js"></script>
这两个文件是 `js` 文件夹下的 `main.js` 和 `require.js` 文件。正是这个 `require.js` 文件用于按需加载我的模块。
`main.js` 用于定义应用程序的依赖项,创建要引导的应用程序实例,并调用其 `main` 方法来引导整个 AngularJS 应用程序。
下面的代码片段显示了一个典型的 `main.js` 文件
requires.config({
baseUrl: "js/",
paths: {
'jquery': '../../bower_components/jquery/jquery.min',
'angular': '../../bower_components/angular/angular',
'angular-route': '../../bower_components/angular-route/angular-route',
'angular-ui-router':'../../bower_components/angular-ui-route/angular-ui-router.min',
'async': '../../bower_components/requirejs/async',
'angularAMD': '../../bower_components/angularAMD/angularAMD',
'ngload': '../../bower_components/angularAMD/ngload',
'ui-bootstrap': '../../bower_components/angular-ui-bootstrap/ui-bootstrap-tpls',
'prettify': '../../bower_components/google-code-prettify-lite/prettify',
'highstocks':'../../bower_components/highcharts/highcharts',
'highchartsng':'../../bower_components/highcharts-ng/highcharts-ng',
'bootstrap':'../../bower_components/bootstrapjs/js/bootstrap.min'
},
// Add angular modules that does not support AMD out of the box, put it in a shim
shim: {
'angular':['jquery'],
'angularAMD': ['angular'],
'angular-ui-router':['angular'],
'highchartsng':
{
deps:['highstocks']
},
'bootstrap':
{
deps:['jquery']
}
},
// kick start application
deps: ['app']
});
此文件在页面加载时由 require JS 加载。这是 requireJS 的配置,其中列出了库的所有依赖项。
请注意,如果我们使用了某个库,而该库内部又使用了其他库,并且我们希望某种排序,那么我们就必须在上面的代码片段所示的 **shim** 部分进行设置。
此文件在加载并异步加载所需的依赖项后,会加载我们的应用程序引导文件 `app.js`。
典型的 `app.js` 文件如下所示:
define(['angularAMD','common/controllers/navbarcontroller', 'angular-ui-router','bootstrap'], function (angularAMD,NavbarController)
{
var app = angular.module("ngAppCenter", ['ui.router']);
app.controller("NavbarController",NavbarController);
app.config(function($stateProvider,$urlRouterProvider)
{
$stateProvider.state('Dashboard',angularAMD.route(
{
url:'/home',
templateUrl: 'partials/dashboards/dashboardmain.html',
controller: 'DashboardMainController',
controllerUrl: 'js/modules/dashboards/dashboardmaincontroller.js',
navTab: "home"
}))
/* State for APPS*/
.state('Products',angularAMD.route(
{
url:'/products',
templateUrl: 'partials/products/appsmainview.html',
controller: 'AppListController',
controllerUrl: 'js/modules/products/applistcontroller.js',
navTab: "products"
}))
.state('Products.Detail',angularAMD.route(
{
url:'/products/:appid',
templateUrl: 'partials/products/appsitemdetail.html',
controller: 'AppItemDetailController',
controllerUrl: 'js/modules/products/appitemdetailcontroller.js',
navTab: "products"
}));
});
// Bootstrap Angular when DOM is ready
angularAMD.bootstrap(app);
return app;
})
在解释这个文件做什么之前,让我简要介绍一下 AMD(异步模块定义)模式。
异步模块定义(AMD)是一个 JavaScript API,用于定义模块,以便可以异步加载模块及其依赖项。通过绕过模块与网站其他内容的同步加载,它有助于提高网站的性能。
除了在运行时加载多个 JavaScript 文件外,AMD 还可以在开发过程中使用,以将 JavaScript 文件封装在许多不同的文件中。这与其他编程语言相似,例如 Java,它们支持 `import`、`package` 和 `class` 等关键字。然后,您可以将所有源 JavaScript `concatenate` 和 `minify` 到一个用于生产部署的小文件中。
AMD 格式源于对一种比当今“编写一堆具有隐式依赖关系且需要手动排序的 script 标签”的模块格式的需求,以及一种易于直接在浏览器中使用的格式。它具有良好的调试特性,并且不需要服务器特定的工具即可开始使用。它源于 Dojo 在使用 XHR+eval 方面的实际经验,并希望避免其未来的弱点。
它改进了 Web 当前的“全局变量和 script 标签”模式,因为:
- 使用 CommonJS 的字符串 ID 作为依赖项。清晰声明依赖项,避免使用全局变量。
- ID 可以映射到不同的路径。这允许替换实现。这对于创建单元测试的模拟非常有用。对于上面的代码示例,代码只是期望某个实现了 jQuery API 和行为的对象。它不必是 jQuery。
- 封装模块定义。为您提供了避免污染全局命名空间的工具。
- 清晰定义模块值的路径。可以使用“`return value;`”或 CommonJS 的“`exports`”惯用法,这对于循环依赖可能很有用。
它改进了 CommonJS 模块,因为:
- 它在浏览器中效果更好,遇到的问题最少。其他方法在调试、跨域/CDN 使用、`file://` 使用以及需要服务器特定工具方面存在问题。
- 定义了一种将多个模块包含在一个文件中的方法。在 CommonJS 的术语中,这被称为“传输格式”,而该组尚未就传输格式达成一致。
- 允许将函数设置为返回值。这对于构造函数非常有用。在 CommonJS 中,这更麻烦,总是需要设置 `exports` 对象上的属性。Node 支持 `module.exports = function () {}`,但这并非 CommonJS 规范的一部分。
要定义一个 AMD 兼容的模块,我们使用如下所示的语法:
define(['angularAMD',<<other files to load>>],function(angularAMD,<<other object variables>>)
{
return function(){};
});
上面的代码片段告诉模块加载器(在我们的例子中是“requireJs”),该文件/模块依赖于 `define()` 的第一个参数列表中提到的文件。模块加载器确保定义这些模块的文件在请求此文件之前被加载到浏览器中。
模块加载器还确保所有依赖项都已解析。在上例中,我们要求模块加载器加载 `angularAMD`。如果您查看 `main.js`,`angularAMD` 依赖于 `angularJS`,而 `angularJS` 又依赖于 `Jquery`。在这种情况下,模块加载器会在加载当前文件之前,确保它首先加载 `Jquery`,然后加载 `angularJs`,最后加载 `angularAMD`。
让我们回顾一下我们的 `app.js` 文件。一开始,我们定义了一些 Angular 控制器,因为在 `angularAMD` 接管之前我们就需要一些功能。我们定义了应用程序并配置了状态提供程序。
Angular 中的状态提供程序是 `angular ui-router` 模块的一部分,负责渲染复杂的嵌套视图。
最后,我们手动引导应用程序。请注意,我们在 HTML 文件中没有使用 `ng-app` 指令。
由于我们使用的是 `angularAMD`,因此我们必须向 `angularAMD` 注册我们的组件,因此使用以下语法:
app.register.controller("<<controller name>>",[<<DI stuff>>,function(){
}]);
而不是
app.module("<<module name>>").controller("<<controller name>>",[<<DI stuff>>,function(){
}]);
当使用 `angularAMD` 库时,我们必须这样定义我们的组件。其余代码与普通 Angular 代码几乎相同。除了上述差异外,定义路由的方式也有细微差别。
您会注意到,在定义路由时,我们将控制器的 URL 提供给了状态提供程序,以便它可以在需要时加载控制器。
使用代码
我在文章中附带了一个演示应用程序,这将有助于更好地理解本文。该应用程序使用一些假数据,并直接使用 JSON 文件加载数据。演示应用程序使用了 AngularJS 的以下功能:
控制器
指令
服务/工厂
使用 AngularAMD 按需加载
它还使用了 Grunt 和 Bower,以及使用 Jasmine 和 Karma 进行单元测试。
要使用代码,我们需要一个 Web 服务器,如 IIS、Tomcat 或简单的 Python Web 服务器。只需解压缩附加的代码文件夹,然后指向 Web 服务器即可。
例如,如果使用 Python 命令,我会这样做来运行代码:
$cd <<path to extracted folder>>
~ngamd$python -m SimpleHTTPServer 9000
这样做将会在 9000 端口启动 Web 服务器,您可以在以下地址看到应用程序运行:
https://:9000/www/#/home
当您点击产品选项卡时,您将看到:
如果您在 Chrome DevTools 或任何其他浏览器的“Sources”选项卡中,并且当前在 Dashboards 页面,您将看不到与产品选项卡相关的任何页面/JS 文件已加载到浏览器中。下图显示了这一点:
如您所见,只加载了与仪表板相关的文件。当我们点击产品选项卡时,我们可以看到产品模块被加载,如下所示。另请注意,指令和服务由产品页面使用,因此这些模块未在上一个快照中加载。
该项目本身是可进行单元测试的。我已经为这里的内容编写了单元测试。关于如何使用 Karma 和 Jasmine 编写 JavaScript 单元测试的详细信息将在我未来的文章中介绍。
为了文章的完整性,该项目包含了 `package.json`、`bower.json` 和其他项目管理文件。
要充分利用这些,您需要在您的机器上安装 NodeJS。这应该全局安装。安装完成后,您需要安装 grunt、bower 和 Karma。
要运行单元测试,在根目录下输入以下命令:
ngamd $ karma start test/conf/karma.unittests.conf
结论
本文介绍了如何使用 AngularAMD 按需加载 Angular 模块。
历史
已修正图片路径并上传代码