AngularJS Phonecat(步骤0-步骤5)

导言


最近在学AngularJS的实例教程PhoneCat Tutorial App,发现网上的中文教程都比较久远,与英文版对应不上,而且缺少组件和文件重构两节。所以决定自己整理一个中文简明教程,内容较多,先整理0-5小节。

教程展示一个Angular应用程序:

AngularJS Phonecat(步骤0-步骤5)_第1张图片
catalog_screen.png

涉及如下技术:

视图和模型的双向数据绑定;

Karma和Protractor测试;

组件化和模块化编程。

英文教程

配置


安装Git

下载Git,并安装。安装后可以使用git命令行工具git bash,在教程中只用到两个git命令:

git clone ... 从远处版本仓库克隆代码到本地计算机

git checkout ...在本地计算机上查看(取出)特定版本(标签)的代码

拷贝源码

命令行中输入:

git clone --depth=16 https://github.com/angular/angular-phonecat.git

然后打开项目文件:

cd angular-phonecat

注意:从现在开始,所有命令都是在angular-phonecat目录下执行。

安装Node

下载Node.js,并安装。所需Node的最低版本是 Node.js v4+,可以通过下面命令行确认node版本:

node --version

安装工具

npm install

该命令行会安装package.jason规定的工具到node_modules文件夹,并下载AngularJS框架到app/bower_components。

下载的工具有:

Bower - 客户端代码包管理工具

Http-Server - 简单的本地静态web服务器

Karma - 单元测试工具

Protractor - 端到端 (E2E) 测试工具

初步接触项目

npm start: 开启本地服务器

npm test: 运行Karma单元测试工具

单元测试:npm test会自动打开谷歌浏览器和火狐,点击debug、再打开控制台可以查看报错信息。测试成功时,命令行窗口会返回success信息。

npm run update-webdriver: 安装Protractor所需驱动

npm run protractor: 运行Protractor端到端测试

端到端测试:npm run update-webdriver、npm start、npm run protractor。测试成功时,命令行窗口会返回success信息。

注意:输入 npm start 后,应该另外开一个命令行窗口(不能将服务器关闭,否则无法测试),再输入 npm run protractor命令。

0 准备


重置项目

git checkout -f step-0

该命令将重置phonecat项目的工作目录,需要在每一学习步骤运行此命令,将step-0的0改成相应步骤的数字(如:2 AngularJS模板,则数字为2)。

启动服务器

npm start

在浏览器中输入: http://localhost:8000/index.html,查看页面内容。

index.html

app/index.html:



  
    
    My HTML File
    
    
  
  

    

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

代码中,ng-app表示html元素会被Angular用作应用程序的根(root)元素。这就是说,ng-app规定以整个html页面还是部分元素作为Angular程序。

双大括号(Double-curly)绑定表达式:

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

这一行展示了Angular模板应用的两个核心功能:{{ }}进行绑定,简单表达式'yet'+'!'可以用于绑定。

程序结构如下:

AngularJS Phonecat(步骤0-步骤5)_第2张图片
tutorial_00.png

1 静态模板


重置项目

git checkout -f step-1

跳到步骤1,后面不再讲这一步,每次都要重置,只需要改变数字。

index.html

app/index.html:

  • Nexus S

    Fast just got faster with Nexus S.

  • Motorola XOOM? with Wi-Fi

    The Next, Next Generation tablet.

Total number of phones: 2

静态的HTML,这节没什么内容,直接进入下一部分。

2 AngularJS模板


视图和模板

视图是模型通过HTML模板渲染之后的映射。这意味着,不论模型什么时候发生变化,AngularJS会实时更新结合点,随之更新视图。

app/index.html:



  ...
  
  


  
  • {{phone.name}}

    {{phone.snippet}}

  • ng-repeat="phone in phones"是一个AngularJS迭代器。这个迭代器告诉AngularJS用第一个li标签作为模板为列表中的每一部手机创建一个li元素。

  • {{phone.name}}和{{phone.snippet}}是我们应用的一个数据模型引用,这些我们在PhoneListCtrl控制器里面都设置好了。

AngularJS Phonecat(步骤0-步骤5)_第3张图片
tutorial_02.png

模型和控制器

在PhoneListCtrl控制器里面初始化了数据模型(这里只是一个包含了数组的函数,数组中存储的对象是手机数据列表)。

