【一起学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表达式。

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

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

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

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


模型和控制器

本例中的模型数据十分简单(一个简单的手机信息数组),它由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中的<html ng-app="phonecatApp">吗?这里的名字可不是随便写的,而是要和JS中新建的Angular应用名称一致),然后为这个应用添加了一个控制器PhoneListCtrl,在这个控制器中,我们在该域下定义了一个数组变量phones,并且为它赋值。
这个控制器代码看起来是十分简单的,不过它扮演的角色可是很关键的。它提供给我们一个存放模型数据的上下文(就是那个$scope参数),从而允许我们轻松建立模型和视图之间的绑定。Anglar在整个表示层、数据层、逻辑层的关联中做了以下两件事:

  • <body>标签中的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 number</th></tr>
  <tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i}}</td></tr>
</table>

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

<table>
  <tr><th>row number</th></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设计方法构建一个动态网站,以及如何测试它。接下来的一章,我们将为这个网站添加全文搜索功能。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容