导言
最近在学AngularJS的实例教程PhoneCat Tutorial App,发现网上的中文教程都比较久远,与英文版对应不上,而且缺少组件和文件重构两节。所以决定自己整理一个中文简明教程,内容较多,先整理0-5小节。
教程展示一个Angular应用程序:
涉及如下技术:
视图和模型的双向数据绑定;
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'+'!'可以用于绑定。
程序结构如下:
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控制器里面都设置好了。
模型和控制器
在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时不会不小心破坏组件;
组件可以单独测试;
组件可以复用
组件测试
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生成的手机数组来自动更新视图。
端到端测试
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)