app/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?",
     "snippet": "The Next, Next Generation tablet."}
  ];
}

单元测试

describe('PhoneListController', function() {

  beforeEach(module('phonecatApp'));

  it('should create a `phones` model with 3 phones', inject(function($controller) {
    var scope = {};
    var ctrl = $controller('PhoneListController', {$scope: scope});

    expect(scope.phones.length).toBe(3);
  }));

});

向命令行输入

npm test

如果未装谷歌或火狐,要修改karma.conf.js文件,否则无法正常测试。

3 组件


什么是组件

控制器+模板-->组件

一个简单的例子:

angular.
  module('myApp').
  component('greetUser', {
    template: 'Hello, {{$ctrl.user}}!',
    controller: function GreetUserController() {
      this.user = 'world';
    }
  });

可以在视图中引入<,Angular将它扩展为DOM子树,由模板生成结构,控制器进行管理。
默认情况下,组件使用$ CTRL作为控制器的别名。

在代码中使用组件

app/index.html:



  ...
  
  
  



  
  



app/app.js:

// 定义主模块 `phonecatApp`
angular.module('phonecatApp', []);

app/phone-list.component.js:

// 注册组件 `phoneList`(模板+控制器)
angular.
  module('phonecatApp').
  component('phoneList', {
    template:
        '
    ' + '
  • ' + '{{phone.name}}' + '

    {{phone.snippet}}

    ' + '
  • ' + '
', controller: function PhoneListController() { this.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.' } ]; } });

使用组件的好处:

  • 让index.html更简洁;

  • 更好地分离视图和模型,修改index.html时不会不小心破坏组件;

  • 组件可以单独测试;

  • 组件可以复用

AngularJS Phonecat(步骤0-步骤5)_第4张图片
tutorial_03.png

组件测试

app/phone-list.component.spec.js:

describe('phoneList', function() {

  // 加载主模板
  beforeEach(module('phonecatApp'));

  // 测试控制器
  describe('PhoneListController', function() {

    it('should create a `phones` model with 3 phones', inject(function($componentController) {
      var ctrl = $componentController('phoneList');

      expect(ctrl.phones.length).toBe(3);
    }));

  });

});

4 文件夹和文件管理


我们在这一节重构文件,让代码结构更清晰,方便开发者快速查找到所需功能或片段。

让每个功能/实体拥有自己的文件。

1)为什么?

为了简单起见,开发者可能把所有代码都在一个文件中,或者将同一类型的代码放入同一个文件(例如在一个文件中放所有控制器,在另一文件中放所有部件,在第三个文件中放所有服务)。

这似乎在一开始很好地工作,但随着应用程序代码的增长,这种结构会成为一种负担维护。随着添加越来越多的功能,文件将变得越来越大,我们将难以找到自己所需代码。

2)怎么做?

将每个功能/实体(比如一个独立的控制器、一个独立的组件)放到单独的文件中。

比如,phone-list功能,文件结构如下:

app/
  phone-list/
    phone-list.component.js
    phone-list.component.spec.js
  app.js

按功能模块组织代码,而不是按功能组织代码。

1)为什么?

模块化结构的好处之一是代码重用 - 不仅在同一应用程序内,但在其他应用程序也可以重用。

代码重用的最后一个阻碍是:每个功能/部分需要声明自己、将自己注册到所有相关的模块。比如将组件注册到主模块,我们在新项目中复用该组件,就需要修改组件代码中的主模块名字。这样影响了功能的封装,复用需要修改组件内部代码。

以phoneList组件为例:

angular.
  module('phonecatApp').     //phoneList组件将自己注册到主模块phonecatApp(这样子,每次测试phonelist,spec文件会先加载phonecatApp模块。)
  component('phoneList', ...);//phoneList组件声明自己

假设我们需要开发另一个项目的手机列表。简单复制phoneList/目录到新项目,并在新项目index.html文件引入该脚本,就搞定了?
好吧,没那么简单。新项目中没有phonecatApp模块,我们需要把代码中所有的“phonecatApp”改为新项目主模块的名称。这样子既费力,而且容易出错。

2)怎么做?

更好的办法是新增一个phonelist功能模块,将phonelist组件注册到这个模块上,(英语原文:在每个功能/部分中声明自己和需要所有相关模块),在主模块(phonecatApp)中声明各功能模块的依赖关系。

改变后的phonelist目录:

app/
  phone-list/
    phone-list.module.js  //增加phonelist模块
    phone-list.component.js
    phone-list.component.spec.js
  app.module.js

app/phone-list/phone-list.module.js 模块文件:

angular.module('phoneList', []);// 定义 `phoneList` 模块

app/phone-list/phone-list.component.js 组件文件:

angular.
  module('phoneList').// 将 `phoneList`组件注册到 `phoneList` 模块上
  component('phoneList', {...});

app/app.module.js 主模块文件(由于app/app.js 现在只包含主模块,我们给它一个 .module后缀):

// 定义主模块 `phonecatApp`
angular.module('phonecatApp', [  
  'phoneList' // 将`phoneList` 模块加入依赖关系数组,这样主模块就可以访问注册到`phoneList`模块上的组件
]);

这样,在新项目中复用代码,只需要直接复制phonelist目录、在新项目主模块中添加phonelist模块的依赖关系。

外部HTML模板

1)为什么?

组件的模板让我们了解数据布局并将HTML代码片段展示给用户。在步骤3中,我们使用字符串的来编写内联模板,但这种方式并不理想的,尤其是对于较大的模板。更好的方式是使用.html文件编写HTML代码,这样在编辑器写代码更顺畅(例如特定的HTML颜色突出显示和自动完成),也能让组件更简洁易读。

2)怎么做?

使用外部模板重构phoneList组件,在组件中用模板url属性指定需要加载的模板,并将模板放在phone-list/ 目录下。

增加外部模板:

将HTML代码复制到app/phone-list/phone-list.template.html中。

修改组件代码:

app/phone-list/phone-list.component.js:
angular.
module('phoneList').
component('phoneList', {
  // 注意:url关联到 `index.html`
  templateUrl: 'phone-list/phone-list.template.html',
  controller: ...
});

当创建phoneList组件的一个实例时,phone-list.component.js会通过http请求得到app/phone-list/phone-list.template.html模板。

使用外部模板虽然好,但会导致http请求增加。所以,Angular还通过$templateRequest和$templateCache来管理外部模板。

文件目录最终布局

app/
  phone-list/
    phone-list.component.js
    phone-list.component.spec.js
    phone-list.module.js
    phone-list.template.html
  app.css
  app.module.js
  index.html

测试

之前phonelist组件进行单元测试时,需要加载主模块,主模块代码增长会影响测试效率。现在只需要加载phonelist模块,这样更加载的内容更少、测试更快。

app/phone-list/phone-list.component.spec.js:

describe('phoneList', function() {

  // Load the module that contains the `phoneList` component before each test
  beforeEach(module('phoneList'));

  ...

});

5 搜索框--过滤迭代器


phone-list模板

app/phone-list/phone-list.template.html:

Search:
  • {{phone.name}}

    {{phone.snippet}}

  • 添加了一个标签,并且使用AngularJS的$filter函数来处理ngRepeat指令的输入。
  • 数据绑定:输入框和过滤器绑定"$ctrl.query",当用户向输入框输入值时,过滤器可以马上获取该值。
  • 搜索功能:filter函数使用query的值过滤数据,得到匹配query的手机数组。迭代器会根据filter生成的手机数组来自动更新视图。
AngularJS Phonecat(步骤0-步骤5)_第5张图片
tutorial_05.png

端到端测试

e2e-tests/scenarios.js:

describe('PhoneCat Application', function() {

  describe('phoneList', function() {

    beforeEach(function() {
      browser.get('index.html');
    });

    it('should filter the phone list as a user types into the search box', function() {
      var phoneList = element.all(by.repeater('phone in $ctrl.phones'));
      var query = element(by.model('$ctrl.query'));

      expect(phoneList.count()).toBe(3);

      query.sendKeys('nexus');
      expect(phoneList.count()).toBe(1);

      query.clear();
      query.sendKeys('motorola');
      expect(phoneList.count()).toBe(2);
    });

  });

});

命令行输入:npm run protractor,自动进行测试。

6-7节:AngularJS Phonecat (步骤6-步骤7)
8-9节:AngularJS Phonecat (步骤8-步骤9)
10-12节:AngularJS Phonecat(步骤10-步骤12)
13-14节:AngularJS Phonecat(步骤13-步骤14)

你可能感兴趣的:(AngularJS Phonecat(步骤0-步骤5))