第 2 章 数据绑定和第一个AngularJS Web应用

第 2 章 数据绑定和第一个AngularJS Web应用

Hello World

写一个Hello World应用是开始学习AngularJS的最基本途径,让我们从一段简单得不能再简单的HTML开始吧。

随着学习的深入,我们会逐渐深入到AngularJS的内部原理中。现在,让我们先来写一个Hello World应用。

<!DOCTYPE html>
<html ng-app>
<head>
  <title>Simple app</title>
  <script
    src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.13/angular.js"></script>
</head>
<body>
  <input ng-model="name" type="text" placeholder="Your name">
  <h1>Hello {{ name }}</h1>
</body>
</html>

图像说明文字

虽然这个例子不怎么有趣,但它展示了AngularJS最基本也最令人印象深刻的功能之一:数据绑定

2.1 AngularJS中的数据绑定

在Rails等传统Web框架中,控制器将多个模型中的数据和模板组合在一起形成视图,并将其提供给用户,这个组合过程会产生一个单向视图。如果没有创建任何自定义的JavaScript组件,视图只会体现它渲染时模型暴露出的数据。在写这篇文章时,已经出现了好几个可以在视图和模型之间自动进行数据绑定的框架。

AngularJS则采用了完全不同的解决方案。它创建实时模板来代替视图,而不是将数据合并进模板之后更新DOM。任何一个独立视图组件中的值都是动态替换的。这个功能可以说是AngularJS中最重要的功能之一,也是让我们只用10行代码,并且在没有任何JavaScirpt的情况下就可以写出Hello World的关键。

要实现这个功能,只要在HTML页面中引用angular.js,并在某个DOM元素上明确设置ng-app属性即可。ng-app属性声明所有被其包含的内容都属于这个AngularJS应用,这也是我们可以在Web应用中嵌套AngularJS应用的原因。只有被具有ng-app属性的DOM元素包含的元素才会受AngularJS影响。

视图中的插值会在计算一个或多个变量时被动态替换,替换结果是字符串中的插值被变量的值替代。


例如,如果有一个叫做name的变量,它的值是“Ari”,那么视图中的"Hello {{ name }}"字符串会被替换成“Hello Ari”。

自动数据绑定使我们可以将视图理解为模型状态的映射。当客户端的数据模型发生变化时,视图就能反映出这些变化,并且不需要写任何自定义的代码,它就可以工作

在MVC(Model View Controller,模型-视图-控制器)的世界里,控制器可以不必担心会牵扯到渲染视图的工作。这样我们就不必再担心如何分离视图和控制器逻辑,并且也可以使测试变得既简单又令人愉悦。

MVC是一种软件架构设计模式,它将表现从用户交互中分离出来。通常来讲,模型中包含应用的数据和与数据进行交互的方法,视图将数据呈献给用户,而控制器则是二者之间的桥梁。


这种表现分离1能将应用中的对象很好地隔离开来,因此视图不需要知道如何保存对象,只要知道如何显示它即可。这也意味着数据模型不需要同视图进行交互,只需要包含数据和操作视图的方法。控制器用来存放将二者绑定在一起的业务逻辑。

1http://martinfowler.com/eaaDev/SeparatedPresentation.html

AngularJS会记录数据模型所包含的数据在任何特定时间点的值(在Hello World例子中就是name的值),而不是原始值。

当AngularJS认为某个值可能发生变化时,它会运行自己的事件循环来检查这个值是否变“脏”。如果该值从上次事件循环运行之后发生了变化,则该值被认为是“脏”值。这也是Angular可以跟踪和响应应用变化的方式。

这个事件循环会调用$digest()循环,第23章将会详细介绍。

这个过程被称作脏检查(dirty checking)。脏检查是检查数据模型变化的有效手段。当有潜在的变化存在时,AngularJS会在事件循环时执行脏检查(第24章会深入讨论)来保证数据的一致性。

如果使用KnockoutJS这种通过在数据模型上绑定事件监听器来监听数据变化的框架,这个过程会变得更复杂且低效2。处理变化管理、依赖跟踪和大量的事件触发(event firing)是非常复杂的,而且会在应用程序UI性能方面导致额外的问题。

2这有些言过其实,低效是真,复杂未必。——译者注

尽管存在更高效的方式,但脏检查可以运行在所有浏览器中并且是可预测的。此外,很多在速度和效率方面有要求的软件都会使用脏检查的方案3

3比如在游戏开发中就大量使用脏检查技术。——译者注

借助AngularJS,不需要构建复杂和新的JavaScript功能,就可以在视图中实现类自动同步的机制。

为了表示内部和内置的库函数,Angular使用$预定义对象。尽管这类似于全局的jQuery对象$,但它们是完全无关的。只要遇到$符号,你都可以只把它看作一个Angular对象。

2.2 简单的数据绑定

审阅一下上面写的代码,我们使用ng-model指令将内部数据模型对象($scope)中的name属性(property)绑定到了文本输入字段上。

这意味着无论在文本输入字段中输入了什么,都会同步到数据模型中。

数据模型对象(model object)是指$scope对象。$scope对象是一个简单的JavaScript对象,其中的属性可以被视图访问,也可以同控制器进行交互。如果不理解这个概念也没有关系,后面的例子将会对这个概念进行详细说明。

双向数据绑定(bi-directional)意味着如果视图改变了某个值,数据模型会通过脏检查观察到这个变化,而如果数据模型改变了某个值,视图也会依据变化重新渲染。

在输入字段上使用ng-model指令来实现数据绑定,如下所示:

<input ng-model="person.name" type="text" placeholder="Yourname">
<h1>Hello{{ person.name }}</h1>

这样绑定就设置好了(没错,就是这么简单)。我们可以观察一下视图是如何更新数据模型的。当输入字段中的值发生变化时,person.name会被更新,而视图将反映出这个更新。

我们仅通过视图就实现了一个双向数据绑定。为了从其他角度(后端到前端)解释双向数据绑定,后面会深入介绍控制器。

2.3 命名你的应用

你可以通过给ng-app属性分配一个值来命名你的应用。注意,下面的代码中将html标签中的ng-app属性改变成了ng-app="myApp"

从技术上讲,当我们谈到ng-app="myApp"时,我们的意思是告诉Angular在这里我们想要使用哪个模块。更多信息可以参考第3章。

<!DOCTYPE html>
<html ng-app="myApp">
<head>
<title>Simple app</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular.js"></script>
</head>
<body>
    <input ng-model="name" type="text" placeholder="Your name">
    <h1>Hello {{ name }}</h1> </body>
</html>

正如ng-app声明所有被它包含的元素都属于AngularJS应用一样,DOM元素上的ng-controller属性声明所有被它包含的元素都属于某个控制器。

为了解释这个概念,我们将前面的例子修改成如下的样子:

<div ng-controller='MyController'>
    <input ng-model="person.name" type="text" placeholder="Your name">
    <h1>Hello {{ person.name }}</h1>
</div>

那么我们的代码看起来应该像这样:

<!DOCTYPE html>
<html ng-app="myApp">
<head>
<title>Simple app</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular.js"></script>
</head>
<body>
<div ng-controller="MyController">
    <input ng-model="name" type="text" placeholder="Your name">     <h1>Hello {{ name }}</h1>
</div>
</body>
</html>

现在,如果你重新载入你的应用,并且打开浏览器开发者工具中的控制台,那么你应该会看到如下所示来自Angular的错误信息:

Uncaught Error: [$injector:modulerr] Failed to instantiate module myApp due to...

这并不是你的错。Angular只是告诉我们还没有创建myApp模块。现在我们来创建:

angular.module('myApp', [])
.controller('MyController', function($scope) {
    // 稍后填充这里的代码
});

你可以将这些代码放在通过html文档加载的JavaScript文件中或者内联的<script>标签内。这里我会使用<script>标签,但是在真实的应用中你一定会想使用分离的.js文件来组织JavaScript代码。

在上面的代码中,我们告诉Angular希望通过使用angular.module('myApp',[])创建一个名为myApp模块angular.module()函数返回一个模块,接下来一行代码中,我们在模块上调用了.controller()函数。

.controller()函数定义了一个新的控制器。它接受两个参数:

  • 第一个参数MyController是一个字符串,它定义了控制器的名称。
  • 第二个参数是一个函数定义,它定义了这个控制器将如何工作。本书将会对这一类型的函数做更多讨论。

JavaScript允许我们使用句点(.)字符分割新行。也就是说上面的代码与angular.module('myApp', []).controller('MyController', ...)是一样的。(这里我们使用...表示更多没有显示的代码,这并不是真正的JavaScript语法)。

那么完整的示例看起来应该像这样:

<!DOCTYPE html>
<html ng-app="myApp">
<head>
<title>Simple app</title>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular.js"></script>
</head>
<body>
<div ng-controller="MyController">
    <input ng-model="name" type="text" placeholder="Your name">     <h1>Hello {{ name }}</h1>
</div>
<script type="text/javascript">
angular.module('myApp', []) .controller('MyController', function($scope) {
       //  稍后填充这里的代码
});
</script>
</body>
</html>

2.4 滴答的时钟

修改一下示例,让我们的时钟每秒钟滴答一下。

改变MyController为以下代码:

angular.module('myApp', [])
.controller('MyController', function($scope, $timeout) {
    var updateClock = function() {
        $scope.clock = new Date();
        $timeout(function() {
            updateClock();
        }, 1000);
    };
    updateClock();
});

在这个示例中,MyController函数定义接受两个参数,DOM元素的$scope对象和$timeout。从技术上讲这叫作依赖注入。不要担心这个奇怪的术语:它只是一种我们想要使用的require(或者import)其他库的方式。后面我们会学习如何控制它,第13章会使用很多强大的库。

在控制器代码中,定时器的触发会调用updateClock函数,它会将新的$scope.clock变量设置为当前时间。

$timeout接受两个参数:

  • 第一个是一个间歇调用的函数;
  • 第二个是毫秒数,表示间歇调用之间的等待周期。

最终效果是这个$timeout将会每1000毫秒调用一次调用updateClock的函数。

