AngularJS开发入门教程


1. 导言 2. 步骤0——准备 3. 步骤1——静态模板 4. 步骤2——Angular模板 5. 步骤3——过滤转换器 6. 步骤4——双向数据绑定 7. 步骤5——XHRs与依赖注入 8. 步骤6——模板链接与图形 9. 步骤7——路由与多视图 10. 步骤8——更多模板 11. 步骤9——转换器 12. 步骤10——行为处理 13. 步骤11——REST与定制服务 14. 步骤12——应用动画 15. 结束 Table of Contents (基于AngularJS1.3.0-rc.1 由xdsnet at gmail 翻译) 本教程是一个介绍利用AngularJS开发的极好入门材料,它给出了一个完整的AngularJS web app开发的过 程。这个app会构建一个Android设备的目录显示服务,可以通过过滤来定位显示个别关注的设备以显示更 多细节信息。 下面这个教程会展示Angular是如何在没有扩展程序或插件的支持下使得浏览器应用更智能,你将: 通过例子了解如何使用客户端数据绑定技术实现能快速响应用户活动操作的动态视图应用。 了解Angular是如何在没有专门的DOM操作中实现视图与数据同步的。 学习如何利用Karma和Protractor技术来更好、更方便的测试应用。 学习如何使用依赖注入和服务来使常见的web任务更容易:例如在应用中获得数据。 当你完成这个入门教程,你将能够: 创建一个能在所有现代浏览器使用的动态应用程序 通过数据绑定连接你的数据模型与视图 利用Karma技术创建和实施单元测试 利用Protractor技术创建和实施 端到端(e2e)的测试 把应用逻辑从template(模板)中移出到Controllers(控制器)中 利用Angular服务从服务器获取数据 利用ngAnimate技术在应用中应用动画 知道学习更多的AngularJS需要哪方面资源 这个教程通过完整的开发一个简单应用的过程指导你学习AngularJS,这包括如何写并且运行单元测试和端 到端测试。每一步还额外提供了建议,供您了解更多的AngularJS和您正在构建的应用程序。 你可以在几个小时内完成整个教程,当然你也可以深度学习来度过愉快的一天。如果你正在寻找一个更短 PhoneCat 入门教程 App 导言 的AngularJS介绍,请看入门文档。 这部门内容展示如何在你本地计算机上设置来进行开发AngularJS相关程序。如果你仅仅想阅读入门教程, 你可以直接跳到步骤0——准备 在你的计算机上,你完全可以一直跟随教程使用代码,也可以进行随意改动,实际上改动代码可以让你更 深入体验如何撰写AngularJS程序以及利用推荐的测试工具工作。 这个教程依赖于Git版本控制工具,但使用本教程你并不需要知道太多Git使用知识,只需要会用教程中用到 的很少几个命令(到时也会给出了完整命令)。 你可以从 http://git-scm.com/download下载安装Git。安装后你应该可以访问到git命令行工具,在本教程中 你仅需要用到两个git命令: git clone ... 从远处版本仓库克隆到本地计算机 git checkout ... 在本地计算机上查看(取出)特定版本(标签)的代码 从 GitHuB上的angular-phonecat repository克隆需要用到下面的命令 git clone --depth=14 https://github.com/angular/angular-phonecat.git 这个命令会在你计算机的当前目录下建立 angular-phonecat 目录: --depth=14 参数选项是告诉Git获取最近的14次提交,这让下载的东西更少,也就更快了。 改变当前目录到 angular-phonecat cd angular-phonecat 本教程说明:从现在开始,都假定你的所有命令都是在 angular-phonecat 目录下执行。 如果你想运行预定义的本地web服务来测试,你需要有 Node.js v0.10.27+。你可以 从http://nodejs.org/download/下载安装你操作系统合适的版本。确认你的Node.js是符合要求的版本,需要 运行: node --version 开始 使用代码 安装Git 下载angular-phonecat 安装Node.js 在基于Debian的发行版,因为有另外一个工具命名为 node ,所以提供的解决方案是安装 nodejs-legacy apt 包,但把 node 改名为 nodejs : apt-get install nodejs-legacy nodejs --version 如果你需要在你的本地环境中运行不同版本的node.js,可以考虑安装 Node Version Manager (nvm) 。 一旦在你的计算机中安了Node.js,你就可以考虑安装下面的工具来自动解决依赖问题: npm install 这个命令会下载下面的工具到 node_modules 目录: Bower -客户端代码包管理 Http-Server - 简单的本地静态web服务 Karma -单元测试工具 Protractor - 端到端(E2E)测试工具 运行 npm install 会自动下载Angular框架到 app/bower_components 目录 注意angular-phonecat项目通过npm脚本自动安装运行各个单元,这意味着你不需要单独为了这个教 程在系统(全局)中安装相应内容。更多信息请在下面的安装辅助工具中了解。 项目中预定义了几个 npm辅助脚本来使得你更容易运行常规任务,这些在整个开发中都是有效的: npm start : 打开本地开发web服务 npm test : 运行Karma单元测试 npm run protractor : 运行Protractor 端到端(E2E)测试 npm run update-webdriver : 安装Protractor需要的驱动程序 Bower, Http-Server, Karma 和 Protractor 模块都是单独可执行的,你可以全局安装,然后在一个终端/命 令行中运行。在这个教程中你不一定需要下面的工作,如果你决定立即执行,你可以在全局安装: sudo npm install -g ... 例如要安装Bower的命令是: sudo npm install -g bower (注意在windows上运行时需要省略那个sudo) 然后你就可以运行bower工具了,例如: bower install 安装辅助工具(可选) 运行开发Web服务 整个Angular应用是纯粹的客户端代码,所以它可以用浏览器在本地文件系统打开,这也可能比通过HTTP 服务打开更好。但是,为了安全现在很多浏览器是不允许JavaScript直接从本地文件系统加载文件的。所以 angular-phonecat项目定义了一个简单的静态web服务来支持开发。打开运行这个服务需要输入: npm start 这会建立一个本地web服务,它监听者端口8000.你可以用浏览器打开 http://localhost:8000/app/index.html 来访问 这个web服务也可以绑定到不同的ip或者端口,你需要编辑package.json中的“start"脚本部分,你可以 用-a 来设置ip地址,用-p来设置端口。 单元测试可以确认JavaScript脚本会执行正确的操作,单元测试关注程序中局部的功能实现。所有的单元测 试都在 test/unit 目录下。 本项目使用Karma来定义单元测试,执行这些测试需要运行 npm test 这将运行Karma单元测试。 Karma读取 test/karma.conf.js 中的配置,它指示Karma执行: 打开一个Chrome 浏览器,并且连接到Karma 在浏览器中执行所有的单元测试 在终端/命令行中报告测试结果 监控项目 JavaScript文件的改变并再次执行测试 确保所有测试项都是正确的才算完成相应的编码工作,因为单元测试可以检测你的改动是否符合你编码需 要,并及时反馈。 我们使用端到端(End to End——E2E) 的测试确保应用的整体操作。端到端测试被设计来测试完整客户 端应用,包括显示的视图和操作交互对应的行为是否正确。它模拟真实的用户在真实浏览器中访问应用的 场景。 端到端测试放置在 test/e2e 目录下。 angular-phonecat项目配置使用Protractor来执行端到端测试。Protractor依赖一套驱动使它能与浏览器交 互。你可以通过下面的命令进行安装: npm run update-webdriver (你只需要执行它一次.) 因为Protractor工作需要与程序交互,所以需要先启动我们的web服务:: 运行单元测试 运行端到端(E2E)测试 npm start 然后在一个特定的终端/命令行窗口中执行Protractor测试脚本:: npm run protractor Protractor会读取在 test/protractor-conf.js 中的配置,这些配置告诉Protractor执行: 打开一个Chrome浏览器并且连接到程序 在一个浏览器中执行端到端测试 在终端/命令行窗口中报告测试结 果 关闭/退出浏览器 无论你对程作了何种修改都运行端到端测试来确认修改仍然使得程序的整体操作是正确的,在把修改提交 到远程仓库前完整运行端到端测试的行为是很常见的。 你已经准备开始建立AngularJS phonecat程序了。在这一步中,你将熟悉最重要的源代码文件,了解如何 开始与angular-seed绑定在一起的开发服务,并且在浏览器中运行程序。 在 angular-phonecat 目录中运行: git checkout -f step-0 这将把你的工作区切换到教程程序的 step-0分支中。完成教程学习中你会重复这个步骤(只是后面的数字 会不同)以切换到不同步骤分支中。注意的是这将丢失你在原有工作区中做的所有改动。 如果你不确定你已经安装了所有的依赖,需要运行一次: npm install 为了在浏览器中运行程序,打开终端/命令行窗口,并运行 npm start 来开始web服务。现在,打开浏览器 窗口,在地址栏输入 [http://localhost:8000/app/](http://localhost:8000/app/) 就可以访问到程序了。 如果你在浏览器中看见个页面(不是报错信息),则表示工作是正常的,当然现在显示的内容不让人兴 奋,但这还是很好的。 这个HTML页面会显示 "Nothing here yet!",它是由如下的HTML代码定义的,这些代码包括了我们后面需 要进一步利用的Angular关键元素。 app/index.html : My HTML File

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

