angularjs 中文版 - v1.0


前言前言 本教程将向您展示 AngularJS 怎样使得 Web 应用更智能更灵活,而且不需要各种扩展程序或插件。学习 Angul arJS 的一个好方法是逐步完成本教程,它将引导您构建一个完整的 AngularJS Web 应用程序。 通过本教程的学习,您将:通过本教程的学习,您将: 1. 阅读示例学习怎样使用 AngularJS 的客户端数据绑定和依赖注入功能来建立可立即响应用户操作的动态数据 视图。 2. 学习如何使用 AngularJS 创建数据侦听器,且不需要进行 DOM 操作。 3. 学习一种更好、更简单的方法来测试您的 Web 应用程序。 4. 学习如何使用 AngularJS 创建常见的 Web 任务,例如更方便的将数据引入应用程序。 而且这一切可在任何一个浏览器实现,无需配置浏览器! 当你完成本教程后,您将学会:当你完成本教程后,您将学会: 1. 创建一个可在任何浏览器中的工作的动态应用。 2. 了解 AngularJS 与其它 JavaScript 框架之间的区别。 3. 了解 AngularJS 如何实现数据绑定。 4. 利用 AngularJS 的种子项目快速创建自己的项目。 5. 创建和运行测试。 6. 学习更多 AngularJS 标识资源(API)。 本教程部分章节内容来源于 AngularJS 中文社区 (http://www.angularjs.cn/T006) ,官方英文原文请见 Angul arJS Tutorial (https://docs.angularjs.org/tutorial) 。 目录目录 前言前言 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 第 1 章第 1 章 导言导言 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 第 2 章第 2 章 引导程序 - Bootstrapping引导程序 - Bootstrapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 第 3 章第 3 章 静态模版 - Static Template静态模版 - Static Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1111 第 4 章第 4 章 AngularJS 模版 - Angular TemplatesAngularJS 模版 - Angular Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1313 - 为 index.html 添加另一个数据绑定。例如:. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 第 5 章第 5 章 迭代器 - Filtering Repeaters迭代器 - Filtering Repeaters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1919 第 6 章第 6 章 双向绑定 - Two-way Data Binding双向绑定 - Two-way Data Binding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2525 第 7 章第 7 章 XHR 和依赖注入 - XHRs Dependency InjectionXHR 和依赖注入 - XHRs Dependency Injection. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3131 第 8 章第 8 章 连接与图片模版- Templating Links Images连接与图片模版- Templating Links Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3838 第 9 章第 9 章 路由与多视图 - Routing Multiple Views路由与多视图 - Routing Multiple Views . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4242 第 10 章第 10 章 更多模版 - More Templating更多模版 - More Templating . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4848 第 11 章第 11 章 过滤器 - Filters过滤器 - Filters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5353 第 12 章第 12 章 事件处理器 - Event Handlers事件处理器 - Event Handlers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5757 第 13 章第 13 章 REST 和定制服务 - REST and Custom ServicesREST 和定制服务 - REST and Custom Services. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6161 第 14 章第 14 章 动画操作 - Applying Animations动画操作 - Applying Animations. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6767 第 15 章第 15 章 完结篇 - The End完结篇 - The End . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7979 11 导言导言 学习 AngularJS 的一个好方法是逐步完成本教程,它将引导您构建一个完整的 AngularJS Web 应用程序。 该 Web 应用是一个 Android 设备清单的目录列表,您可以筛选列表以便查看您感兴趣的设备,然后查看设备的详 细信息。 本教程将向您展示 AngularJS 怎样使得 Web 应用更智能更灵活,而且不需要各种扩展程序或插件。 通过本教 程的学习,您将: 1. 阅读示例学习怎样使用 AngularJS 的客户端数据绑定和依赖注入功能来建立可立即响应用户操作的动态数据 视图。 2. 学习如何使用 AngularJS 创建数据侦听器,且不需要进行 DOM 操作。 3. 学习一种更好、更简单的方法来测试您的 Web 应用程序。 4. 学习如何使用 AngularJS 创建常见的 Web 任务,例如更方便的将数据引入应用程序。 而且这一切可在任何一个浏览器实现,无需配置浏览器! 当你完成了本教程后,您将学会: 1. 创建一个可在任何浏览器中的工作的动态应用。 2. 了解 AngularJS 与其它 JavaScript 框架之间的区别。 3. 了解 AngularJS 如何实现数据绑定。 第 1 章 导言 | 4 4. 利用 AngularJS 的种子项目快速创建自己的项目。 5. 创建和运行测试。 6. 学习更多 AngularJS 标识资源(API)。 本教程将指导您完成一个简单的应用程序创建过程,包括编写和运行单元测试、不断地测试应用。 教程的每个步 骤为您提供建议以了解更多有关 AngularJS 和您创建的 Web 应用程序。 您可能会在短时间内快速读完本教 程,也可能需要花大量时间深入研究本教程。 如果想看一个简短的 AngularJS 介绍文档,请查看 快速开始 (boo tstrapping.md) 文档。 搭建学习环境搭建学习环境 无论是Mac、Linux或Windows环境中,您均可遵循本教程学习编程。您可以使用源代码管理版本控制系统Git获 取本教程项目的源代码文件,或直接从网上下载本教程项目源代码文件的镜像归档压缩包。 1、您需要安装Node.js和Testacular来运行本项目,请到Node.js (https://nodejs.org/) 官方网站下载并安装最 新版,然后把node可执行程序路径添加到系统环境变量PATH中,完成后在命令行中运行一下命令可以查看是否 安装成功: node -version 然后安装Testacular (http://karma-runner.github.io/0.12/index.html) 单元测试程序,请运行如下命令: npm install -g testacular 2、安装 Git (http://git-scm.com/download) 工具,然后用以下命令从Github复制本教程项目的源代码文件: git clone git://github.com/angular/angular-phonecat.git 您也可以直接从网上下载 (https://github.com/angular/angular-phonecat) 本教程项目源代码的镜像归档压缩 包。这个命令会在您当前文件夹中建立新文件夹angular-phonecatangular-phonecat。 3、最后一件事要做的就是确保您的计算机安装了web浏览器和文本编辑器。 4、进入教程源代码文件包 angular-phonecat,运行服务器后台程序,开始学习 AngularJS (bootstrappin g.md) ! cd angular-phonecat node scripts/web-server.js 第 1 章 导言 | 5 22 引导程序 - Bootstrapping引导程序 - Bootstrapping 我们现在开始准备编写AngularJS应用——phonecatphonecat。这一步骤(步骤0),您将会熟悉重要的源代码文件,学 习启动包含AngularJS种子项目的开发环境,并在浏览器端运行应用。 1.进入angular-phonecat目录,运行如下命令: git checkout -f step-0 该命令将重置phonecat项目的工作目录,建议您在每一学习步骤运行此命令,将命令中的数字改成您学习步骤对 应的数字,该命令将清除您在工作目录内做的任何更改。 2.运行以下命令: node scripts/web-server.js 来启动服务器,启动后命令行终端将会提示 Http Server running at http://localhost:8000Http Server running at http://localhost:8000 ,请不要关闭该终 端,关闭该终端即关闭了服务器。在浏览器中输入http://localhost:8000/app/index.html来访问我们的phonecphonec atat应用。 现在,在浏览器中您应该已经看到了我们的初始应用,很简单,但说明我们的项目已经可以运行了。 应用中显示的“Nothing here yet!”是由如下HTML代码构建而成,代码中包含了AngularJS的关键元素,正是 我们需要学习的。 app/index.htmlapp/index.html My HTML File

Nothing here {{'yet' + '!'}}

代码在做什么呢?代码在做什么呢? ** ng-app 指令**: 第 2 章 引导程序 - Bootstrapping | 7 ng-app 指令标记了AngularJS脚本的作用域,在 中添加 ng-app 属性即说明整个 都是Angul arJS脚本作用域。开发者也可以在局部使用 ng-app 指令,如
,则AngularJS脚本仅在该 中运行。 AngularJS脚本标签:AngularJS脚本标签: 这行代码载入angular.js脚本,当浏览器将整个HTML页面载入完毕后将会执行该angular.js脚本,angular.js脚 本运行后将会寻找含有 ng-app 指令的HTML标签,该标签即定义了AngularJS应用的作用域。 双大括号绑定的表达式:双大括号绑定的表达式:

Nothing here {{'yet' + '!'}}

这行代码演示了AngularJS模板的核心功能——绑定,这个绑定由双大括号 {{}} 和表达式 'yet' + '!' 组成。 这个绑定告诉AngularJS需要运算其中的表达式并将结果插入DOM中,接下来的步骤我们将看到,DOM可以随 着表达式运算结果的改变而实时更新。 AngularJS表达式表达式 (https://docs.angularjs.org/guide/expression) 是一种类似于JavaScript的代码片 段,AngularJS表达式仅在AngularJS的作用域中运行,而不是在整个DOM中运行。 引导AngularJS应用引导AngularJS应用 通过ngApp指令来自动引导AngularJS应用是一种简洁的方式,适合大多数情况。在高级开发中,例如使用脚本 装载应用,您也可以使用bootstrap手动引导AngularJS应用。 AngularJS应用引导过程有3个重要点:AngularJS应用引导过程有3个重要点: 1. 注入器 (https://docs.angularjs.org/api/auto/service/$injector) 将用于创建此应用程序的依赖注入(depe ndency injection); 2. 注入器将会创建根作用域 (https://docs.angularjs.org/api/ng/service/$rootScope) 作为我们应用模型的 范围; 3. AngularJS将会链接根作用域中的DOM,从用ngApp标记的HTML标签开始,逐步处理DOM中指令和绑 定。 一旦AngularJS应用引导完毕,它将继续侦听浏览器的HTML触发事件,如鼠标点击事件、按键事件、HTTP传 入响应等改变DOM模型的事件。这类事件一旦发生,AngularJS将会自动检测变化,并作出相应的处理及更新。 第 2 章 引导程序 - Bootstrapping | 8 上面这个应用的结构非常简单。该模板包仅含一个指令和一个静态绑定,其中的模型也是空的。下一步我们尝试 稍复杂的应用! 图片 2.1图片 2.1 Image of bootstrapping-1.png 我工作目录中这些文件是干什么的?我工作目录中这些文件是干什么的? 上面的应用来自于AngularJS种子项目,我们通常可以使用AngularJS种子项目 (https://github.com/angular/a ngular-seed) 来创建新项目。种子项目包括最新的AngularJS代码库、测试库、脚本和一个简单的应用程序示 例,它包含了开发一个典型的web应用程序所需的基本配置。 对于本教程,我们对AngularJS种子项目进行了下列更改: 1. 删除示例应用程序; 2. 添加手机图像到app/img/phones/; 3. 添加手机数据文件(JSON)到app/phones/; 4. 添加Bootstrap (http://getbootstrap.com/) 文件到app/css/ 和app/img/。 练习练习 试试把关于数学运算的新表达式添加到index.html:

1 + 2 = {{ 1 + 2 }}

第 2 章 引导程序 - Bootstrapping | 9 总结总结 现在让我们转到静态模版 (static-template.md) ,将一些内容添加到web应用程序。 第 2 章 引导程序 - Bootstrapping | 10 33 静态模版 - Static Template静态模版 - Static Template 为了说明AngularJS如何增强了标准HTML,我们先将创建一个静态HTML页面模板,然后把这个静态HTML页 面模板转换成能动态显示的AngularJS模板。 在本步骤中,我们往HTML页面中添加两个手机的基本信息,用以下命令将工作目录重置到步骤1。 git checkout -f step-1 请编辑app/index.html文件,将下面的代码添加到index.html文件中,然后运行该应用查看效果。 app/index.htmlapp/index.html
  • Nexus S

    Fast just got faster with Nexus S.

  • Motorola XOOM™ with Wi-Fi

    The Next, Next Generation tablet.

练习练习 尝试添加多个静态HTML代码到index.html, 例如:

Total number of phones: 2

总结总结 本步骤往应用中添加了静态HTML手机列表, 现在让我们转到AngularJS模版 (angular-templates.md) 以了 解如何使用AngularJS动态生成相同的列表。 第 3 章 静态模版 - Static Template | 12 44 AngularJS 模版 - Angular TemplatesAngularJS 模版 - Angular Templates 是时候给这些网页来点动态特性了——用AngularJS!我们这里为后面要加入的控制器添加了一个测试。 一个应用的代码架构有很多种。对于AngularJS应用,我们鼓励使用模型-视图-控制器(MVC)模式 (http://e n.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) 解耦代码和分离关注点。考虑到这 一点,我们用AngularJS来为我们的应用添加一些模型、视图和控制器。 请重置工作目录: git checkout -f step-2 我们的应用现在有了一个包含三部手机的列表。 步骤1和步骤2之间最重要的不同在下面列出。,你可以到GitHub (https://github.com/angular/angular-phon ecat/compare/step-1...step-2) 去看完整的差别。 视图和模板视图和模板 在AngularJS中,一个视图视图是模型通过HTML模板模板渲染之后的映射。这意味着,不论模型什么时候发生变化,An gularJS会实时更新结合点,随之更新视图。 比如,视图组件被AngularJS用下面这个模板构建出来: ...
  • {{phone.name}}

    {{phone.snippet}}

我们刚刚把静态编码的手机列表替换掉了,因为这里我们使用ngRepeat指令 (https://docs.angularjs.org/api/n g/directive/ngRepeat) 和两个用花括号包裹起来的AngularJS表达式—— {{phone.name}} 和 {{phone.snippe t}} ——能达到同样的效果。 第 4 章 AngularJS 模版 - Angular Templates | 14 • 在
  • 标签里面的 ng-repeat="phone in phones" 语句是一个AngularJS迭代器。这个迭代器告诉Angula rJS用第一个
  • 标签作为模板为列表中的每一部手机创建一个
  • 元素。 • 正如我们在第0步时学到的,包裹在 phone.name 和 phone.snippet 周围的花括号标识着数据绑定。和常量 计算不同的是,这里的表达式实际上是我们应用的一个数据模型引用,这些我们在 PhoneListCtrl 控制器里 面都设置好了。 图片 4.1图片 4.1 Image of angular-templates-1.png 模型和控制器模型和控制器 在 PhoneListCtrl 控制器控制器里面初始化了数据模型数据模型(这里只不过是一个包含了数组的函数,数组中存储的对象是手 机数据列表): app/js/controller.jsapp/js/controller.js: function PhoneListCtrl($scope) { $scope.phones = [ {"name": "Nexus S", "snippet": "Fast just got faster with Nexus S."}, {"name": "Motorola XOOM™ with Wi-Fi", "snippet": "The Next, Next Generation tablet."}, {"name": "MOTOROLA XOOM™", 第 4 章 AngularJS 模版 - Angular Templates | 15 "snippet": "The Next, Next Generation tablet."} ]; } 尽管控制器看起来并没有起到什么控制的作用,但是它在这里起到了至关重要的作用。通过给定我们数据模型的 语境,控制器允许我们建立模型和视图之间的数据绑定。我们是这样把表现层,数据和逻辑部件联系在一起的: • PhoneListCtrl ——控制器方法的名字(在JS文件 controllers.js 中)和 标签里面的ngController (https://docs.angularjs.org/api/ng/directive/ngController) 指令的值相匹配。 • 手机的数据此时与注入到我们控制器函数的作用域作用域( $scope )相关联。当应用启动之后,会有一个根作用 域被创建出来,而控制器的作用域是根作用域的一个典型后继。这个控制器的作用域对所有 标记内部的数据绑定有效。 AngularJS的作用域理论非常重要:一个作用域可以视作模板、模型和控制器协同工作的粘接器。AngularJS使 用作用域,同时还有模板中的信息,数据模型和控制器。这些可以帮助模型和视图分离,但是他们两者确实是同 步的!任何对于模型的更改都会即时反映在视图上;任何在视图上的更改都会被立刻体现在模型中。 想要更加深入理解AngularJS的作用域,请参看AngularJS作用域文档 (https://docs.angularjs.org/api/ng/typ e/$rootScope.Scope) 。 测试测试 “AngularJS方式”让开发时代码测试变得十分简单。让我们来瞅一眼下面这个为控制器新添加的单元测试: test/unit/controllersSpec.js:test/unit/controllersSpec.js: describe('PhoneCat controllers', function() { describe('PhoneListCtrl', function(){ it('should create "phones" model with 3 phones', function() { var scope = {}, ctrl = new PhoneListCtrl(scope); expect(scope.phones.length).toBe(3); }); }); }); 第 4 章 AngularJS 模版 - Angular Templates | 16 这个测试验证了我们的手机数组里面有三条记录(暂时无需弄明白这个测试脚本)。这个例子显示出为AngularJ S的代码创建一个单元测试是多么的容易。正因为测试在软件开发中是必不可少的环节,所以我们使得在Angular JS可以轻易地构建测试,来鼓励开发者多写它们。 在写测试的时候,AngularJS的开发者倾向于使用Jasmine行为驱动开发(BBD)框架中的语法。尽管Angular JS没有强迫你使用Jasmine,但是我们在教程里面所有的测试都使用Jasmine编写。你可以在Jasmine的Jasmi ne Wiki (https://github.com/pivotal/jasmine/wiki) 上获得相关知识。 基于AngularJS的项目被预先配置为使用[JsTestDriver][]来运行单元测试。你可以像下面这样运行测试: 1. 在一个单独的终端上,进入到 angular-phonecat 目录并且运行 ./scripts/test-server.sh 来启动测试(Win dows命令行下请输入 .\scripts\test-server.bat 来运行脚本,后面脚本命令运行方式类似); 2. 打开一个新的浏览器窗口,并且转到http://localhost:9876 ; 3. 选择“Capture this browser in strict mode”。 这个时候,你可以抛开你的窗口不管然后把这事忘了。JsTestDriver会自己把测试跑完并且把结果输出在你 的终端里。 4. 运行 ./scripts/test.sh 进行测试 。 你应当看到类似于如下的结果: Chrome: Runner reset. . Total 1 tests (Passed: 1; Fails: 0; Errors: 0) (2.00 ms) Chrome 19.0.1084.36 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (2.00 ms) 耶!测试通过了!或者没有... 注意:如果在你运行测试之后发生了错误,关闭浏览器然后回到终端关了脚本,然后在重新来一边上面的 练习练习 第 4 章 AngularJS 模版 - Angular Templates | 17 - 为- 为 index.htmlindex.html 添加另一个数据绑定。例如:添加另一个数据绑定。例如:

    Total number of phones: {{phones.length}}

    • 创建一个新的数据模型属性,并且把它绑定到模板上。例如: $scope.hello = "Hello, World!" 更新你的浏览器,确保显示出来“Hello, World!” • 用一个迭代器创建一个简单的表:
    row number
    {{i}}
    现在让数据模型表达式的`i`增加1:
    row number
    {{i+1}}
    • 确定把 toBe(3) 改成 toBe(4) 之后单元测试失败,然后重新跑一遍 ./scripts/test.sh 脚本 总结总结 你现在拥有一个模型,视图,控制器分离的动态应用了,并且你随时进行了测试。现在,你可以进入到迭代器 (filt ering-repeaters.md) 来为应用加入全文检索功能了。 第 4 章 AngularJS 模版 - Angular Templates | 18 55 迭代器 - Filtering Repeaters迭代器 - Filtering Repeaters 我们在上一步做了很多基础性的训练,所以现在我们可以来做一些简单的事情喽。我们要加入全文检索功能(没 错,这个真的非常简单!)。同时,我们也会写一个端到端测试,因为一个好的端到端测试可以帮上很大忙。它 监视着你的应用,并且在发生回归的时候迅速报告。 请重置工作目录: git checkout -f step-3 我们的应用现在有了一个搜索框。注意到页面上的手机列表随着用户在搜索框中的输入而变化。 步骤2和步骤3之间最重要的不同在下面列出。你可以在GitHub (https://github.com/angular/angular-phonec at/compare/step-2...step-3) 里看到完整的差别。 控制器控制器 我们对控制器不做任何修改。 模板模板 app/index.htmlapp/index.html
    Search:
    • {{phone.name}}

      {{phone.snippet}}

    第 5 章 迭代器 - Filtering Repeaters | 20 我们现在添加了一个 标签,并且使用AngularJS的$filter (https://docs.angularjs.org/api/ng/filter/filt er) 函数来处理ngRepeat (https://docs.angularjs.org/api/ng/directive/ngRepeat) 指令的输入。 这样允许用户输入一个搜索条件,立刻就能看到对电话列表的搜索结果。我们来解释一下新的代码: • 数据绑定: 这是AngularJS的一个核心特性。当页面加载的时候,AngularJS会根据输入框的属性值名 字,将其与数据模型中相同名字的变量绑定在一起,以确保两者的同步性。 在这段代码中,用户在输入框中输入的数据名字称作 query ,会立刻作为列表迭代器( phone in phones | fi lter: query`)其过滤器的输入。当数据模型引起迭代器输入变化的时候,迭代器可以高效得更新DOM将数 据模型最新的状态反映出来。 图片 5.1图片 5.1 Image of filtering-repeaters-1.png • 使用 filter 过滤器:filter (https://docs.angularjs.org/api/ng/filter/filter) 函数使用 query 的值来创建一个 只包含匹配 query 记录的新数组。 ngRepeat 会根据 filter 过滤器生成的手机记录数据数组来自动更新视图。整个过程对于开发者来说都是透 明的。 第 5 章 迭代器 - Filtering Repeaters | 21 测试测试 在步骤2,我们学习了编写和运行一个测试的方法。单元测试用来测试我们用js编写的控制器和其他组件都非常方 便,但是不能方便的对DOM操作和应用集成进行测试。对于这些来说,端到端测试是一个更好的选择。 搜索特性是完全通过模板和数据绑定实现的,所以我们的第一个端到端测试就来验证这些特性是否符合我们的预 期。 test/e2e/scenarios.js:test/e2e/scenarios.js: describe('PhoneCat App', function() { describe('Phone list view', function() { beforeEach(function() { browser().navigateTo('../../app/index.html'); }); it('should filter the phone list as user types into the search box', function() { expect(repeater('.phones li').count()).toBe(3); input('query').enter('nexus'); expect(repeater('.phones li').count()).toBe(1); input('query').enter('motorola'); expect(repeater('.phones li').count()).toBe(2); }); }); }); 尽管这段测试代码的语法看起来和我们之前用Jasmine写的单元测试非常像,但是端到端测试使用的是AngularJ S端到端测试器 (https://code.angularjs.org/1.1.0/docs/guide/dev_guide.e2e-testing) 提供的接口。 运行一个端到端测试,在浏览器新标签页中打开下面任意一个: • node.js用户: http://localhost:8000/test/e2e/runner.html • 使用其他http服务器的用户: http://localhost:[port-number]/[context-path]/test/e2e/runner.html 这个测试验证了搜素框和迭代器被正确地集成起来。你可以发现,在AngularJS里写一个端到端测试多么的简 单。尽管这个例子仅仅是一个简单的测试,但是用它来构建任何一个复杂、可读的端到端测试都很容易。 第 5 章 迭代器 - Filtering Repeaters | 22 练习练习 • 在 index.html 模板中添加一个 {{query}} 绑定来实时显示 query 模型的当前值,然后观察他们是如何根据输 入框中的值而变化。 • 现在我们来看一下我们怎么让 query 模型的值出现在HTML的页面标题上。 你或许认为像下面这样在 title 标签上加上一个绑定就行了: Google Phone Gallery: {{query}} 但是,当你重载页面的时候,你根本没办法得到期望的结果。这是因为 query 模型仅仅在 body 元素定义的作用 域内才有效。 如果你想让 元素绑定上 query 模型,你必须把 ngController 声明移动移动到 HTML 元素上,因为它是 title 和 body 元素的共同祖先。 <html ng-app ng-controller="PhoneListCtrl"> 一定要注意把 body 元素上的 ng-controller 声明给删了。 当绑定两个花括号在 title 元素上可以实现我们的目标,但是你或许发现了,页面正加载的时候它们已经显示给用 户看了。一个更好的解决方案是使用ngBind (https://docs.angularjs.org/api/ng/directive/ngBind) 或者ngBin dTemplate (https://docs.angularjs.org/api/ng/directive/ngBindTemplate) 指令,它们在页面加载时对用户 是不可见的: <title ng-bind-template="Google Phone Gallery: {{query}}">Google Phone Gallery • 在 test/e2e/scenarios.js 的 describe 块中加入下面这些端到端测试代码: it('should display the current filter value within an element with id "status"', function() { expect(element('#status').text()).toMatch(/Current filter: \s*$/); input('query').enter('nexus'); expect(element('#status').text()).toMatch(/Current filter: nexus\s*$/); //alternative version of the last assertion that tests just the value of the binding using('#status').expect(binding('query')).toBe('nexus'); }); 刷新浏览器,端到端测试器会报告测试失败。为了让测试通过,编辑 index.html ,添加一个 id为“status” 的 d iv 或者 p 元素,内容是一个 query 绑定,再加上 Current filter: 前缀。例如: 第 5 章 迭代器 - Filtering Repeaters | 23
    Current filter: {{query}}
    • 在端到端测试里面加一条 pause(); 语句,重新跑一遍。你将发现测试器暂停了!这样允许你有机会在测试运 行过程中查看你应用的状态。测试应用是实时的!你可以更换搜索内容来证明。稍有经验你就会知道,这对 于在端到端测试中迅速找到问题是多么的关键。 总结总结 我们现在添加了全文搜索功能,并且完成一个测试证明了搜索是对的!现在让我们继续到双向绑定 (two-way-d ata-binding.md) 来看看给我们的手机应用增加排序功能。 第 5 章 迭代器 - Filtering Repeaters | 24 66 双向绑定 - Two-way Data Binding双向绑定 - Two-way Data Binding 在这一步你会增加一个让用户控制手机列表显示顺序的特性。动态排序可以这样实现,添加一个新的模型属 性,把它和迭代器集成起来,然后让数据绑定完成剩下的事情。 请重置工作目录: git checkout -f step-4 你应该发现除了搜索框之外,你的应用多了一个下来菜单,它可以允许控制电话排列的顺序。 步骤3和步骤4之间最重要的不同在下面列出。你可以在GitHub (https://github.com/angular/angular-phonec at/compare/step-3...step-4) 里看到完整的差别。 模板模板 app/index.htmlapp/index.html Search: Sort by:
    • {{phone.name}}

      {{phone.snippet}}

    我们在index.htmlindex.html中做了如下更改: • 首先,我们增加了一个叫做 orderProp 的 Sort by:
  • 同时我们为手机详细信息视图添加一个占位模板。 app/partials/phone-detail.htmlapp/partials/phone-detail.html TBD: detail view for {{phoneId}} 注意到我们的布局模板中没再添加 PhoneListCtrl 或 PhoneDetailCtrl 控制器属性! 第 9 章 路由与多视图 - Routing Multiple Views | 46 测试测试 为了自动验证所有的东西都良好地集成起来,我们需要写一些端到端测试,导航到不同的URL上然后验证正确地 视图被渲染出来。 ... it('should redirect index.html to index.html#/phones', function() { browser().navigateTo('../../app/index.html'); expect(browser().location().url()).toBe('/phones'); }); ... describe('Phone detail view', function() { beforeEach(function() { browser().navigateTo('../../app/index.html#/phones/nexus-s'); }); it('should display placeholder page with phoneId', function() { expect(binding('phoneId')).toBe('nexus-s'); }); }); 你现在可以刷新你的浏览器,然后重新跑一遍端到端测试。 练习练习 试着在 index.html 上增加一个 {{orderProp}} 绑定,当你在手机列表视图上时什么也没变。这是因为 orderPro p 模型仅仅在 PhoneListCtrl 管理的作用域下才是可见的,这与
    元素相关。如果你在 phone-lis t.html 模板中加入同样的绑定,那么这个绑定会按你设想的那样被渲染出来。 总结总结 设置路由并实现手机列表视图之后,我们已经可以进入更多模版 (more-templating.md) 来实现手机详细信息视 图了。 第 9 章 路由与多视图 - Routing Multiple Views | 47 1010 更多模版 - More Templating更多模版 - More Templating 在这一步,你将实现手机详细信息视图,这个视图会在用户点击手机列表中的一部手机时被显示出来。 请重置工作目录: git checkout -f step-8 现在当你点击列表中的一部手机之后,这部手机的详细信息页面就会被显示出来。 为了实现手机详细信息视图我们将会使用$http (https://code.angularjs.org/1.1.0/docs/api/ng.$http) 来获取数 据,同时我们也要增添一个 phone-detail.html 视图模板。 步骤7和步骤8之间最重要的不同在下面列出。你可以在GitHub (https://github.com/angular/angular-phonec at/compare/step-7...step-8) 里看到完整的差别。 数据数据 除了 phones.json , app/phones/ 目录也包含了每一部手机信息的json文件。 app/phones/nexus-s.jsonapp/phones/nexus-s.json(样例片段) { "additionalFeatures": "Contour Display, Near Field Communications (NFC),...", "android": { "os": "Android 2.3", "ui": "Android" }, ... "images": [ "img/phones/nexus-s.0.jpg", "img/phones/nexus-s.1.jpg", "img/phones/nexus-s.2.jpg", "img/phones/nexus-s.3.jpg" ], "storage": { "flash": "16384MB", "ram": "512MB" } } 这些文件中的每一个都用相同的数据结构描述了一部手机的不同属性。我们会在手机详细信息视图中显示这些数 据。 第 10 章 更多模版 - More Templating | 49 控制器控制器 我们使用 $http 服务获取数据,以此来拓展我们的 PhoneListCtrl 。这和之前的手机列表控制器的工作方式是一 样的。 app/js/controllers.jsapp/js/controllers.js function PhoneDetailCtrl($scope, $routeParams, $http) { $http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) { $scope.phone = data; }); } //PhoneDetailCtrl.$inject = ['$scope', '$routeParams', '$http']; 为了构造HTTP请求的URL,我们需要 $route 服务提供的当前路由中抽取 $routeParams.phoneId 。 模板模板 phone-detail.htmlphone-detail.html文件中原有的TBD占位行已经被列表和构成手机详细信息的绑定替换掉了。注意到,这里我 们使用AngularJS的 {{表达式}} 标记和 ngRepeat 来在视图中渲染数据模型。 app/partials/phone-detail.htmlapp/partials/phone-detail.html

    {{phone.name}}

    {{phone.description}}

    • Availability and Networks
      Availability
      {{availability}}
      第 10 章 更多模版 - More Templating | 50
    • ... Additional Features
      {{phone.additionalFeatures}}
    测试测试 我们来写一个新的单元测试,这个测试和我们在步骤5中为 PhoneListCtrl 写的那个很像。 test/unit/controllersSpec.jstest/unit/controllersSpec.js ... describe('PhoneDetailCtrl', function(){ var scope, $httpBackend, ctrl; beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) { $httpBackend = _$httpBackend_; $httpBackend.expectGET('phones/xyz.json').respond({name:'phone xyz'}); $routeParams.phoneId = 'xyz'; scope = $rootScope.$new(); ctrl = $controller(PhoneDetailCtrl, {$scope: scope}); })); it('should fetch phone detail', function() { expect(scope.phone).toBeUndefined(); $httpBackend.flush(); expect(scope.phone).toEqual({name:'phone xyz'}); }); }); ... 执行`./scripts/test.sh`脚本来执行测试,你应该会看到如下输出: Chrome: Runner reset. ... Total 3 tests (Passed: 3; Fails: 0; Errors: 0) (5.00 ms) Chrome 19.0.1084.36 Mac OS: Run 3 tests (Passed: 3; Fails: 0; Errors 0) (5.00 ms) 第 10 章 更多模版 - More Templating | 51 同时,我们也添加一个端到端测试,指向Nexus S手机详细信息页面并且验证页面的头部是“Nexus S”。 test/e2e/scenarios.jstest/e2e/scenarios.js ... describe('Phone detail view', function() { beforeEach(function() { browser().navigateTo('../../app/index.html#/phones/nexus-s'); }); it('should display nexus-s page', function() { expect(binding('phone.name')).toBe('Nexus S'); }); }); ... 你现在可以刷新你的浏览器,然后重新跑一遍端到端测试。 练习练习 使用AngularJS端到端测试API (https://code.angularjs.org/1.1.0/docs/guide/dev_guide.e2e-testing) 写一 个测试,用它来验证我们在Nexus S详细信息页面显示的四个缩略图。 总结总结 现在手机详细页面已经就绪了,在过滤器 (filters.md) 中我们将学习如何写一个显示过滤器。 第 10 章 更多模版 - More Templating | 52 1111 过滤器 - Filters过滤器 - Filters 在这一步你将学习到如何创建自己的显示过滤器。 请重置工作目录: git checkout -f step-9 现在转到一个手机详细信息页面。在上一步,手机详细页面显示“true”或者“false”来说明某个手机是否具有 特定的特性。现在我们使用一个定制的过滤器来把那些文本串图形化:√作为“true”;以及×作为“fals e”。来让我们看看过滤器代码长什么样子。 步骤8和步骤9之间最重要的不同在下面列出。你可以在 [GitHub][https://github.com/angular/angular-phone cat/compare/step-8...step-9] 里看到完整的差别。 定制过滤器定制过滤器 为了创建一个新的过滤器,先创建一个 phonecatFilters 模块,并且将定制的过滤器注册给这个模块。 app/js/filters.jsapp/js/filters.js angular.module('phonecatFilters', []).filter('checkmark', function() { return function(input) { return input ? '\u2713' : '\u2718'; }; }); 我们的过滤器命名为checkmarkcheckmark。它的输入要么是 true ,要么是 false ,并且我们返回两个表示true或false的 unicode字符( \u2713和\u2718 )。 现在我们的过滤器准备好了,我们需要将我们的 phonecatFilters 模块作为一个依赖注册到我们的主模块 phone cat 上。 app/js/app/jsapp/js/app/js ... angular.module('phonecat', ['phonecatFilters']). ... 模板模板 由于我们的模板代码写在 app/js/filter.js 文件中,所以我们需要在布局模板中引入这个文件。 app/index.htmlapp/index.html 第 11 章 过滤器 - Filters | 54 ... ... 在AngularJS模板中使用过滤器的语法是: {{ expression | filter }} 我们把过滤器应用到手机详细信息模板中: app/partials/phone-detail.htmlapp/partials/phone-detail.html ...
    Infrared
    {{phone.connectivity.infrared | checkmark}}
    GPS
    {{phone.connectivity.gps | checkmark}}
    ... 测试测试 过滤器和其他组件一样,应该被测试,并且这些测试实际上很容易完成。 test/unit/filtersSpec.jstest/unit/filtersSpec.js describe('filter', function() { beforeEach(module('phonecatFilters')); describe('checkmark', function() { it('should convert boolean values to unicode checkmark or cross', inject(function(checkmarkFilter) { expect(checkmarkFilter(true)).toBe('\u2713'); expect(checkmarkFilter(false)).toBe('\u2718'); })); }); }); 注意在执行任何过滤器测试之前,你需要为 phonecatFilters 模块配置我们的测试注入器。 第 11 章 过滤器 - Filters | 55 执行 ./scripts/test/sh 运行测试,你应该会看到如下的输出: Chrome: Runner reset. .... Total 4 tests (Passed: 4; Fails: 0; Errors: 0) (3.00 ms) Chrome 19.0.1084.36 Mac OS: Run 4 tests (Passed: 4; Fails: 0; Errors 0) (3.00 ms) 练习练习 • 现在让我们来练习一下[AngularJS内置过滤器][ng.$filter],在index.htmlindex.html中加入如下绑定: • {{ "lower cap string" | uppercase }} • {{ {foo: "bar", baz: 23} | json }} • {{ 1304375948024 | date }} • {{ 1304375948024 | date:"MM/dd/yyyy @ h:mma" }} • 我们也可以用一个输入框来创建一个模型,并且将之与一个过滤后的绑定结合在一起。在index.htmlindex.html中加入 如下代码: Uppercased: {{ userInput | uppercase }} 总结总结 现在你已经知道了如何编写和测试一个定制化插件,在 事件处理器 (event-handlers.md) 中我们会学习如何用 AngularJS 继续丰富我们的手机详细信息页面。 第 11 章 过滤器 - Filters | 56 1212 事件处理器 - Event Handlers事件处理器 - Event Handlers 在这一步,你会在手机详细信息页面让手机图片可以点击。 请重置工作目录: git checkout -f step-10 手机详细信息视图展示了一幅当前手机的大号图片,以及几个小一点的缩略图。如果用户点击缩略图就能把那张 大的替换成自己那就更好了。现在我们来看看如何用AngularJS来实现它。 步骤9和步骤10之间最重要的不同在下面列出。你可以在 GitHub (https://github.com/angular/angular-phone cat/compare/step-9...step-10) 里看到完整的差别。 控制器控制器 app/js/controllers.jsapp/js/controllers.js ... function PhoneDetailCtrl($scope, $routeParams, $http) { $http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) { $scope.phone = data; $scope.mainImageUrl = data.images[0]; }); $scope.setImage = function(imageUrl) { $scope.mainImageUrl = imageUrl; } } //PhoneDetailCtrl.$inject = ['$scope', '$routeParams', '$http']; 在 PhoneDetailCtrl 控制器中,我们创建了 mainImageUrl 模型属性,并且把它的默认值设为第一个手机图片的 URL。 模板模板 app/partials/phone-detail.htmlapp/partials/phone-detail.html ...
      第 12 章 事件处理器 - Event Handlers | 58
    ... 我们把大图片的 ngSrc 指令绑定到 mainImageUrl 属性上。 同时我们注册一个[ngClick][ng.directive:ngClick]处理器到缩略图上。当一个用户点击缩略图的任意一个时,这 个处理器会使用 setImage 事件处理函数来把 mainImageUrl 属性设置成选定缩略图的URL。 测试测试 为了验证这个新特性,我们添加了两个端到端测试。一个验证主图片被默认设置成第一个手机图片。第二个测试 点击几个缩略图并且验证主图片随之合理的变化。 test/e2e/scenarios.jstest/e2e/scenarios.js ... describe('Phone detail view', function() { ... it('should display the first phone image as the main phone image', function() { expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.0.jpg'); }); it('should swap main image if a thumbnail image is clicked on', function() { element('.phone-thumbs li:nth-child(3) img').click(); expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.2.jpg'); element('.phone-thumbs li:nth-child(1) img').click(); expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.0.jpg'); }); }); }); 你现在可以刷新你的浏览器,然后重新跑一遍端到端测试。 练习练习 为 PhoneDetailCtrl 添加一个新的控制器方法: 第 12 章 事件处理器 - Event Handlers | 59 $scope.hello = function(name) { alert('Hello ' + (name || 'world') + '!'); } 并且添加: 到phone-details.htmlphone-details.html模板。 总结总结 现在图片浏览器已经做好了,我们已经为 REST 和定制服务 (rets-and-custom-services.md) (最后一步 啦!)做好了准备,我们会学习用一种更加优雅的方式来获取数据。 第 12 章 事件处理器 - Event Handlers | 60 1313 REST 和定制服务 - REST and Custom ServiREST 和定制服务 - REST and Custom Servi cesces 在这一步中,我们会改进我们APP获取数据的方式。 请重置工作目录: git checkout -f step-11 对我们应用所做的最后一个改进就是定义一个代表 [RESTful][http://en.wikipedia.org/wiki/Representationa l_State_Transfer] 客户端的定制服务。有了这个客户端我们可以用一种更简单的方式来发送XHR请求,而不用 去关心更底层的 $http (https://docs.angularjs.org/api/ng/service/$http) 服务(API、HTTP方法和URL)。 步骤9和步骤10之间最重要的不同在下面列出。你可以在 [GitHub][https://github.com/angular/angular-phon ecat/compare/step-10...step-11] 里看到完整的差别。 模板模板 定制的服务被定义在app/js/servicesapp/js/services,所以我们需要在布局模板中引入这个文件。另外,我们也要加载angularjangularj s-resource.jss-resource.js这个文件,它包含了 ngResource 模块以及其中的 $resource 服务,我们一会就会用到它们: app/index.htmlapp/index.html ... ... 服务服务 app/js/services.jsapp/js/services.js angular.module('phonecatServices', ['ngResource']). factory('Phone', function($resource){ return $resource('phones/:phoneId.json', {}, { query: {method:'GET', params:{phoneId:'phones'}, isArray:true} }); }); 我们使用模块API通过一个工厂方法注册了一个定制服务。我们传入服务的名字 Phone 和工厂函数。工厂函数和 控制器构造函数差不多,它们都通过函数参数声明依赖服务。Phone服务声明了它依赖于 $resource 服务。 第 13 章 REST 和定制服务 - REST and Custom Services | 62 [$resource][ngResource.$resource]服务使得用短短的几行代码就可以创建一个 [RESTful][http://en.wikipe dia.org/wiki/Representational_State_Transfer] 客户端。我们的应用使用这个客户端来代替底层的[$http][n g.$http]服务。 app/js/app.jsapp/js/app.js ... angular.module('phonecat', ['phonecatFilters', 'phonecatServices']). ... 我们需要把 phonecatServices 添加到 phonecat 的依赖数组里。 控制器控制器 通过重构掉底层的[$http][ng.$http]服务,把它放在一个新的服务 Phone 中,我们可以大大简化子控制器( Ph oneListCtrl 和 PhoneDetailCtrl )。AngularJS的[$resource][ngResource.$resource]相比于 $http 更加适 合于与RESTful数据源交互。而且现在我们更容易理解控制器这些代码在干什么了。 app/js/controllers.jsapp/js/controllers.js ... function PhoneListCtrl($scope, Phone) { $scope.phones = Phone.query(); $scope.orderProp = 'age'; } //PhoneListCtrl.$inject = ['$scope', 'Phone']; function PhoneDetailCtrl($scope, $routeParams, Phone) { $scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) { $scope.mainImageUrl = phone.images[0]; }); $scope.setImage = function(imageUrl) { $scope.mainImageUrl = imageUrl; } } //PhoneDetailCtrl.$inject = ['$scope', '$routeParams', 'Phone']; 第 13 章 REST 和定制服务 - REST and Custom Services | 63 注意到,在`PhoneListCtrl`里我们把: $http.get('phones/phones.json').success(function(data) { $scope.phones = data; }); 换成了: $scope.phones = Phone.query(); 我们通过这条简单的语句来查询所有的手机。 另一个非常需要注意的是,在上面的代码里面,当调用Phone服务的方法是我们并没有传递任何回调函数。尽管 这看起来结果是同步返回的,其实根本就不是。被同步返回的是一个“future”——一个对象,当XHR相应返回 的时候会填充进数据。鉴于AngularJS的数据绑定,我们可以使用future并且把它绑定到我们的模板上。然 后,当数据到达时,我们的视图会自动更新。 有的时候,单单依赖future对象和数据绑定不足以满足我们的需求,所以在这些情况下,我们需要添加一个回调 函数来处理服务器的响应。 PhoneDetailCtrl 控制器通过在一个回调函数中设置 mainImageUrl 就是一个解释。 测试测试 修改我们的单元测试来验证我们新的服务会发起HTTP请求并且按照预期地处理它们。测试同时也检查了我们的 控制器是否与服务正确协作。 [$resource][ngResource.$resource]服务通过添加更新和删除资源的方法来增强响应得到的对象。如果我们打 算使用 toEqual 匹配器,我们的测试会失败,因为测试值并不会和响应完全等同。为了解决这个问题,我们需要 使用一个最近定义的 toEqualData [Jasmine匹配器][]。当 toEqualData 匹配器比较两个对象的时候,它只考虑 对象的属性而忽略掉所有的方法。 test/unit/controllersSpec.js:test/unit/controllersSpec.js: describe('PhoneCat controllers', function() { beforeEach(function(){ this.addMatchers({ toEqualData: function(expected) { return angular.equals(this.actual, expected); } }); }); 第 13 章 REST 和定制服务 - REST and Custom Services | 64 beforeEach(module('phonecatServices')); describe('PhoneListCtrl', function(){ var scope, ctrl, $httpBackend; beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) { $httpBackend = _$httpBackend_; $httpBackend.expectGET('phones/phones.json'). respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]); scope = $rootScope.$new(); ctrl = $controller(PhoneListCtrl, {$scope: scope}); })); it('should create "phones" model with 2 phones fetched from xhr', function() { expect(scope.phones).toEqual([]); $httpBackend.flush(); expect(scope.phones).toEqualData( [{name: 'Nexus S'}, {name: 'Motorola DROID'}]); }); it('should set the default value of orderProp model', function() { expect(scope.orderProp).toBe('age'); }); }); describe('PhoneDetailCtrl', function(){ var scope, $httpBackend, ctrl, xyzPhoneData = function() { return { name: 'phone xyz', images: ['image/url1.png', 'image/url2.png'] } }; beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) { $httpBackend = _$httpBackend_; $httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData()); $routeParams.phoneId = 'xyz'; scope = $rootScope.$new(); ctrl = $controller(PhoneDetailCtrl, {$scope: scope}); })); 第 13 章 REST 和定制服务 - REST and Custom Services | 65 it('should fetch phone detail', function() { expect(scope.phone).toEqualData({}); $httpBackend.flush(); expect(scope.phone).toEqualData(xyzPhoneData()); }); }); }); 执行 ./scripts/test.sh 运行测试,你应该会看到如下的输出: Chrome: Runner reset. .... Total 4 tests (Passed: 4; Fails: 0; Errors: 0) (3.00 ms) Chrome 19.0.1084.36 Mac OS: Run 4 tests (Passed: 4; Fails: 0; Errors 0) (3.00 ms) 总结总结 现在我们已经看到了如何建立一个定制服务REST,我们已经准备好了动画操作 (applying-animations.md) (最后一步!),以了解如何改善这个应用程序的动画。 第 13 章 REST 和定制服务 - REST and Custom Services | 66 1414 动画操作 - Applying Animations动画操作 - Applying Animations 最后一步,我们通过在之前创建的模板代码上方附加CSS和JavaScript动画来加强phonecat web应用。 • 现在我们使用 ngAnimate 模块来保证动画可以贯穿这个应用。 • 另外我们使用常用的 ng 指令来自动触发使动画接入的挂钩。 • 当发现一个动画之后,这个动画就会在标准的DOM操作之间运行,该操作在给定的时间中发布在元素上(如 在 ngRepeat 上插入或删除节点或者在 ngClass 上添加或删除类)。 工作区复位说明: git checkout -f step-11 下面列出几种最重要的变化。你可以在 GitHub (https://github.com/angular/angular-phonecat/compare/st ep-11...step-12) 上浏览全部差异。 依赖关系依赖关系 动画功能由 ngAniamte 模块中的Angular提供,它是从核心的Angular框架中分离出来的。另外,在这个项目中 我们使用 jQuery 模块来完成其余的 JavaScript 动画。 我们使用的是Bower (http://bower.io/) 来安装客户端依 赖关系。这一步更新 bower.json 配置文件来包含新的依赖关系: { "name": "angular-seed", "description": "A starter project for AngularJS", "version": "0.0.0", "homepage": "https://github.com/angular/angular-seed", "license": "MIT", "private": true, "dependencies": { "angular": "~1.3.0", "angular-mocks": "~1.3.0", "bootstrap": "~3.1.1", "angular-route": "~1.3.0", "angular-resource": "~1.3.0", "jquery": "~2.1.1", "angular-animate": "~1.3.0" } } • “angular-animate”:“~1.3.0” 意味着bower要安装与版本1.3.x兼容的angular-animate组件版本。 • “jquery”:“2.1.1” 意味着bower要安装jQuery2.1.1版本。注意这不是Angular库,而是标准的jQuery 库。我们可以使用bower来安装一个大范围的第三方库。 第 14 章 动画操作 - Applying Animations | 68 我们必须要求bower来下载和安装这个依赖关系。通过运行下述程序来实现这一要求: npm install 警告:如果你上次运行 npm install 后,一个新版本的Angular已经发布,那么由于需要安装的angular.js版本之 间的冲突,你的 bower install 可能会遇到问题。如果你有这个问题,那么只需要在运行 npm install 之前,简单 的删除你的 app/bower_components 文件夹。 注意:如果你在全局范围内安装bower,那么就可以运行 bower instal ,但是对于这个项目来说,我们已经预配 置了 npm install 来运行bower。 在 ngAnimate 下动画是如何工作的在 ngAnimate 下动画是如何工作的 想要了解在AngularJS下动画是如何工作的,请先阅读 AngularJS Animation Guide (https://docs.angularj s.org/guide/animations) 。 模板模板 我们需要的带有HTML动画代码的更改是用来链接asset文件夹的,这些文件夹定义了动画和 angular-animate.j s 文件。动画模块,即 ngAnimate ,由 angualr-animate.js 文件定义,包含了使您的应用程序成为动画意识的 必要的代码。 以下是在索引文件中需要更改的: app/index.html. ... ... ... 第 14 章 动画操作 - Applying Animations | 69 ... 重要重要:当使用Angular 1.3时,请确保使用jQuery版本2.1或更新的版本;jQuery 1.x官网不在支持。要确保加载 j Query在所有AngularJS 脚本之前,否则AngularJS不会检测jQuery,并且动画也不会像预期的那样起作用。 现在可以在CSS代码( animations.css )和JavaScript代码( animations.js )中创建动画。但是在开始之前,让我 们创建一个新的模块,该模块使用ngAnimate模块作为依赖,这就像我们之前使用 ngResource 一样。 模块与动画模块与动画 app/js/animations.js . angular.module('phonecatAnimations', ['ngAnimate']); // ... // this module will later be used to define animations // ... 现在让我们将这个模块附加到应用模块中… app/js/app.js . // ... angular.module('phonecatApp', [ 'ngRoute', 'phonecatAnimations', 'phonecatControllers', 'phonecatFilters', 'phonecatServices', ]); // ... 现在,phonecat模块是动画意识的。让我们做一些动画吧! 用 CSS 过渡动画实现动画的 ngRepeat用 CSS 过渡动画实现动画的 ngRepeat 我们通过把CSS过渡动画添加到 ngRepeat 指令呈现在 phone-list.html 页面上开始。首先,在重复的元素中添 加一个额外的CSS类,这样我们可以用CSS动画代码来与它连接。 app/partials/phone-list.html . 第 14 章 动画操作 - Applying Animations | 70 注意到怎样添加 phone-listing CSS类了吗?这就是我们需要的在HTML代码中使动画起作用的全部东西。 以下是实际的CSS 过渡动画代码: app/css/animations.css .phone-listing.ng-enter, .phone-listing.ng-leave, .phone-listing.ng-move { -webkit-transition: 0.5s linear all; -moz-transition: 0.5s linear all; -o-transition: 0.5s linear all; transition: 0.5s linear all; } .phone-listing.ng-enter, .phone-listing.ng-move { opacity: 0; height: 0; overflow: hidden; } .phone-listing.ng-move.ng-move-active, .phone-listing.ng-enter.ng-enter-active { opacity: 1; height: 120px; } .phone-listing.ng-leave { opacity: 1; overflow: hidden; } .phone-listing.ng-leave.ng-leave-active { opacity: 0; 第 14 章 动画操作 - Applying Animations | 71 height: 0; padding-top: 0; padding-bottom: 0; } 正如你所见到的, phone-listing CSS类与动画钩相结合,当列表中有项目被插入或者移出的时候,动画钩就会 出现。 • ng-enter类应用于元素中,当一个新的phone添加到列表中并呈现在页面上。 • ng-move类应用于列表中项目的移动。 • ng-leave类应用于列表中项目删除。 phone listing项目的添加和删除取决于传给 ng-repeat 属性的数据。例如,若过滤数据改变了,项目就会动画地 加入到repeat列表中或从repeat列表中动画地去除。 一些重要的声明,当动画出现时,两组CSS类会被添加到 元素中: 1. “starting”类在动画开始时表明风格 2. “active”类在动画结束时表明风格 starting类的名称是被激发事件的名称(如 enter , move 或 leave ),带有ng -前缀。所以一个 enter 事件将 产生一个称为 ng-enter 的类。 active类名与starting类名相似,但带有一个 -active 的后缀。这两类CSS命名 约定允许开发人员制作动画,自始至终。 在上面的例子中,当添加或删除项目时,元素从00到120像素120像素的高度扩展,在从列表中删除项目之前,元素使项目崩 溃。还有一个漂亮的淡入和淡出效果也在同时发生。所有的这些都是由上述示例代码顶部的CSS过渡声明操作 的。 尽管大多数现代的浏览器为CSS过渡 (http://caniuse.com/#feat=css-transitions) 和CSS动画CSS动画 (htt p://caniuse.com/#feat=css-animation) 提供了很好的支持,但是IE9和更早的浏览器却没有。如果你想使动画 与更早的浏览器兼容,考虑使用基于JavaScript的动画,下面会具体介绍。 用 CSS 关键帧动画实现动画的 ngView用 CSS 关键帧动画实现动画的 ngView 接下来,让我们在 ngView 中的路径更改之间为过渡添加动画。 首先,像上述例子一样,在HTML中添加一个新的CSS类。这一次,不是 ng-repeat 元素,而是添加包含 ng-v iew 指令的元素。为了实现这个,我们必须在HTML代码中做一些小小的改变,以使得在view改变中我们对动画 有更多的控制。 第 14 章 动画操作 - Applying Animations | 72 app/index.html.
    通过这个改变, ng-view 指令是嵌套在一个带有 view-container CSS类的父元素中。这个类添加了一个 positio n:relative 样式,所以定位 ng-view 是相对于父元素的,因为它模拟转换。 这里,让我们为这动画过渡将这个CSS类添加到 animations.css 文件: app/css/animations.css . .view-container { position: relative; } .view-frame.ng-enter, .view-frame.ng-leave { background: white; position: absolute; top: 0; left: 0; right: 0; } .view-frame.ng-enter { -webkit-animation: 0.5s fade-in; -moz-animation: 0.5s fade-in; -o-animation: 0.5s fade-in; animation: 0.5s fade-in; z-index: 100; } .view-frame.ng-leave { -webkit-animation: 0.5s fade-out; -moz-animation: 0.5s fade-out; -o-animation: 0.5s fade-out; animation: 0.5s fade-out; z-index:99; } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } 第 14 章 动画操作 - Applying Animations | 73 @-moz-keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } @-webkit-keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes fade-out { from { opacity: 1; } to { opacity: 0; } } @-moz-keyframes fade-out { from { opacity: 1; } to { opacity: 0; } } @-webkit-keyframes fade-out { from { opacity: 1; } to { opacity: 0; } } /* don't forget about the vendor-prefixes! */ 这里没有什么出乎意料的!只是一个简单的页面之间的淡入和淡出效果。这里的唯一不寻常的是,在页面之间实现软 切换动画时,我们在前一页(有 ng-leave 类的页面)的上方使用绝对定位来定位下一个页面(通过 ng-enter 识 别)。这样前一页即将被删除时,它逐渐消失而新的页面逐渐出现在它上面。 一旦离开动画结束元素被移除,一旦进入动画完成时, ng-enter 和 ng-enter-active CSS类从元素中被移 除,使其rerender和重新定位其默认CSS代码(所以一旦动画结束,没有绝对定位)。这运作起来非常流畅,使得页 面在路径变化中自然流动,不会有任何东西跳动。 应用的CSS类(开始和结束类)与 ng-repeat 大体相同。每次加载一个新页面, ng-view 指令将创建自身的一个 副本,下载模板并且附加内容。这将确保所有的视图都包含在一个单独的HTML元素中,该元素允许简单的动画控 制。 更多关于 CSS 动画,请看 Web Platform documentation (https://docs.webplatform.org/wiki/css/properti es/animations) 。 第 14 章 动画操作 - Applying Animations | 74 用JavaScript 实现动画的用JavaScript 实现动画的 ngClassngClass 让我们在应用程序中添加另一个动画。切换到 phone-detail.html 页面,我们看到,我们有一个不错的缩略图交换程 序。通过点击页面上列出的缩略图,这个手机图片改变了。但我们怎样才能改变这些添加动画? 让我们先考虑一下。基本上,当你点击缩略图,你正在改变图像的状态来反映新选中的缩略图。HTML内来指定状态 改变的最好办法是使用类。像之前一样,我们如何使用CSS类指定一个动画,这一次每当CSS类自身变化时,动画 就会出现。 每当一个新的手机缩略图被选中时,状态改变, .active CSS类添加到匹配的图像中,动画出现。 首先让我们对 phone-detail.html 页面上的 HTML代码稍作调整。请注意,我们已经改变了显示大图的方式: app/partials/phone-detail.html .

    {{phone.name}}

    {{phone.description}}

    就像缩略图,我们使用一个中继器来显示所有的图片,这些图片作为一个列表。然而我们不是使任何repeat-relat ed动画来产生动作。相反,我们关注ng-class指令因为若 active 类是正确的,那么它将被应用到元素中并且是可 见的。否则,图像是隐藏的。在我们的例子中,总有一个元素有active类,,因此,总会有一个手机图片在屏幕上可 见。 当活跃类添加到元素中, 在给AngularJS信号使它发射一个动画之前, active-add 类和 active-add-active 类 被添加。当活跃类被删除时, active-remove 类和 active-remove-active 类应用于元素,该元素会反过来触发 另一个动画。 第 14 章 动画操作 - Applying Animations | 75 确保手机图片在第一次加载页面时正确显示,我们也调整细节页面的CSS样式: app/css/app.css .phone-images { background-color: white; width: 450px; height: 450px; overflow: hidden; position: relative; float: left; } ... img.phone { float: left; margin-right: 3em; margin-bottom: 2em; background-color: white; padding: 2em; height: 400px; width: 400px; display: none; } img.phone:first-child { display: block; } 你可能会想,我们只是要创建另一个CSS-enabled动画。虽然我们可以那样做,但是让我们抓住机会来学习如何使 用 animation() 模块来创建javascript-enabled动画的方法。 app/js/animations.js . var phonecatAnimations = angular.module('phonecatAnimations', ['ngAnimate']); phonecatAnimations.animation('.phone', function() { var animateUp = function(element, className, done) { if(className != 'active') { return; } element.css({ position: 'absolute', 第 14 章 动画操作 - Applying Animations | 76 top: 500, left: 0, display: 'block' }); jQuery(element).animate({ top: 0 }, done); return function(cancel) { if(cancel) { element.stop(); } }; } var animateDown = function(element, className, done) { if(className != 'active') { return; } element.css({ position: 'absolute', left: 0, top: 0 }); jQuery(element).animate({ top: -500 }, done); return function(cancel) { if(cancel) { element.stop(); } }; } return { addClass: animateUp, removeClass: animateDown }; }); 注意,我们使用 jQuery (http://jquery.com/) 来实现动画。有了AngularJS,jQuery不要求做JavaScript动画,但 是我们要用它,因为编写自己的JavaScript动画库已经超出了本教程的范围。更多关于jQuery.animate,看 jQue 第 14 章 动画操作 - Applying Animations | 77 ry documentation (http://api.jquery.com/animate/) 。 当在元素中添加或删除一个类时,使用 addClass 和 r emoveClass 回调函数,该元素包含我们注册的类,这是在这种情况下 .phone 。当。活跃的类添加到元素(通过 ng-class 指令)theaddClass JavaScript回调将发射元素作为参数传递给回调。传入的最后一个参数是 done 回调函数。done回调函数的作用是,通过调用该函数,可以使Angular知道JavaScript动画结束。 removeClass 回调以同样的方式工作,当从元素中国移除一个类时得到触发。 在JavaScript回调中,通过操纵DOM创建动画。在上面的代码中,这是 element.css() 和 element.animate() 所做 的。回调用一个500像素的偏移量定位下一个元素的位置,并通过把每个项目上移 500像素 来定位之前的动画以 及新的项目。这产生一个像动画一样的传送带。当 animate 函数完成后,调用 done 。 注意 addClass 和 removeClass 每个返回一个函数。这是一个可选的函数,当动画消失时(当另一个动画在相同的 元素中替代) 以及动画完成后,可以被调用。一个布尔参数传递到函数中,可以让开发人员知道动画是否被取消 了。当动画结束时,这个函数可以用来做任何必要的清理工作。 总结总结 现在你学会了!我们已经在一个相对短的时间内创建了一个web应用程序。在 完结篇 (the-end.md) 中,我们将 讨论这里的东西给我们的指引。 第 14 章 动画操作 - Applying Animations | 78 1515 完结篇 - The End完结篇 - The End 我们的应用现在完成了。你可以随意练习这些代码,用 git checkout 或者 goto_step.sh 命令切换到之前的步 骤。 对于更多我们在教程部分提及的细节以及 AngularJS 理论的例子,你可以在 开发指南 - Developer Guide (htt ps://docs.angularjs.org/guide/) 中找到。 当你准备好使用 AngularJS 创建一个新项目时,我们推荐使用 angular-seed (https://github.com/angular/a ngular-seed) 项目来引导你的开发。 我们希望这篇教程对你有用,让你对 AngularJS 有了足够的了解,并且愿意对其进行更加深入的学习。我们特别 期待着你能够开发出自己的 AngularJS 应用,或者让你对 AngularJS 贡献代码 (http://docs.angularjs.org/mi sc/contribute) 产生兴趣。 如果你有什么问题、反馈或是想跟我们打个招呼,直接向 https://groups.google.com/forum/#!forum/angular 发消息吧! 第 15 章 完结篇 - The End | 80 更多信息请访问 http://wiki.jikexueyuan.com/project/angularjs-tutorial/
    还剩81页未读

    继续阅读

    下载pdf到电脑,查找使用更方便

    pdf的实际排版效果,会与网站的显示效果略有不同!!

    需要 10 金币 [ 分享pdf获得金币 ] 0 人已下载

    下载pdf

    pdf贡献者

    文杰天下

    贡献于2016-10-25

    下载需要 10 金币 [金币充值 ]
    亲,您也可以通过 分享原创pdf 来获得金币奖励!
    下载pdf