updateClock内,我们给变量$scope.clock赋值为new Date()。这个$scope是你的代码和视图之间的Angular“胶水”。在后面的章节中我们会深入讨论$scope。现在,只需知道当为特殊变量$scope分配一些信息时,你可以在你的HTML视图中访问这些信息。

为了在HTML视图中展示这个$scope.clock,我们使用如下所示的模板标记:

<div ng-controller="MyController">
    <h5>{{clock}}</h5>
</div>

注意这里:在我们的JavaScript代码的MyController中我们给$scope.clock分配了一个值。但是在我们的视图中只指定了clock来读取该值(而不是$scope.clock)。这是因为这个scope隐含在视图中。稍后我们会讨论更多关于它如何工作的信息。

让我们先从上一个示例中移除inputh1标签,此时我们的Web app示例看起来应该像这样:

<!doctype html>
<html ng-app="myApp">
<head>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular.min\ .js"></script>
</head>
<body>
    <div ng-controller="MyController">
    <h1>Hello {{ clock }}!</h1>
</div>
<script type="text/javascript">
angular.module('myApp', [])
.controller('MyController', function($scope, $timeout) {
    var updateClock = function() { 
        $scope.clock = new Date(); 
        $timeout(function() {
             updateClock();
         }, 1000);
    };
    updateClock();
});
</script>
</body>
</html>

在线示例:http://jsbin.com/xavula/1/edit

虽然上面的代码工作在一个独立的文件中,但它导致这个Web app很难与其他人进行协作开发或者很难分离出不同功能的组件。因此不要将所有代码都包含在index.html文件中,通常更好的做法是将JavaScript放在单独的文件中。

那么上面的代码就变成了:

<!doctype html>
<html ng-app="myApp">
<head>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular.min\ .js"></script>
</head>
<body>
    <div ng-controller="MyController">
    <h1>Hello {{ clock }}!</h1>
</div>
<script type="text/javascript" src="js/app.js"></script>
</body>
</html>

我们将会把上面的JavaScript代码放到js/app.js文件中,而不是直接嵌入到HTML中:

// app.js
angular.module('myApp', [])
.controller('MyController', function($scope, $timeout) {
    var updateClock = function() { 
        $scope.clock = new Date(); 
        $timeout(function() {
             updateClock();
         }, 1000);
    };
    updateClock();
});

2.5 数据绑定的最佳实践

由于JavaScript自身的特点,以及它在传递值和引用时的不同处理方式,通常认为,在视图中通过对象的属性而非对象本身来进行引用绑定,是Angular中的最佳实践

如果把这个最佳实践应用到上面时钟的例子中,需要把视图中的代码改写成下面这样:

<!doctype html>
<htmlng-app>
  <head>
    <script src="https://ajax.googleapis.com/ajax/ libs/angularjs/1.2.13/angular.js"></script>
  </head>
  <body>
    <div ng-controller="MyController">
      <h1>Hello {{ clock.now }}!</h1>
    </div>
    <script type="text/javascript" src="js/app.js"></script>
   </body>
</html>

在这个例子中,相比每秒钟都更新$scope.clock,更新clock.now的值会是更好的选择。有了这个优化后,我们将对反映数据变化的逻辑进行如下修改:

// 在app.js中
angular.module('myApp', [])
.controller('MyController', function($scope, $timeout) {
    $scope.clock = {};
    var updateClock = function() {
        $scope.clock.now = new Date();
        $timeout(function() {
            updateClock();
        }, 1000);
    };
    updateClock();
});

将所有绑定都通过这样的形式放在视图中是个非常好的主意。

目录

  • 版权声明
  • 在微博上分享这本书
  • 献词
  • 译者序
  • 引言
  • 第 1 章 初识AngularJS
  • 第 2 章 数据绑定和第一个AngularJS Web应用
  • 第 3 章 模块
  • 第 4 章 作用域
  • 第 5 章 控制器
  • 第 6 章 表达式
  • 第 7 章 过滤器
  • 第 8 章 指令简介
  • 第 9 章 内置指令
  • 第 10 章 指令详解
  • 第 11 章 AngularJS模块加载
  • 第 12 章 多重视图和路由
  • 第 13 章 依赖注入
  • 第 14 章 服务
  • 第 15 章 同外界通信:XHR和服务器通信
  • 第 16 章 XHR实践
  • 第 17 章 promise
  • 第 18 章 服务器通信
  • 第 19 章 测试
  • 第 20 章 事件
  • 第 21 章 架构
  • 第 22 章 Angular动画
  • 第 23 章 digest循环和$apply
  • 第 24 章 揭秘Angular
  • 第 25 章 Angular精华扩展
  • 第 26 章 移动应用
  • 第 27 章 本地化
  • 第 28 章 缓存
  • 第 29 章 安全性
  • 第 30 章 AngularJS和IE浏览器
  • 第 31 章 构建Angular Chrome应用
  • 第 32 章 优化Angular应用
  • 第 33 章 调试AngularJS
  • 第 34 章 下一步
  • 第 35 章 总结
  • ngbook-2014年10月更新