您的位置:首页 > Web前端 > AngularJS

AngularJS Phonecat(步骤0-步骤5)

2017-07-29 17:34 357 查看

导言

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

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



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:

<!doctype html>
<html lang="en" ng-app>
<head>
<meta charset="utf-8">
<title>My HTML File</title>
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />
<script src="bower_components/angular/angular.js"></script>
</head>
<body>

<p>Nothing here {{'yet' + '!'}}</p>

</body>
</html>

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

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

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

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

程序结构如下:



tutorial_00.png

1 静态模板

重置项目

git checkout -f step-1

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

index.html

app/index.html:

<ul>
<li>
<span>Nexus S</span>
<p>
Fast just got faster with Nexus S.
</p>
</li>
<li>
<span>Motorola XOOM? with Wi-Fi</span>
<p>
The Next, Next Generation tablet.
</p>
</li>
</ul>
<p>Total number of phones: 2</p>

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

2 AngularJS模板

视图和模板

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

app/index.html:

<html ng-app>
<head>
...
<script src="lib/angular/angular.js"></script>
<script src="js/controllers.js"></script>
</head>
<body ng-controller="PhoneListCtrl">
<ul>
<li ng-repeat="phone in phones">
{{phone.name}}
<p>{{phone.snippet}}</p>
</li>
</ul>
</body>
</html>


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

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



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';
}
});

可以在视图中引入
<<greet-user></greet-user>
,Angular将它扩展为DOM子树,由模板生成结构,控制器进行管理。

默认情况下,组件使用$ CTRL作为控制器的别名。

在代码中使用组件

app/index.html:

<html ng-app="phonecatApp">
<head>
...
<script src="bower_components/angular/angular.js"></script>
<script src="app.js"></script>
<script src="phone-list.component.js"></script>
</head>
<body>

<!-- 使用自定义组件渲染手机列表 -->
<phone-list></phone-list>

</body>
</html>

app/app.js:

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

app/phone-list.component.js:

// 注册组件 `phoneList`(模板+控制器)
angular.
module('phonecatApp').
component('phoneList', {
template:
'<ul>' +
'<li ng-repeat="phone in $ctrl.phones">' +
'<span>{{phone.name}}</span>' +
'<p>{{phone.snippet}}</p>' +
'</li>' +
'</ul>',
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时不会不小心破坏组件;

组件可以单独测试;

组件可以复用



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:

<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<!--Sidebar content-->

Search: <input ng-model="$ctrl.query" />

</div>
<div class="col-md-10">
<!--Body content-->

<ul class="phones">
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query">
<span>{{phone.name}}</span>
<p>{{phone.snippet}}</p>
</li>
</ul>

</div>
</div>
</div>

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



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)

作者:minxuan

链接:http://www.jianshu.com/p/85220c95f3eb

來源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: