【一起学AngularJS】第四章、Angular模版技术

本章将使用AngularJS打造动态网页。同时我们还会测试下控制器代码。
应用开发中组织代码结构的方法有很多种。对于Angular应用来说,我们鼓励使用MVC的设计方法,它可以很好的解耦代码,让其有各自的侧重关注点。下面,我们将用一点Angular和JS代码来为我们的APP添加一些模型(model)、视图(Views)和控制器(Controllers)控件。
代码不用自己写~,只需要使用git命令切换出该步骤对应的代码即可。命令如下:

git checkout -f step-2

我们假设你如第一、二章介绍的那样已经运行了你的网站,所以你只需要刷新浏览器来查看效果。如果还没有运行,请参照第一章或第二章。

视图和模版

在Angular中,视图可以被理解为是模型(Model)数据在HTML页面的一种投射。这意味着无论何时,只要模型数据发生变化,视图也应该变化。这一切将由Angular自动完成,它将更新对应的数据绑定点的数据,从而更新视图。
下面是本章实例中视图对应的模版代码:
app/index.html

<html ng-app="phonecatApp">
<head>
  ...
  <script src="bower_components/angular/angular.js">script>
  <script src="js/controllers.js">script>
head>
<body ng-controller="PhoneListCtrl">

  <ul>
    <li ng-repeat="phone in phones">
      <span>{{phone.name}}span>
      <p>{{phone.snippet}}p>
    li>
  ul>

body>
html>

我们把之前静态页面里的展示手机列表的硬编码代码换成了ngRepeat指令和2个Angualr表达式。

  • 标签里中属性ng-repeat="phone in phones"对应了Angular中的repeater(循环)指令,这个循环指令告诉Angular为phones列表中的每一个phone对象都创建一个
  • 标签。
  • 花括号中的2个表达式({{phone.name}}{{phone.snippet}})将会被Angular替换为表达式对应的值。

细心的读者可以发现,这个例子中我们新增了一种Angular命令——ng-controller,它把一个控制器即PhoneListCtrl附着在标签中。
目前来看:
花括号中的两个变量代表了模型数据,而模型数据由PhoneListCtrl控制器来完成设置或者叫做填充

注意:我们已经使用ng-app="phonecatApp"加载了一个Angular模块,其中这个模块的名称叫phonecatApp。这个模块将包含我们定义的PhoneListCtrl控制器。

如图所示为本例Angular对应的域(Scopes)。

【一起学AngularJS】第四章、Angular模版技术_第1张图片

模型和控制器

本例中的模型数据十分简单(一个简单的手机信息数组),它由PhoneListCtrl控制器负责初始化工作。 定义一个Angular控制器也很简单,它对应了一个JS函数,这个函数只有一个参数 $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.'}
  ];
});

这段代码首先定义了一个Angular应用phonecatApp(还记得大明湖畔的夏雨荷吗?...台词错了,还记得上文HTML中的吗?这里的名字可不是随便写的,而是要和JS中新建的Angular应用名称一致),然后为这个应用添加了一个控制器PhoneListCtrl,在这个控制器中,我们在该域下定义了一个数组变量phones,并且为它赋值。
这个控制器代码看起来是十分简单的,不过它扮演的角色可是很关键的。它提供给我们一个存放模型数据的上下文(就是那个$scope参数),从而允许我们轻松建立模型和视图之间的绑定。Anglar在整个表示层、数据层、逻辑层的关联中做了以下两件事:

  • 标签中的ngController命令设置的控制器名称,指导Angular使用JS文件controllers.js中我们为应用创建的名为PhoneListCtrl的控制器。
  • PhoneListCtrl控制器中的代码把对应的手机数据绑定到$scope变量里,这个$scope变量代表了一个,这个域是根域(root scope)的派生。Angular应用定义的时候也会同时创建这个

域(Scope)

