導(dǎo)言
最近在學(xué)AngularJS的實(shí)例教程PhoneCat Tutorial App,發(fā)現(xiàn)網(wǎng)上的中文教程都比較久遠(yuǎn),與英文版對(duì)應(yīng)不上,而且缺少組件和文件重構(gòu)兩節(jié)。所以決定自己整理一個(gè)中文簡(jiǎn)明教程,內(nèi)容較多,先整理0-5小節(jié)。
教程展示一個(gè)Angular應(yīng)用程序:
涉及如下技術(shù):
視圖和模型的雙向數(shù)據(jù)綁定;
Karma和Protractor測(cè)試;
組件化和模塊化編程。
配置
安裝Git
下載Git,并安裝。安裝后可以使用git命令行工具git bash,在教程中只用到兩個(gè)git命令:
git clone ... 從遠(yuǎn)處版本倉(cāng)庫(kù)克隆代碼到本地計(jì)算機(jī)
git checkout ...在本地計(jì)算機(jī)上查看(取出)特定版本(標(biāo)簽)的代碼
拷貝源碼
命令行中輸入:
git clone --depth=16 https://github.com/angular/angular-phonecat.git
然后打開項(xiàng)目文件:
cd angular-phonecat
注意:從現(xiàn)在開始,所有命令都是在angular-phonecat目錄下執(zhí)行。
安裝Node
下載Node.js,并安裝。所需Node的最低版本是 Node.js v4+,可以通過(guò)下面命令行確認(rèn)node版本:
node --version
安裝工具
npm install
該命令行會(huì)安裝package.jason規(guī)定的工具到node_modules文件夾,并下載AngularJS框架到app/bower_components。
下載的工具有:
Bower - 客戶端代碼包管理工具
Http-Server - 簡(jiǎn)單的本地靜態(tài)web服務(wù)器
Karma - 單元測(cè)試工具
Protractor - 端到端 (E2E) 測(cè)試工具
初步接觸項(xiàng)目
npm start: 開啟本地服務(wù)器
npm test: 運(yùn)行Karma單元測(cè)試工具
單元測(cè)試:npm test會(huì)自動(dòng)打開谷歌瀏覽器和火狐,點(diǎn)擊debug、再打開控制臺(tái)可以查看報(bào)錯(cuò)信息。測(cè)試成功時(shí),命令行窗口會(huì)返回success信息。
npm run update-webdriver: 安裝Protractor所需驅(qū)動(dòng)
npm run protractor: 運(yùn)行Protractor端到端測(cè)試
端到端測(cè)試:npm run update-webdriver、npm start、npm run protractor。測(cè)試成功時(shí),命令行窗口會(huì)返回success信息。
注意:輸入 npm start 后,應(yīng)該另外開一個(gè)命令行窗口(不能將服務(wù)器關(guān)閉,否則無(wú)法測(cè)試),再輸入 npm run protractor命令。
0 準(zhǔn)備
重置項(xiàng)目
git checkout -f step-0
該命令將重置phonecat項(xiàng)目的工作目錄,需要在每一學(xué)習(xí)步驟運(yùn)行此命令,將step-0的0改成相應(yīng)步驟的數(shù)字(如:2 AngularJS模板,則數(shù)字為2)。
啟動(dòng)服務(wù)器
npm start
在瀏覽器中輸入: http://localhost:8000/index.html,查看頁(yè)面內(nèi)容。
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元素會(huì)被Angular用作應(yīng)用程序的根(root)元素。這就是說(shuō),ng-app規(guī)定以整個(gè)html頁(yè)面還是部分元素作為Angular程序。
雙大括號(hào)(Double-curly)綁定表達(dá)式:
Nothing here {{'yet'+'!'}}
這一行展示了Angular模板應(yīng)用的兩個(gè)核心功能:{{ }}進(jìn)行綁定,簡(jiǎn)單表達(dá)式'yet'+'!'可以用于綁定。
程序結(jié)構(gòu)如下:
1 靜態(tài)模板
重置項(xiàng)目
git checkout -f step-1
跳到步驟1,后面不再講這一步,每次都要重置,只需要改變數(shù)字。
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>
靜態(tài)的HTML,這節(jié)沒(méi)什么內(nèi)容,直接進(jìn)入下一部分。
2 AngularJS模板
視圖和模板
視圖是模型通過(guò)HTML模板渲染之后的映射。這意味著,不論模型什么時(shí)候發(fā)生變化,AngularJS會(huì)實(shí)時(shí)更新結(jié)合點(diǎn),隨之更新視圖。
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"是一個(gè)AngularJS迭代器。這個(gè)迭代器告訴AngularJS用第一個(gè)<code>li</code>標(biāo)簽作為模板為列表中的每一部手機(jī)創(chuàng)建一個(gè)<code>li</code>元素。
{{phone.name}}和{{phone.snippet}}是我們應(yīng)用的一個(gè)數(shù)據(jù)模型引用,這些我們?cè)赑honeListCtrl控制器里面都設(shè)置好了。
模型和控制器
在PhoneListCtrl控制器里面初始化了數(shù)據(jù)模型(這里只是一個(gè)包含了數(shù)組的函數(shù),數(shù)組中存儲(chǔ)的對(duì)象是手機(jī)數(shù)據(jù)列表)。
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."}
];
}
單元測(cè)試
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文件,否則無(wú)法正常測(cè)試。
3 組件
什么是組件
控制器+模板-->組件
一個(gè)簡(jiǎn)單的例子:
angular.
module('myApp').
component('greetUser', {
template: 'Hello, {{$ctrl.user}}!',
controller: function GreetUserController() {
this.user = 'world';
}
});
可以在視圖中引入<code><<greet-user></greet-user></code>,Angular將它擴(kuò)展為DOM子樹,由模板生成結(jié)構(gòu),控制器進(jìn)行管理。
默認(rèn)情況下,組件使用$ 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>
<!-- 使用自定義組件渲染手機(jī)列表 -->
<phone-list></phone-list>
</body>
</html>
app/app.js:
// 定義主模塊 `phonecatApp`
angular.module('phonecatApp', []);
app/phone-list.component.js:
// 注冊(cè)組件 `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更簡(jiǎn)潔;
更好地分離視圖和模型,修改index.html時(shí)不會(huì)不小心破壞組件;
組件可以單獨(dú)測(cè)試;
組件可以復(fù)用
組件測(cè)試
app/phone-list.component.spec.js:
describe('phoneList', function() {
// 加載主模板
beforeEach(module('phonecatApp'));
// 測(cè)試控制器
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 文件夾和文件管理
我們?cè)谶@一節(jié)重構(gòu)文件,讓代碼結(jié)構(gòu)更清晰,方便開發(fā)者快速查找到所需功能或片段。
讓每個(gè)功能/實(shí)體擁有自己的文件。
1)為什么?
為了簡(jiǎn)單起見(jiàn),開發(fā)者可能把所有代碼都在一個(gè)文件中,或者將同一類型的代碼放入同一個(gè)文件(例如在一個(gè)文件中放所有控制器,在另一文件中放所有部件,在第三個(gè)文件中放所有服務(wù))。
這似乎在一開始很好地工作,但隨著應(yīng)用程序代碼的增長(zhǎng),這種結(jié)構(gòu)會(huì)成為一種負(fù)擔(dān)維護(hù)。隨著添加越來(lái)越多的功能,文件將變得越來(lái)越大,我們將難以找到自己所需代碼。
2)怎么做?
將每個(gè)功能/實(shí)體(比如一個(gè)獨(dú)立的控制器、一個(gè)獨(dú)立的組件)放到單獨(dú)的文件中。
比如,phone-list功能,文件結(jié)構(gòu)如下:
app/
phone-list/
phone-list.component.js
phone-list.component.spec.js
app.js
按功能模塊組織代碼,而不是按功能組織代碼。
1)為什么?
模塊化結(jié)構(gòu)的好處之一是代碼重用 - 不僅在同一應(yīng)用程序內(nèi),但在其他應(yīng)用程序也可以重用。
代碼重用的最后一個(gè)阻礙是:每個(gè)功能/部分需要聲明自己、將自己注冊(cè)到所有相關(guān)的模塊。比如將組件注冊(cè)到主模塊,我們?cè)谛马?xiàng)目中復(fù)用該組件,就需要修改組件代碼中的主模塊名字。這樣影響了功能的封裝,復(fù)用需要修改組件內(nèi)部代碼。
以phoneList組件為例:
angular.
module('phonecatApp'). //phoneList組件將自己注冊(cè)到主模塊phonecatApp(這樣子,每次測(cè)試phonelist,spec文件會(huì)先加載phonecatApp模塊。)
component('phoneList', ...);//phoneList組件聲明自己
假設(shè)我們需要開發(fā)另一個(gè)項(xiàng)目的手機(jī)列表。簡(jiǎn)單復(fù)制phoneList/目錄到新項(xiàng)目,并在新項(xiàng)目index.html文件引入該腳本,就搞定了?
好吧,沒(méi)那么簡(jiǎn)單。新項(xiàng)目中沒(méi)有phonecatApp模塊,我們需要把代碼中所有的“phonecatApp”改為新項(xiàng)目主模塊的名稱。這樣子既費(fèi)力,而且容易出錯(cuò)。
2)怎么做?
更好的辦法是新增一個(gè)phonelist功能模塊,將phonelist組件注冊(cè)到這個(gè)模塊上,(英語(yǔ)原文:在每個(gè)功能/部分中聲明自己和需要所有相關(guān)模塊),在主模塊(phonecatApp)中聲明各功能模塊的依賴關(guān)系。
改變后的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`組件注冊(cè)到 `phoneList` 模塊上
component('phoneList', {...});
app/app.module.js 主模塊文件(由于app/app.js 現(xiàn)在只包含主模塊,我們給它一個(gè) .module后綴):
// 定義主模塊 `phonecatApp`
angular.module('phonecatApp', [
'phoneList' // 將`phoneList` 模塊加入依賴關(guān)系數(shù)組,這樣主模塊就可以訪問(wèn)注冊(cè)到`phoneList`模塊上的組件
]);
這樣,在新項(xiàng)目中復(fù)用代碼,只需要直接復(fù)制phonelist目錄、在新項(xiàng)目主模塊中添加phonelist模塊的依賴關(guān)系。
外部HTML模板
1)為什么?
組件的模板讓我們了解數(shù)據(jù)布局并將HTML代碼片段展示給用戶。在步驟3中,我們使用字符串的來(lái)編寫內(nèi)聯(lián)模板,但這種方式并不理想的,尤其是對(duì)于較大的模板。更好的方式是使用.html文件編寫HTML代碼,這樣在編輯器寫代碼更順暢(例如特定的HTML顏色突出顯示和自動(dòng)完成),也能讓組件更簡(jiǎn)潔易讀。
2)怎么做?
使用外部模板重構(gòu)phoneList組件,在組件中用模板url屬性指定需要加載的模板,并將模板放在phone-list/ 目錄下。
增加外部模板:
將HTML代碼復(fù)制到app/phone-list/phone-list.template.html中。
修改組件代碼:
app/phone-list/phone-list.component.js:
angular.
module('phoneList').
component('phoneList', {
// 注意:url關(guān)聯(lián)到 `index.html`
templateUrl: 'phone-list/phone-list.template.html',
controller: ...
});
當(dāng)創(chuàng)建phoneList組件的一個(gè)實(shí)例時(shí),phone-list.component.js會(huì)通過(guò)http請(qǐng)求得到app/phone-list/phone-list.template.html模板。
使用外部模板雖然好,但會(huì)導(dǎo)致http請(qǐng)求增加。所以,Angular還通過(guò)$templateRequest和$templateCache來(lái)管理外部模板。
文件目錄最終布局
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
測(cè)試
之前phonelist組件進(jìn)行單元測(cè)試時(shí),需要加載主模塊,主模塊代碼增長(zhǎng)會(huì)影響測(cè)試效率。現(xiàn)在只需要加載phonelist模塊,這樣更加載的內(nèi)容更少、測(cè)試更快。
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 搜索框--過(guò)濾迭代器
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>
- 添加了一個(gè)<input>標(biāo)簽,并且使用AngularJS的$filter函數(shù)來(lái)處理ngRepeat指令的輸入。
- 數(shù)據(jù)綁定:輸入框和過(guò)濾器綁定"$ctrl.query",當(dāng)用戶向輸入框輸入值時(shí),過(guò)濾器可以馬上獲取該值。
- 搜索功能:filter函數(shù)使用query的值過(guò)濾數(shù)據(jù),得到匹配query的手機(jī)數(shù)組。迭代器會(huì)根據(jù)filter生成的手機(jī)數(shù)組來(lái)自動(dòng)更新視圖。
端到端測(cè)試
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,自動(dòng)進(jìn)行測(cè)試。
6-7節(jié):AngularJS Phonecat (步驟6-步驟7)
8-9節(jié):AngularJS Phonecat (步驟8-步驟9)
10-12節(jié):AngularJS Phonecat(步驟10-步驟12)
13-14節(jié):AngularJS Phonecat(步驟13-步驟14)