ng-app 指令: 这个 ng-app 属性代表一个Angular指令 ngApp (在Angular约定 spinal-case 暨连词线拼接词用在定制属性 中, camelCase 暨驼峰拼接词用在指令中,并提供一致的效果)。这个指令标志这个html元素会被Angular 步骤0(step-0)——准备 这些代码做了什么? 用作应用程序的根(root)元素。这将告诉Angular是整个html页面还是部分元素作为Angular程序。 AngularJS脚本标签:
  • {{phone.name}}

    {{phone.snippet}}

这里我们用 ngRepeat指令 和两个 Angular表达式(expression) 取代了硬编码的电话列表:
  • 的 ng-repeat="phone in phones" 属性就是一个Angular重复指令,它告诉angular在列表中为每 个电话信息重复创建
  • 元素,这里的
  • 标签就作为了模板。 在双大括号中的表达式 {{phone.name}} 和 {{phone.snippet}} 会用实际的值替代。 这里,我们还添加了新的指令,叫做 ng-controller ,它为 标签指定了一个 PhoneListCtrl 的控制 器,对于这点 步骤2——Angular模板 工作区切换到步骤2 视图(view)和模板(template) 在双大括号中的表达式 {{phone.name}} 和 {{phone.snippet}} 指定的绑定,将参考我们程序中的数 据模型,这些都是在我们的 PhoneListCtrl 的控制器进行设置的。 注意:我们已经通过加载使用 ng-app="phonecatApp" 来指定了一个Angular数据模型,这 里 phonecatApp 是我们数据模型的名字,这个模型被包含在 PhoneListCtrl 中。 这里的数据模型(Model)(一个简单的电话信息数值)是由 PhoneListCtrl 控制器(Controller)实例化 的。这个控制器只是简单的一个构造函数,它操作了一个$scope参数。 app/js/controllers.js : var phonecatApp = angular.module('phonecatApp', []); phonecatApp.controller('PhoneListCtrl', function ($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™', 'snippet': 'The Next, Next Generation tablet.'} ]; }); 这里,我们定义了一个叫做 PhoneListCtrl 的控制器,并且注册它到一个叫做 phonecatApp 的AngularJS数 据模型中。注意,我们在 中的 ng-app 指令现在指定了一个 phonecatApp 的数据模型,这将在启 动Angular过程中加载。 尽管现在看来控制器并没有做什么特别的事,但它是至关重要的。通过上下文数据模型,控制器允许我们建 立模型和视图之间的数据绑定。我们由此连接起二者间的描述、数据和逻辑,它们是如下工作的: 数据模型(Model)和控制器(Controller) ngController 指令定位到 标签,引用指定名称的控制器, PhoneListCtrl 在JavaScript文 件 controllers.js 中设定。 PhoneListCtrl 控制器把数据链接到 $scope 来注入我们的控制器函数。这个scope是一个在应用定义时 预定义(指定)的根作用范围(root scope),然后这个控制器通过 标签绑定。 在Angular中作用范围的概念至关重要。作用范围被视作把模型、视图和控制器粘合起来协同工作的胶水。 Angular利用作用范围来整合模板中的信息、数据模型和控制器,保持模型和视图之间的独立与同步。任何 在模型中的变化都被反映到视图中,在视图中的操作变化也反映到数据模型。 要了解更多的关于Angular作用范围的内容,请参考Angular作用范围文档主题部分。 Angular分离了控制器和视图,这使得更容易实施测试驱动的开发。如果我们的控制器可以在全局命名空间 起效,那么我们可以在一个模拟的范围内实例化它: describe('PhoneListCtrl', function(){ it('should create "phones" model with 3 phones', function() { var scope = {}, ctrl = new PhoneListCtrl(scope); expect(scope.phones.length).toBe(3); }); }); 这个对于 PhoneListCtrl 的测试实例检验了手机数组中是否有3条记录。这个例子简单的示范了如何为 Angular项目创建一个单元测试。因为测试是软件开发的关键部分,我们可以让这一过程更简单,从而鼓励 开发者写测试。 实际上,你不一定想你的控制器工作在全局名称空间中,反而你更愿意把控制器通过匿名构造函数注册 为 phonecatApp 的模块。在这种情况下Angular提供了一种服务( $controller )让你可以通过名字查询到控制 器。下面就是利用 $controller 进行相同的测试: test/unit/controllersSpec.js : 作用范围 测试 测试非全局控制器 describe('PhoneListCtrl', function(){ beforeEach(module('phonecatApp')); it('should create "phones" model with 3 phones', inject(function($controller) { var scope = {}, ctrl = $controller('PhoneListCtrl', {$scope:scope}); expect(scope.phones.length).toBe(3); })); }); 在每个测试前告诉Angular要加载 phonecatApp 数据模型 请求Angular向我们的测试函数注入 $controller 服务 我们利用 $controller 来创建 PhoneListCtrl 的实例 *通过这个实例,我们检测范围内的电话数组是否有 预期的3条记录 Angular开发者一般更喜欢采用Jasmine的行为驱动开发(Behavior-driven Development——BDD)框架来写 测试。但是Angular并不限定你只能使用Jasmine。我们(原文作者)的为教程采用Jasmine V1.3编写过所 有测试。如果你打算学习下Jasmine,可以到Jasmine主页和Jasmine文档去访问以学到更多。 angular-seed 预配采用Kama进行单元测试,你需要确保Karma的支持组件安装好了。对此你只需要简单 的执行 npm install 为了运行测试,或者在文件修改后检测,你需要: npm test Karma会自动打开一个新的Chrome实例,只是忽略它而运行在后台。Karma会使用浏览器来运行测 试。 你会在控制台看到类似下面的输出: info: Karma server started at http://localhost:9876/ info (launcher): Starting browser "Chrome" info (Chrome 22.0): Connected on socket id tPUm9DXcLHtZTKbAEO-n Chrome 22.0: Executed 1 of 1 SUCCESS (0.093 secs / 0.004 secs) 这代表了测试通过或者没有通过 在测试没有主动关闭前,在对源码或者test.js有任何修改后都会自动运行测试。Karma会注意到变化并 执行测试返回结果给你。难道这不是很好? 确认你没有最小化karma打开的浏览器。在一些操作系统中,最小化浏览器会导致内存访问受 限,这使得karma测试运行的特别慢。 为 index.html 添加其他的绑定,例如:

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

    编写并运行测试 尝试 在控制器中创建一个新的模型属性,并绑定到模板。例如: $scope.name = "World"; 为 index.html 添加新的绑定:

    Hello,{{name}}!

    刷新你的浏览器来检测是否会显示"Hello, World!"。 在 ./test/unit/controllersSpec.js 中为控制器更新单元测 试来反映前面的修改。例如添加: expect(scope.name).toBe('World'); 利用 repeater 指令在 index.html 中构建一个简单的表格:
    row number
    {{i}}
    现在把列表改成对i都增加1的绑定:
    row number
    {{i+1}}
    额外还可以尝试通过使用附加的ngrepeat实现8×8的表格。 改变测试项让单元测试失败:如把 expect(scope.phones.length).toBe(3) 改成 toBe(4) 。 你现在已经有了一个动态程序,有了分离的数据模型、视图和控制组件,你还测试了它们按预期工作,现 在让我们进入步骤3去学习如何为程序添加文字搜索。 小结 我们在上一步我们做了大量的工作,为后面打下了良好基础。所以现在我们可以简单的扩展就添加一个全 文搜索的功能(是的,这很简单!)。我们也会写一个端到端(end-to-end,E2E)测试,因为良好的端到端 测试是应用的好朋友,就像有一双眼睛时刻看着应用,快速定位故障。 程序会有一个搜索框。你需要注意在搜索框输入时电话列表的变化。 直接用浏览器访问步骤3在线演示 大多数的重要改变都会列在下面,不过你也可以在GitHub看到完整的差异。 这里我们没有修改控制器 app/index.html :
    Search:
    • {{phone.name}}

      {{phone.snippet}}

    我们添加了一个标准的HTML 标签,并对ngRepeat指令应用了Angular的filter功能来处 理 输入。 就是这些改变让用户输入搜索条件后马上可以在手机列表中看到对应的变化。这些新代码展示了: 数据绑定:这一Angular核心特性。当页面加载后,Angular把input标签定义的输入框的值绑定到一个 命名变量中(页面中命名为query的变量),这个变量还以相同的名字存在于数据模型中,二者是完全 同步的。在这个代码中输入框键入的内容(绑定到 query )会立即作为列表输出时的过滤条件 (通 步骤3——过滤转换器 工作区切换到步骤3 控制器 模板 过 phone in phones | filter:query 这一句)。数据模型的变化导致转换器的输入改变,转换器通过更新 DOM来反映模型的当前状态。 使用 filter 过滤器: filter 功能使用 query 来创建一个新的数组(只有那些记录中匹配了 query 对应 值), ngRepeat 自动根据附加了 filter 变动的手机列表数据。这在开发过程中是完全透明的。 在步骤2中,我们学习了如何写单元测试。单元测试可以测试控制器和其他由JavaScript写的组件,但不能 方便测试DOM操作或者我们写的程序本身。对此,端到端测试就是更好的选择。 这个搜索功能完全是通过模板和数据绑定来实施的,所以我们写出第1个端到端测试来验证这个工作。 test/e2e/scenarios.js : 测试 describe('PhoneCat App', function() { describe('Phone list view', function() { beforeEach(function() { browser.get('app/index.html'); }); it('根据用户在search输入框中的输入过滤手机列表', function() { var phoneList = element.all(by.repeater('phone in phones')); var query = element(by.model('query')); expect(phoneList.count()).toBe(3); query.sendKeys('nexus'); expect(phoneList.count()).toBe(1); query.clear(); query.sendKeys('motorola'); expect(phoneList.count()).toBe(2); }); }); }); 这个测试验证了搜索框和转换器很好的连接在一起工作。你可以看出在Angular中是多么容易的写出端到端 测试啊。虽然这个例子展示的是一个简单测试,但它却是是十分容易设置功能的、且可读的端到端测试。 尽管这些测试语句很像我们前面用Jasmine写的对控制器测试的语法,但端到端测试是使用的Protractor提 供的APIs。要想了解关于这些Protractor APIs的更多信息,可以阅读 https://github.com/angular/protractor/blob/master/docs/api.md 我们用Protractor来完成端到端测试与采用Karma进行单元测试过程类似,尝试运行 npm run protractor 。 端到端测试比单元测试运行的速度要慢些。Protractor会在运行了一次端到端测试后自动退出,而不会自动 的在测试区内文件变化时运行,为此,你必须在有变化后手动启动端到端测试,即再次运行 npm run protractor 。 注意:你必须确认你已经打开了一个web服务来供protractor测试使用,你可以采用 npm start (译 者注:这时你有两个终端/命令行在运行,其中一个运行着 npm start 以提供web服务,另外一个执行 着 npm run protractor 来进行端到端测试)。你还需要确认你已经安装了protractor 并且升级了 webdriver使之符合运行 npm run protractor 的条件,这需要你之前执行过 npm install 和 npm run update-webdriver 即在 index.html 中添加用 {{query}} 来显示当前 query 模型中当前值的模板设计,这可以观察到当输入框 键入信息改变时最直接的变化。 利用Protractor运行端到端测试 尝试 显示当前的查询条件 让我们看看如何把 query 模型的值赋予HTML页面标题: 添加一个端到端测试到 test/e2e/scenarios.js 文件中 describe 部分: describe('PhoneCat App', function() { describe('Phone list view', function() { beforeEach(function() { browser.get('app/index.html'); }); var phoneList = element.all(by.repeater('phone in phones')); var query = element(by.model('query')); it('should filter the phone list as user types into the search box', function() { expect(phoneList.count()).toBe(3); query.sendKeys('nexus'); expect(phoneList.count()).toBe(1); query.clear(); query.sendKeys('motorola'); expect(phoneList.count()).toBe(2); }); it('should display the current filter value in the title bar', function() { query.clear(); expect(browser.getTitle()).toMatch(/Google Phone Gallery:\s*$/); query.sendKeys('nexus'); expect(browser.getTitle()).toMatch(/Google Phone Gallery: nexus$/); }); }); }); 现在运行protractor( npm run protractor )肯定会看到失败信息。 你想该如何添加 {{query}} 到title标签,是不是应该像下面: Google Phone Gallery: {{query}} 然而你直接刷新页面将不能看到期望的结果,这是因为"query"模型只存在于一定的作用范围(scope) 内,这里是利用`ng-controller="PhoneListCtrl"命令定义在body元素内的: 这意味着只有在body标签起效的区域(其本身和包括在内的子标签内)是query有效作用范围,之外则 不是,而title标签是在这之外的。所以为了让这个数据模型在 内也有效,我们需要移动/改 变 ngController 使得对整个HTML元素都有效(只有这个标签是同时包括了body和title的): <html ng-app="phonecatApp" ng-controller="PhoneListCtrl"> 在标题中显示查询 并且确认已经把 ng-controller 从body标签中移除了。 再运行 npm run protractor ,这时你将看到测试通过的信息。 可能你注意到了因为angular的加载和初始化是需要时间的,你在浏览器标题栏中会在一瞬间看到 如 Google Phone Gallery:{{query}} 这样的信息,但其实这不是你希望的(这时angular的数据绑定还 没有起作用,就造成了前面的原始数据显示,和后面起作用后的正确显示),对此一个更好的处理方 法是使用 ngBind 或者 ngBindTemplate 命令,这使得在加载器无关的信息对用户来说是不可见的: <title ng-bind-template="Google Phone Gallery: {{query}}">Google Phone Gallery 我们已经实现了文本搜索功能,还为该功能提供了测试,现在让我们进入步骤4,以给这个程序增添排 序能力。 小结 在这一步骤中,我们将为程序添加一个特性使得可以对手机的显示排序。这还需要我们为手机数据模型中 添加一些新信息作为排序的依据,并以此写出转换器的处理让数据起作用使得程序达到预期效果。 程序现在除了有一个搜索框还有一个下拉选择框,它允许选择数据排序的要求。 直接用浏览器访问步骤4在线演示 大多数的重要改变都会列在下面,不过你也可以在GitHub看到完整的差异。 app/index.html : Search: Sort by:
    • {{phone.name}}

      {{phone.snippet}}

    这里只列出了在 index.html 模板中发生变化的部分,主要有: 首先,我们添加了一个 Uppercased: {{ userInput | uppercase }} 现在我们已经学习了如何写一个定制转换器,并知道如何进行测试,让我们进入步骤10学习如何使用 测试 尝试 小结 Angular来增强手机详细说明页面。 在这一步中,我们会对手机图片添加点击支持,点击后会在手机详细说明页面显示新的图形。 手机详细说明显示当前手机的一个巨大的图片和一些缩略图。我们点击缩略图会显示对应的大图片, 这个效果很令人兴奋。让我们看看如何利用Angular来实现。 直接用浏览器访问步骤10在线演示 大多数的重要改变都会列在下面,不过你也可以在GitHub看到完整的差异。 app/js/controllers.js : ... var phonecatControllers = angular.module('phonecatControllers',[]); phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$http', function($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 控制器中,我们创建了一个 mainImageUrl 模型属性,并且设置默认值为第一个手机图片 的URL。 我们也创建可一个 setImage 的行为处理函数,来改变 mainImageUrl 。 app/partials/phone-detail.html : ...
    ... 步骤10——行为处理 工作区切换到步骤10 控制器 模板 我们把 ngSrc 指令通过 mainImageUrl 绑定到大图片的URL。 我们也为缩略图注册了 ngClick 处理,当用户点击任何缩略图的时候进行处理,这个处理会使 用 setImage 行为处理函数依据点击的缩略图来改变 mainImageUrl 的值,从而改变大图显示内容。 为了检测新特性,我们需要写2个端到端测试。其中一个验证手机详细说明页面开始时,主要图(大图)是 否是对应显示的第1个手机图片(默认值)。第二个测试检查点击一些缩略图是否会正确进行图片切换。 test/e2e/secnarios.js : ... describe('Phone detail view', function() { ... it('should display the first phone image as the main phone image', function() { expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/); }); it('should swap main image if a thumbnail image is clicked on', function() { element(by.css('.phone-thumbs li:nth-child(3) img')).click(); expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.2.jpg/); element(by.css('.phone-thumbs li:nth-child(1) img')).click(); expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/); }); }); 我们现在可以运行 npm run protractor 来完成测试了。 你也可以重构单元测试,因为这一步添加了 mainImageUrl 模型到 PhoneDetailCtrl 控制器中。下面,我们创 建一个叫 xyzPhoneData 的函数,它会返回 images 属性的json数据来完成测试。 test/unit/controllersSpec.js : 测试 ... beforeEach(module('phonecatApp')); ... 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}); })); it('should fetch phone detail', function() { expect(scope.phone).toBeUndefined(); $httpBackend.flush(); expect(scope.phone).toEqual(xyzPhoneData()); }); }); 现在单元测试能通过了。 让我们给 PhoneDetailCtrl 添加新的方法: $scope.hello = function(name) { alert('Hello ' + (name || 'world') + '!'); } 并把 添加到`phone-detail.html`模板中。 我们的手机图片已经可以在指定位置进行切换了,我们准备进入步骤11来学习一个更好的获取数据的方 尝试 小结 法。 在这一步,我们将改变数据获取方法: 我们定义定制服务来代表一个RESTful客户端。使用这样的客户端,我们能够用更简单的方法向服务器 请求数据,而不需要处理低层次的 $http API,HTTP方法以及URL。 直接用浏览器访问步骤11在线演示 大多数的重要改变都会列在下面,不过你也可以在GitHub看到完整的差异。 RESTful功能是由Angular中的 ngResource 模块提供,这个模块不属于Angular框架核心模块,所以我们必 须要单独的安装和引入。 我们可以使用Bower来安装客户端依赖,这一步将更新 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.2.x", "angular-mocks": "~1.2.x", "bootstrap": "~3.1.1", "angular-route": "~1.2.x", "angular-resource": "~1.2.x" } } 新的依赖描述 "angular-resource": "~1.2.x" 告诉bower要安装的angular-resource组件兼容版本是1.2.x。 我们必须命令bower下载并安装这个依赖,这里我们运行下: npm install 如果你是利用bower全局安装,你可能要单独运行 bower install ,但在这个项目中只需要运行 npm install 即可调用bower 我们在 app/js/services.js 中定制服务,所以需要在布局模板中引入这个文件。额外,我们也需要引 入 angular-resource.js 文件,它提供了 ngResource 模块: app/index.html : 步骤11——REST与定制服务 工作区切换到步骤11 依赖 模板 ... ... 我们创建自己的服务提供从服务器访问手机数据: app/js/services.js var phonecatServices = angular.module('phonecatServices', ['ngResource']); phonecatServices.factory('Phone', ['$resource', function($resource){ return $resource('phones/:phoneId.json', {}, { query: {method:'GET', params:{phoneId:'phones'}, isArray:true} }); }]); 我们使用模块API来注册了一个定制服务(作为工厂函数)。我们用 Phone 来代表这个服务(调用工厂函 数)。工厂函数类似于一个控制器的构造函数,通过函数参数可以声明依赖注入。这个 Phone 服务描述了 对 $resource 服务功能的依赖。 这个 $resource 服务功能只需要很少几行代码就简单的创建一个RESTful客户端。这个客户端可以被用于我 们的程序以替代低层次的 $http 服务。 app/js/app.js ... angular.module('phonecatApp', ['ngRoute', 'phonecatControllers','phonecatFilters', 'phonecatServices']). ... 我们也需要把 phonecatServices 模块添加进 phonecatApp 的模块依赖数组中。 通过使用 Phone 服务代理替换低层次的 $http 服务,我们可以让我们的子控制器 ( PhoneListCtrl 和 PhoneDetailCtrl )进一步简化。Angular的 $resource 服务利用RESTful资源模型也提供了 比 $http 更好的资源交互(展示),让我们更容易理解控制器中的代码如何工作。 app/js/controllers.js : 服务 控制器 var phonecatControllers = angular.module('phonecatControllers', []); ... phonecatControllers.controller('PhoneListCtrl', ['$scope', 'Phone', function($scope, Phone) { $scope.phones = Phone.query(); $scope.orderProp = 'age'; }]); phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', 'Phone', function($scope, $routeParams, Phone) $scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) { $scope.mainImageUrl = phone.images[0]; }); $scope.setImage = function(imageUrl) { $scope.mainImageUrl = imageUrl; } }]); 注意现在 PhoneListCtrl 中我们把 $http.get('phones/phones.json').success(function(data) { $scope.phones = data; }); 替换成 $scope.phones = Phone.query(); 这是一个简单的声明。我们可以查询到所有手机。 注意在上面的代码中一个重要的事情发生了,就是我们在调用 Phone 服务的方法时,没有定义任何的回调 函数。虽然这像可以获得同步的返回果,但其实并不是。我们是在"未来"获得一个同步返回结果——一个由 XHR响应返回数据填充的对象。因为Angular的数据绑定,我们可以使用这个"未来"结果,并绑定到我们的 模板中。到时,当数据获取到后,视图会自动更新。 当然有时这样的"未来"对象以及相关数据绑定并不能满足我们所有的工作要求,所以在具体实现中,我们有 时也需要添加回调来处理服务响应。例如 PhoneDetailCtrl 控制器中就包括了设置 mainImageUrl 这样的回调 函数。 因为我们现在使用了 ngResource 模块,所以我们要更新Karma配置来完成测试。 test/karma.conf.js : 测试 files : [ 'app/bower_components/angular/angular.js', 'app/bower_components/angular-route/angular-route.js', 'app/bower_components/angular-resource/angular-resource.js', 'app/bower_components/angular-mocks/angular-mocks.js', 'app/js/**/*.js', 'test/unit/**/*.js' ], 我们编辑单元测试来验证新服务是否能够像预期一样正确发出HTTP请求和处理响应。这些测试也检验我们 的控制器能够与服务正常协作。 $resource 服务利用响应对象(作为参数)方法来更新和移除资源。如果我们使用标准的 toEqual 检测将因 为不确定(恰当)的响应(比如包括一些额外方法定义等)而肯定失败,为了处理这样的问题,我们使用 新定义一个Jasmine matcher式 toEqualData 完成检测。使用 toEqualData 比较两个对象,将之考虑对象属 性而忽略方法。 test/unit/controllersSpec.js : describe('PhoneCat controllers', function() { beforeEach(function(){ this.addMatchers({ toEqualData: function(expected) { return angular.equals(this.actual, expected); } }); }); beforeEach(module('phonecatApp')); 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).toEqualData([]); $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}); })); it('should fetch phone detail', function() { expect(scope.phone).toEqualData({}); $httpBackend.flush(); expect(scope.phone).toEqualData(xyzPhoneData()); }); }); }); 我们现在可以看到类似如下的Karma输出了: Chrome 22.0: Executed 4 of 4 SUCCESS (0.038 secs / 0.01 secs) 我们准备在步骤12(最后一步)中为手机图片替换应用上动画效果。 小结 在这个最终步骤中,我们通过在以前代码基础上附加CSS和JavaScript实现动画效果来增强手机展示web程 序。 用 ngAnimater 使得在程序中可以应用动画 公用的 ng 指令自动利用钩子(hook)来触发动画 当标准的DOM操作被赋予了动画时,对应的元素将会应用指定的动画效果,这些操作包括了利 用 ngRepeat 插入或者移除DOM节点,以及利用 ngClass 对元素指定或者去除类型设定。 直接用浏览器访问步骤12在线演示 大多数的重要改变都会列在下面,不过你也可以在GitHub看到完整的差异。 动画功能由Angular的 ngAnimate 模块提供,它不属于Angular框架核心。此外我们使用 JQuery 来提供额外 的JavaScript动画支持。 我们使用 Bower 来安装客户端依赖。这一步骤中我们会更新 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.2.x", "angular-mocks": "~1.2.x", "bootstrap": "~3.1.1", "angular-route": "~1.2.x", "angular-resource": "~1.2.x", "jquery": "1.10.2", "angular-animate": "~1.2.x" } } "angular-animate": "~1.2.x" 告诉bower需要安装1.2.x版本的angular-animate兼容组件。 "jquery": "1.10.2" 告诉bower需要1.10.2版本的JQuery,注意,JQuery不属于Angular库,它是一个 标准的JQuery库。我们可以用bower安装第三方的库。 我们必须执行bower来下载安装,这里运行 npm install 如果你是在全局环境中使用bower安装,你可能会用直接到 bower install 指令,但是在这个项目中我 们已经预配使用 npm 来启动 bower install ,所以你只需要: npm install 步骤12——应用动画 工作区切换到步骤12 依赖 如何利用 ngAnimate 实现动画 如何利用AngularJS实现动画,请首先阅读 AngularJS Animation Guide (AngularJS动画指南)。 需要在HTML模板代码中进行一些改动以链接诸如 angular-animate.js 等定义动画所需的资源文件。动画模 块被称为 ngAnimate ,其在 angular-animate 中定义,并包含让程序应用动画的必要代码。 这里,我们在index文件中进行必要改动。 app/index.html ... ... ... ... 重要提醒:在Angular 1.3中必须要使用jQuery 2.1以上版本,而jQuery1.x版本是不被正式支持的。一 定要在所有AngularJS脚本之前加载jQuery,否则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', ]); // ... 现在这个手机展示已经准备好动画环境了,让我们创作一些动画。 让我们在 phone-list.html 中添加CSS过渡动画到 ngRepeat 指令处理过程中。首先,我们给重复的元素添加 一个额外CSS类,这样我们就可以以此钩住(hook)CSS过渡动画。 app/partials/phone-list.html 注意我们是怎么添加 phone-listing CSS类的?这些就是我们获得动画效果所有必须的HTML代码。 现在为CSS过渡动画实现代码 在动态 ngRepeat 中使用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; height: 0; padding-top: 0; padding-bottom: 0; } 正如所见, phone-listing CSS类通过向对象添加或者去除(CSS类)而使得动画钩子触发。 ng-enter 类被赋予一个新加入并渲染展示在页面中的手机元素。 ng-move 类赋予在列表中移动的手机元素 ng-leave 类赋予从列表中去除的元素 手机列表根据 ng-repeat 属性添加或者删除元素。例如如果转换器输出数据改变,导致在列表中动态的添加 或者放置元素。 这点尤为需要注意,在动画工作时,有两套CSS类可被加入元素: 1. 一套是"starting"类,这代表了动画的开始形式 2. 一套"active"类,代表了动画结束的形式 这个命名为 starting 的类会触发一些有 ng- 前缀的行为(例如 enter , move 或者 leve ),像 enter 就会触 发 ng-enter 。 而 active 的类像 starting 类一样也会触发一些行为,只是这些行为是有 -active 后缀。这两套CSS类的名称 可以在开发中指定动画的实现,是开始还是结束。 在我们的例子中,在列表中添加或者移动时,元素会从高度0扩展到高度120像素,在去除时逐渐消隐掉 (缩小),同时还有一个淡入淡出效果。所有这些操作都是通过CSS过渡声明在前面的例子代码中。 尽管许多现代浏览器都能很好的支持CSS过渡和CSS动画,但IE9及以前版本是不支持的。如果你想对老的 浏览器提供兼容的动画效果,可以考虑使用基于JavaScript的动画,这将在后面描述。 接着,让我们为 ngView 中的内容切换提供动画。 首先,还是要添加一个新的CSS类到我们的HTML中,就像上面示例一样。这次,不是插入到 ng-repeat 所 在的元素中,而是加入到应用 ng-view 命令所在的元素。为此,我们需要对HTML进行一些小改动,所以我 们可以在视图变化时有更多的控制动画。 app/index.html
    通过这点改变,嵌入在 view-container CSS类元素下的 ng-view 指令所在元素有了 view-frame CSS类属 性。而 view-container CSS类元素下的各个子元素有了 position: relative 样式设定,使得 ng-view 是相对于 父级元素进行定位实现动画转换。 在这里,我们添加一些CSS到 animations.css 中实现转换动画: app/css/animations.css 通过CSS提供 ngView 关键帧动画 .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; } } @-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 类的元素),都会移除附加的预订CSS类(即动画相关的CSS类,例 如 ng-enter 、 ng-enter-active 和 ng-leve 等)。这显得十分自然流畅,页面处理流程没有任何跳来跳去。 这些CSS类利用 ng-repeat 赋予所有需要的元素(开始和结束类)。每次页面加载 ng-view 都会创建一个它 的副本,下载模板并且添加内容。这将确保所有的内容在一个单页HTML的元素中来允许简单的动画控制。 要了解更多CSS动画,请看WEB平台文档。 我们将在我们的程序中添加其他动画,转换到 phone-detail.html 页面,我们已经有了一个不错的缩略图切 换效果:点击缩略图,将显示对应的手机不同角度图片,但我们能否为它添加动画呢? 让我们先考虑一下这一过程:当你点击缩略图,会改变显示不同角度图像(状态)来反映新点选的缩略图。 在HTML中要改变描述状态最好是使用类。所以像前面一样,我们可以通过指定CSS类来描述一个动画,这 次每当CSS类本身发生变化时会显示动画。 每当一个新手机缩略图被选中, .active CSS类就会添加到匹配的图片上,并播放动画。 首先,我们开始调整 phone-detail.html 页面中的HTML代码: app/partials/phone-detail.html

    {{phone.name}}

    {{phone.description}}

    就像缩略图,我们使用一个转换器来显示所有的概要文件列表图片,但是我们没有动画,也没有重复关联 动画。反而,我们把视线投向了 ng-class 指令,因为无论何时,只有 active 类被激活,它就被加入到元 素,并作为可视元素渲染显示出来。另外,其他不同角度的图片是隐藏的。在这个例子中,同一时间总有 一个元素(图片)是 active 而被显示在屏幕上的。 当 active 类被加入到元素,在 active-add 和 active-add-active 类被添加前AngularJS可以触发一个动画。当 元素移除时, active-remove 和 active-remove-active 类被赋予到元素,这也可以触发另一个动画。 为了确保手机图片中页面第一次加载时正确显示,我们也要处理一下页面CSS样式: app/css/app.css : 利用JavaScript实现 ngClass 动画 .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动画?事实上,我们不这样做,这里我们将借此机会学习如何利 用 animation() 方法创建基于JavaScript的动画: 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', 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来实现动画。在AngularJ中实现动画时jQuery并不是必须的,但是直接用 JavaScript实现动画其实已经超出了本教程的范围。为了更多了解 jQuery.animat 请查看jQuery文档。 我们注册了 addClass 和 removeClass 回调函数,其会在 .phone 内部的任何元素添加或是移除类属性时调 用。当 .active 类型被添加到元素(通过 ng-class 指令),则JavaScript的 addClass 回调被调用,而且所对应 的 element 作为参数传入。最后一个参数 done 是一个回调函数。事实上 done 是为了通知Angular知道 JavaScript动画已经完成,从而调用以完成后续工作。 removeClass 回调也基于同样的机制工作,不过是插入到一个类从元素中移除时。 通过JavaScript的回调,我们创建了对DOM的手动操作,在代码内,这些就是 element.css() 和 element.animate() 被执行。回调定位了下一个元素在500像素下,并移动前后两张图片,并动态同步移 动以实现动画。这就是一个传送带动画,在 animate 功能完成后,调用 done 。 注意: addClass 和 removeClass 每次返回一个函数,这是一个可选项,以实现函数调用链(可以让一个动画 结束后进入下一个动画——或者功能),一个布尔值被传递,让开发者知道动画是否被调用。这通常被用 于在动画结束后执行一些清理工作。 你已经完成了它!我们在一个相对短的时间内就创建了一个web程序。最后(下一页),我们将指出如何 进一步学习。 小结 我们的程序现在已经完成了,你可以任意利用, git checkout 命令跳到某个步骤继续体验代码。 为了更多体验了解本教程例子信息,可进一步观看开发指南。 当你准备开始开发一个使用Angular的程序时,我们建议你在 angular-seed 项目的基础上扩展开发。 我们希望这个教程对你有用,让你学到关于Angular足够多的内容(比你预期更好)。我们特别希望你能开 发出你自己的Angular程序,并以你感兴趣的合适方式为Angular发展提供灵感。 如果你有问题或者仅仅是想说声"hi",都欢迎你在(https://groups.google.com/forum/#!forum/angular)发消 息。 结束
  • 还剩65页未读

    继续阅读

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

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

    需要 8 金币 [ 分享pdf获得金币 ] 1 人已下载

    下载pdf

    pdf贡献者

    mkle

    贡献于2015-08-16

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