官方很希望我们能理解的概念,他们认为它非常关键。一个就像胶水一样,把模版、模型和控制器黏在一起。Angular使用和包含在模版、数据模型和控制器内的信息以同步的方式来分离模型和视图(注:这句话没太看懂,原文:Angular uses scopes, along with the information contained in the template, data model, and controller, to keep models and views separate, but in sync.)。任意模型的改变将被投射在视图上,同样的视图的变化也会反射到模型中。
可以从这里angular scope documentation了解更多关于 Angular域的相关知识。

测试

Angular设计路线使用的把视图和控制器分开的方式,让代码测试也变得简单了。如果我们的把控制器函数定义在了全局范围(就是JS的全局域),我们可以mock一个简单对象来初始化这个控制器。
test/e2e/scenarios.js

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模块创建了一个控制器。(指这句phonecatApp.controller('PhoneListCtrl', function ($scope) {)。
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);
  }));

});

这个测试的过程包含4个关键步骤:

  • 在每次测试开始之前,我们告诉Angular加载phoneApp应用模块。
  • 我们要求Angular把$controller服务注入到测试函数中。
  • 我们使用$controller服务创建PhoneListCtrl控制器的一个实例。
  • 有了这个实例之后,我们就可以验证三条手机信息记录了。

编写和运行测试

Angular开发者在编写测试时很喜欢使用Jasmine开创的行为驱动开发框架(Behavior-driven Development (BBD) framework)。虽然Angular不要求你一定要使用Jasmine的这个框架,该教程中所有的测试用例都是基于Jasmine V1.3。你可以从Jasmine的主页或者Jasmine框架文档获得更多的知识。
Angular种子项目已经预先配置了使用Karma来运行单元测试,前提是你得确保你已经安装了Karma和响应的依赖控件。没有的话你需要先 npm install
然后我们输入npm test来运行我们的测试代码。有几点需要说明一下:

  • 首先Karma将自动创建Chrome和Fixfox浏览器的实例。这些都可以在后台自动完成,你无需care。Karma将自动使用创建的浏览器实例来进行测试。
  • 如果你本地只安装了这两个浏览器中的一个,那么对应的你要修改下对应的Karma配置。比如如果你只安装了Chrome浏览器,你可以打开test/karma.conf.js文件,然后做以下修改:
    ...
    browsers: ['Chrome' ],
    ...
  • 你将看到类似下面一样的终端输出:
    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)
    这就对了!说明已经通过了。
  • 要想重新运行测试,你只需要修改任意JS源代码。Karma将会监测到,然后重新运行测试。怎么样,Angular是不是很贴心?

有一点需要注意一下,如果自动测试过程中Karma打开了浏览器窗口,请不要最小化。因为有些操作系统对于最小化的浏览器,会限制其内存分配,这将会是的Karma的整个测试过程很慢。

实验小能手

在index.html中新增一个绑定,比如:

<p>Total number of phones: {{phones.length}}p>

在控制器中创建一个新的模型变量,如:

$scope.name = "World";

然后绑定其到index.html模版中:

<p>Hello, {{name}}!p>

刷新你的浏览器,看看是不是显示"Hello, World!"。
在控制器测试代码中添加对本次改动的测试,打开./test/unit/controllersSpec.js添加:

expect(scope.name).toBe('World');

index.html中定义一个循环器,用它来创建一个简单的表格:

<table>
  <tr><th>row numberth>tr>
  <tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i}}td>tr>
table>

然后,让表格的序号从1开始。

<table>
  <tr><th>row numberth>tr>
  <tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i+1}}td>tr>
table>

加分作业:尝试使用ng-repeat创建一个8*8的表格。


为了故意让单元测试出错,从而观察输出,我们可以把代码expect(scope.phones.length).toBe(3) 改成 toBe(4)

总结

现在你已经初步了解了如何使用Angular以MVC设计方法构建一个动态网站,以及如何测试它。接下来的一章,我们将为这个网站添加全文搜索功能。

原博客文章地址:http://www.tbwood.cn/articles/2016/03/08/1457434796630.html


你可能感兴趣的:(前端技术)