導(dǎo)語
Angular2(已經(jīng)統(tǒng)一更名為Angular,而Angular1表示1.x版本核无,以下統(tǒng)稱Angular都是2.x版本以上)的目標(biāo)是一套框架多個(gè)平臺(tái)扣唱,這是所有前端工作的理想目標(biāo)。
angular-cli它是angular框架官方的一個(gè)構(gòu)建工具团南,當(dāng)你使用 ng new xxx
創(chuàng)建一個(gè)項(xiàng)目時(shí)噪沙,所自動(dòng)生成的項(xiàng)目結(jié)構(gòu)是很有良心的。
我會(huì)從它開始吐根,以我們目前生產(chǎn)項(xiàng)目中的一些經(jīng)驗(yàn)正歼,分享一些很基礎(chǔ)的東西,希望有助于你了解整個(gè)Angular拷橘。
注:angular-cli的項(xiàng)目更新很頻繁局义,但現(xiàn)在已經(jīng)是rc0版本喜爷,所以以下不再探討任何bate版本的內(nèi)容。
一萄唇、安裝注意項(xiàng)目
angular-cli的核心是webpack檩帐,以及npm做為依賴包。但往往在安裝過程中會(huì)遇到很多奇怪問題另萤,我把這一切都追根于網(wǎng)絡(luò)問題湃密。
相信很多利用npm解決依賴包的人都知道淘寶有良心產(chǎn)品 cnpm,但這一次cnpm在安裝angular依賴包時(shí)可能會(huì)行不通。那么一個(gè)正確的安裝依賴包的姿勢應(yīng)該是:
1、Windows下必須是【管理員模式】下運(yùn)行CMD提揍;再使用 ng
命令。
2、當(dāng) ng new xx
創(chuàng)建項(xiàng)目時(shí)會(huì)自動(dòng)執(zhí)行 npm install
下載依賴包毒嫡。
3癌蚁、如果你網(wǎng)絡(luò)沒有問題的情況下,此時(shí) ng serve
就可以正常運(yùn)行兜畸。
然努释,很多時(shí)候,你可能會(huì)收到像:
懵逼了吧咬摇,無從下手了吧伐蒂。其實(shí)是因?yàn)樗蕾嚨?d.ts聲明文件是存在rawgit里,靠腰啊肛鹏,大部分網(wǎng)絡(luò)環(huán)境是被搶R莅睢!所以類似這種問題在扰,建議解決你的網(wǎng)絡(luò)問題缕减,那就是VPN。這也是前面我說cnpm也幫不了你的原因芒珠,無意黑cnpm桥狡!
UPDATE 2017-04-11 有一次我嘗試以下辦法完成:
npm install -g nrm
nrm use taobao
npm install
所以一個(gè)完整的創(chuàng)建項(xiàng)目步驟是:
-- windows下使用管理員模式CMD
-- 1、先安裝全局包
npm uninstall -g @angular/cli
npm cache clean
npm install -g @angular/cli@latest
-- 2皱卓、創(chuàng)建項(xiàng)目
ng new ng-article
cd ng-article
ng serve
-- 3裹芝、如果ng serve運(yùn)行不起來,嘗試:
+ 刪除node_modules
+ npm install
--4娜汁、依然錯(cuò)誤
+ 嘗試VPN嫂易,再循環(huán)第3步
升級老項(xiàng)目也比較簡單:
-- windows下使用管理員模式CMD
1、全局版本
npm uninstall -g @angular/cli
npm cache clean
npm install -g @angular/cli@latest
2掐禁、項(xiàng)目版本炬搭,先刪除node_modules
npm install --save-dev @angular/cli@latest
npm install
3蜈漓、最麻煩就是可能會(huì)一些配置的變更,這個(gè)只能看CHANGELOG.md宫盔。
二融虽、IDE
"工欲善其事必先利器",別著急去看生成后的文件灼芭。因?yàn)槲野l(fā)現(xiàn)很多人使用webstorm來做開發(fā)angular有额,這樣要強(qiáng)烈抗議,vs code與Typescript才是最配的好嗎彼绷?
vs code默認(rèn)對ts支持非常激進(jìn)的巍佑,必須這兩樣都是M$的東西嘛。而且寄悯,還能再加點(diǎn)擴(kuò)展萤衰,讓開發(fā)更高效。
1猜旬、Angular 2 TypeScript Snippets
一個(gè)Agnualr代碼片斷脆栋。
2、Path Intellisense
路徑感知洒擦,這讓我們在寫 import
路徑時(shí)更高效椿争。
3、Auto Import
看圖不解釋熟嫩。
4秦踪、Angular Files
創(chuàng)建Angular文件,就是
ng
命令轉(zhuǎn)化成操作掸茅,減少cmd的打開次數(shù)椅邓;看圖不解釋。VS CODE執(zhí)行ng serve
在Windows下不需要再開啟一個(gè)CMD命令窗口昧狮,只需要打開 TERMINAL(ctrl+`) 就可以直接在IDE里面使用 ng
命令希坚。
三、初始化目錄結(jié)構(gòu)解讀
1陵且、.angular-cli.json
styles
和 scripts
鍵
當(dāng)需要引入用于全局作用域的類庫裁僧,就需要添加相應(yīng)類庫的腳本和樣式,比如在使用 jQuery
慕购、bootstrap
時(shí):
"styles": [
"../node_modules/bootstrap/dist/css/bootstrap.css",
"styles.css"
],
"scripts": [
"../node_modules/jquery/dist/jquery.js",
"../node_modules/bootstrap/dist/js/bootstrap.js"
]
其實(shí)不光一些全局作用域類庫聊疲,有一些第三方(例如jQuery插件)插件,因?yàn)檫@類插件并不能被 TypeScript 識(shí)別沪悲,依然在npm安裝完相應(yīng)插件包后获洲,也需要引相應(yīng)的js和css加入到這里面。
defaults 鍵
生成方式的相關(guān)配置殿如,比如默認(rèn)初始化的項(xiàng)目都是采用 css贡珊,前端如果不使用CSS預(yù)處理語言最爬,就不要好意思說你懂前端。我就是Sass的重度依賴者门岔,所以初始化項(xiàng)目的時(shí)候會(huì)把css換成scss爱致。只需要簡單一步:
"defaults": {
"styleExt": "scss"
}
因?yàn)閍ngular-cli默認(rèn)就支持sass/scss、less寒随、stylus糠悯,你唯一要做的,就是把文件后綴由css變?yōu)閟css即可妻往。
支持JSON Schema
值得說明的是angular-cli.json配置文件支持JSON Schema互艾,每一個(gè)鍵值都會(huì)智能提醒,以及完整的含義解釋(雖然是英文的)讯泣。
2纫普、tsconfig.json
TypeScript的配置基類,為什么說基類好渠,這是因?yàn)閠s配置文件是允許被繼承的昨稼,有沒有發(fā)現(xiàn) src/tsconfig.app.json 和 src/tsconfig.spce.json 這兩個(gè)分別針對APP和測試環(huán)境的TS配置文件。那么angular-cli在執(zhí)行tsc時(shí)會(huì)把 tsconfig.json + src/tsconfig.app.json 作為真正的配置文件晦墙。
有關(guān)更多細(xì)節(jié)點(diǎn)tsconfig.json。
3肴茄、src/polyfills.ts
用于解決瀏覽器兼容問題的晌畅,比如像為了支持IE11以下可能你還可以導(dǎo)入一些ES6相應(yīng)的polyfill。
如果你需要讓一些pipe支持i18n的話寡痰,需要額外的安裝相應(yīng)intl抗楔。
Zone.js
之所以特意在這提一下zone.js,是因?yàn)門A對于angular來說非常重要拦坠,應(yīng)該說像 (click)
這些操作和zone.js息息相關(guān)连躏,這是angular團(tuán)隊(duì)專用angular開發(fā)用來解決異步任務(wù)間在上下文間傳遞數(shù)據(jù)的解決方案。有關(guān)這個(gè)話題另文在探討贞滨。
四入热、NgModule與路由
Angular引導(dǎo)啟動(dòng)時(shí)是從根模塊開始;而模塊(NgModule)定義著組件晓铆、指令勺良、服務(wù)、管道等等的訪問權(quán)限骄噪,這樣會(huì)使得每一個(gè)模塊更內(nèi)聚尚困,這也是軟件設(shè)計(jì)工程里面一直提倡且所追求的“高內(nèi)聚、低耦合”链蕊。
@NgModule({
// 聲明組件和指令
declarations: [
AppComponent
],
// 導(dǎo)入其他模塊事甜,這樣本模塊可以使用暴露出來的組件谬泌、指令、管道等
imports: [
BrowserModule,
FormsModule,
HttpModule
],
// 服務(wù)依賴注入
providers: [],
// 暴露本模塊的組件逻谦、指令掌实、管道等
exports: [],
entryComponents: [],
// APP啟動(dòng)的根組件
bootstrap: [AppComponent]
})
在代碼中已經(jīng)在大概的描述,更詳細(xì)見參考跨跨。
1潮峦、entryComponents
描述 entryComponents
時(shí),我們需要先談angular-cli的搖樹優(yōu)化勇婴,什么意思呢忱嘹?當(dāng)編譯生產(chǎn)環(huán)境代碼時(shí) ng build --prod
,angular-cli會(huì)自動(dòng)對那一些完全沒有被用到模板里的組件耕渴、管道等等自動(dòng)排除掉拘悦,那怕是你在 declarations
聲明過,這樣才可以很大幅度減少文件大小橱脸。
所以有一些組件的確不會(huì)出現(xiàn)在模板中础米,但又會(huì)用到,比如某個(gè)組件是放在模態(tài)框里面添诉,而模態(tài)框則是通過動(dòng)態(tài)加載的方式來初始化組件屁桑,這個(gè)時(shí)候這些組件如果不在 entryComponents
中定義的話就會(huì)被排除掉。
2栏赴、模塊在項(xiàng)目結(jié)構(gòu)中的應(yīng)用
前面說過模塊可以讓代碼工程更內(nèi)聚蘑斧,利在“模塊”,而器在“人”须眷;因此竖瘾,每個(gè)人如何去組織代碼結(jié)構(gòu)都會(huì)不一樣,那我是怎么做的呢花颗?
假設(shè)應(yīng)用我們都會(huì)有一個(gè)布局捕传,比如上左右結(jié)構(gòu),而正常上用戶登錄信息扩劝,左為菜單庸论,右為內(nèi)容。而唯一的特點(diǎn)是上左是通用的棒呛,右是根據(jù)路由來確定內(nèi)容葡公。
那么基于此,我的模塊分布會(huì)是這樣:
src/app
│ app.component.html
│ app.component.scss
│ app.component.spec.ts
│ app.component.ts
│ app.module.ts
├─layout // 通用布局組件
│ layout.module.ts
└─routes
│ routes.ts // 路由配置文件
│ routes.module.ts
├─trade // 訂單
│ │ trade.module.ts
│ ├─list // 訂單列表組件目錄
│ └─view // 訂單明細(xì)組件目錄
└─user // 會(huì)員
│ user.module.ts
├─list
└─view
layout模塊里面包含我上左的組件信息条霜,這個(gè)模塊與trade/user完全無關(guān)的催什;而對于trade的模塊會(huì)有相應(yīng)的list/view兩個(gè)組件。而對于 routes.module.ts
是會(huì)導(dǎo)入 trade/user 兩個(gè)模塊一些通用的模塊。
路由寫在模塊里
整個(gè)結(jié)構(gòu)中蒲凶,只出現(xiàn)一個(gè) routes.ts
文件來管理路由气筋,但它并不是用來管理所有應(yīng)用的路由,只是路由一些根級路由的配置旋圆,比如登錄宠默、未找到路由時(shí)處理方式。
export const routes = [
{
path: '',
component: LayoutComponent, // 這個(gè)組件會(huì)在每個(gè)路由中優(yōu)先加載
children: [
]
},
{ path: 'login', component: LoginComponent },
// Not found
{ path: '**', redirectTo: 'dashboard' }
]
路由就是一個(gè)帶有層次結(jié)構(gòu)的灵巧,這點(diǎn)和URI地址一樣搀矫,用/來表示區(qū)隔。
等等刻肄,那我們后面的訂單瓤球、用戶的怎么辦?怎么關(guān)聯(lián)敏弃?
模塊懶加載
模塊間的導(dǎo)入與導(dǎo)出卦羡,其實(shí)從代碼的角度來講還是很依賴的,但是我們有一種辦法可以讓這種依賴變得更模糊麦到。比如說讓路由來幫忙加載绿饵,而不是通過模塊與模塊間的編碼方式。
因此瓶颠,只需要在 routes.ts
的 children
配置路徑拟赊。
children: [
{ path: 'trade', loadChildren: './trade/trade.module#TradeModule' },
{ path: 'user', loadChildren: './user/user.module#UserModule' }
]
3、最佳實(shí)踐
@NgModule
的信息量就幾個(gè)屬性而已粹淋,本沒有什么特殊之處吸祟,而官網(wǎng)也提供了一些最佳實(shí)踐的方法供借鑒。
共享模塊
所謂共享是指在每個(gè)模塊中可能都需要用到的廓啊,比如表單模塊欢搜、Http模塊封豪、路由模塊等等谴轮,這樣的模塊你想用必須手動(dòng)導(dǎo)入。
因此吹埠,創(chuàng)建一個(gè) app/shared/shared.module.ts
模塊來管理你共享的模塊第步。
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { HttpModule, Http } from '@angular/http';
import { BootstrapModalModule } from 'ng2-bootstrap-modal';
@NgModule({
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
BootstrapModalModule.forRoot({container:document.body})
],
exports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
HttpModule,
RouterModule
]
})
// https://github.com/ocombe/ng2-translate/issues/209
export class SharedModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: SharedModule
};
}
}
Service服務(wù)不應(yīng)該放在共享模塊中,這是因?yàn)镾ervice是依靠DI來實(shí)現(xiàn)缘琅,只有DI才能保證Service是單一實(shí)例粘都。
核心模塊
如果你希望有些東西只是在Angular啟動(dòng)時(shí)初始化,然后在任何地方都可以用到刷袍,那么把這些東西放在這最適宜的翩隧。
import { NgModule, Optional, SkipSelf } from '@angular/core';
import { throwIfAlreadyLoaded } from './module-import-guard';
@NgModule({
imports: [
],
providers: [
],
declarations: [
],
exports: [
]
})
export class CoreModule {
constructor( @Optional() @SkipSelf() parentModule: CoreModule) {
throwIfAlreadyLoaded(parentModule, 'CoreModule');
}
}
既然是允許根模塊才需要的核心模塊,就不允許在其他地方被導(dǎo)入呻纹。所以還需要一個(gè)防止不小心的人堆生。
throwIfAlreadyLoaded.ts
// https://angular.io/styleguide#!#04-12
export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) {
if (parentModule) {
throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`);
}
}
五专缠、HTTP
@angular/http
已經(jīng)提供非常豐富的HTTP請求方法,但實(shí)際實(shí)踐中發(fā)現(xiàn)很麻煩淑仆,特別是在REST請求時(shí)涝婉,總感覺在寫很多很委屈的代碼。
1蔗怠、REST
只談REST墩弯,不會(huì)談別的,因?yàn)檫@樣才最配寞射,沒有之一渔工。正常我們需要這么來寫:
return this.http.get(this.url)
.map(this.extractData)
.subscribe(res => {
this.list = res;
})
.catch(this.handleError);
這是一個(gè)很標(biāo)準(zhǔn)的請求寫法,走四步:請求>提取數(shù)據(jù)>訂閱結(jié)果>異常怠惶。然而問題來了涨缚,Token?統(tǒng)一處理異常消息策治?401時(shí)跳轉(zhuǎn)登錄脓魏?這幾個(gè)問題我們當(dāng)然可以對上面代碼加工后得以滿足,但不可能每一次請求通惫,都要在做寫同樣的茂翔,哪怕是多一行代碼,也無法忍受履腋。
我找到了一個(gè)捷徑珊燎,ng2-resource-rest,TA和大部分REST客戶端沒有太多的區(qū)別(可以查閱TA的源碼遵湖,沒有幾行悔政,很簡單),只不過做了很不錯(cuò)的封裝延旧,但又能解決我上面提出的幾個(gè)問題谋国。
REST特征
一個(gè)REST URI包含了最簡單的CRUD操作,只需要簡單是幾行可以編寫一個(gè)CRUD Service迁沫。
@Injectable()
@ResourceParams({
url: 'https://domain.net/api/users'
})
export class NewRes extends ResourceCRUD<IQueryInput, INewsShort, INews> {}
-- 使用
-- this._res.query / get / save / update / remove芦瘾。
自定義基類
可以自定義一個(gè) Resource
來解決我們上面提中的幾個(gè)問題。
export class AuthResource extends Resource {
getHeaders(methodOptions: any): any {
// 在這里重寫header集畅,加入token
}
responseInterceptor(observable: Observable<any>, request: Request, methodOptions: ResourceActionBase): Observable<any> {
// 對結(jié)果統(tǒng)一處理 401近弟、API中錯(cuò)誤消息、Http Status等
}
}
更多方法挺智,可以參考github祷愉,作者寫了很多直觀的DEMO。
Service文件位置
如前面說過在 Core Module 中,把需要通過的 Service 放在里面二鳄。但迫摔,對于一些并特別針對某個(gè)組件,最好放在和 .module.ts 同等的位置泥从,當(dāng)然這取決于你對粒度的一種控制句占。
比如,我們項(xiàng)目大部分會(huì)在這樣放置REST Service躯嫉。
│ user.memory.service.ts
│ user.module.ts
│ user.service.ts
├─list
│ list.component.ts
└─view
view.component.ts
list & view 雖然是兩個(gè)不同的組件纱烘,但對于他們來說都使用著相同的Service服務(wù),但也不能把粒度做得太細(xì)祈餐,比如 list 和 view 分別有一個(gè) service擂啥。這看起來像是在男人的房間。
2帆阳、Observable
RxJS是Reactive編程庫哺壶,提供了強(qiáng)大的數(shù)據(jù)流組合與控制能力,而Observable就是其中之一蜒谤;RxJS在Angular里非常有地位山宾,網(wǎng)上很多人把他拿 Promise 相比,個(gè)人認(rèn)為是不合理的鳍徽,壓根就沒法比资锰。RxJS有豐富的組合和控制能力,而Promise只能告訴你是與不是阶祭。
數(shù)據(jù)控制
如果單純認(rèn)為Observable和Promise有實(shí)際中的運(yùn)用沒有什么區(qū)別绷杜,那說明你out了。來看一個(gè)我們真實(shí)的示例(適當(dāng)做了簡化):
-- template
<li *ngFor="let item of list | async" >{{item.time}}</li>
-- js
this.list = this.form.get('name')
.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.do(val => {
console.log('新值', val)
})
.map(val => {
// HTTP請求查詢
return new Array(5).fill(0).map(i => { return { time: val + Math.random() }; });
});
這是一個(gè)很簡單的文本框過濾列表的功能濒募,但區(qū)區(qū)幾行代碼鞭盟,帶著很不簡單的功能。有400ms的抖動(dòng)瑰剃、去重齿诉、新值的監(jiān)控、HTTP請求培他。怎么樣鹃两,這是Promise無法做到的吧遗座。
這樣的功能在我們項(xiàng)目里面舀凛,大部分列表頁都有。
Async Pipe
在用法上面是否采用Observable或Promise沒有太多區(qū)別途蒋,很多人依然還是很依賴Promise猛遍,可能因?yàn)閷W(xué)習(xí)成本低一點(diǎn)。而Observable更可以通過一些組合和控制,達(dá)到更好的編碼體驗(yàn)懊烤√菪眩看一個(gè)隔2秒生成一數(shù)據(jù)的示例:
--template
`<li *ngFor="let num of numbers | async">{{num}}</li>`
-- js
public numbers: Observable<Array<any>>;
ngOnInit() {
this.numbers = Observable.interval(1000 * 2).map( i => {
return new Array(5).fill(0).map(i => { return Math.random(); });
});
}
示例中并沒有編寫任何 subscribe
來訂閱結(jié)果,而只是在模塊中添加了 async
Pipe腌紧。這樣的好處是代碼量減少了點(diǎn)茸习、值變更時(shí)自動(dòng)檢查組件的變化、當(dāng)組件被銷毀時(shí)自動(dòng)取消訂閱避免內(nèi)存泄露壁肋。
toPromise()
很多人在通過Http請求一個(gè)數(shù)據(jù)時(shí)号胚,會(huì)使用 toPromise()
,這簡直就是多此一舉好嗎浸遗?
-- promise
this.http.get(``).toPromise().then();
-- Observate
this.http.get(``).subscribe();
使用 Promise 的好處是多寫幾個(gè)字母猫胁,翻閱 toPromise 源碼這檢查就是脫褲子放屁。
3跛锌、代理請求API
這里代理是指angular-cli在開發(fā)過程中弃秆,原因是解決跨域請求問題。非常簡單的髓帽,根目錄創(chuàng)建 proxy.conf.json
文件菠赚,內(nèi)容:
{
"/api": {
"target": "http://localhost:3000",
"secure": false
}
}
原先 ng serve
改為 ng serve --proxy-config proxy.conf.json
。
不過郑藏,建議還是使用CROS來解決跨域問題锈至,只需要簡單的后端配置,安全译秦、靠譜峡捡、方便!
六筑悴、表單
Angular提供模板和模型不同的驅(qū)動(dòng)方式來構(gòu)建復(fù)雜表單们拙,二者編寫方式完全不同。表單我最關(guān)心就是校驗(yàn)問題阁吝,目的盡可能讓后端接受到的是一個(gè)合理的數(shù)據(jù)砚婆,了解他們的風(fēng)格才能更好的掌握表單。
1突勇、模板驅(qū)動(dòng)
模板表單最核心的是 ngModel
指令装盯,依賴雙向綁定讓表單與數(shù)據(jù)同步,一個(gè)簡單的示例:
<form #f="ngForm" (ngSubmit)="onTemplateSave()">
<p>Name:<input type="text" [(ngModel)]="user.name" name="name" required maxlength="20" /></p>
<p>Pwd:<input type="password" [(ngModel)]="user.pwd" name="pwd" required /></p>
<p><button type="submit" [disabled]="!f.valid">Submit</button></p>
</form>
最核心是 ngForm
甲馋,使得表單具備一些HTML5表單屬性的檢驗(yàn)埂奈,比如 required
必填項(xiàng),并以不同CSS樣式來表達(dá)狀態(tài)定躏,所有跟校驗(yàn)有關(guān)全都在模板中完成账磺。
很明顯非常簡單芹敌,但無法完成復(fù)雜的檢驗(yàn),比如:用戶名是否重復(fù)垮抗;而且無法寫單元測試氏捞。
2、模型驅(qū)動(dòng)
把上面示例改成模型驅(qū)動(dòng)冒版。
<form [formGroup]="form" (ngSubmit)="onModelSave()">
<p>Name:<input type="text" formControlName="name" /></p>
<p>Pwd:<input type="password" formControlName="pwd" /></p>
<p><button type="submit" [disabled]="!form.valid">Submit</button></p>
</form>
nameCheck(ctrl: FormControl) {
return new Observable((obs: Observer<any>) => {
ctrl
.valueChanges
.debounceTime(500)
.distinctUntilChanged()
.map(value => {
if (value != 'admin') throw new Error('無效用戶');
return value;
})
.subscribe(
res => {
obs.next(null);
obs.complete();
},
err => {
obs.next({asyncInvalid: true});
obs.complete();
}
);
});
}
constructor(private fb: FormBuilder) {
this.form = fb.group({
'name': new FormControl('', [Validators.required, Validators.maxLength(20)], [ this.nameCheck ]),
'pwd': ['', Validators.required]
});
}
相同的功能液茎,雖代碼量上升了,但模型驅(qū)動(dòng)的可塑造性非常強(qiáng)辞嗡。示例中使用了內(nèi)置檢驗(yàn)對象 Validators
(其實(shí)這些模型和模板驅(qū)動(dòng)所采用的模型完全一置)豁护,以及自定義了一個(gè)異步檢查用戶名是否有效的檢驗(yàn)。
細(xì)心欲间,你會(huì)發(fā)現(xiàn)模板中連 ngModel
也不見了楚里,因?yàn)?this.form
已經(jīng)自帶完整的數(shù)據(jù)模型,雖然你依然可以寫上來支持雙向綁定猎贴,但這看起來會(huì)非常奇怪不建議這樣子做班缎。
3、如何選擇她渴?
很明顯二者在可塑造性有很大的區(qū)別达址,當(dāng)然二者不一定非要二選一,你完全可以混合著用趁耗。
但我建議整個(gè)項(xiàng)目最好只采用其中一種形式沉唠。特別是基于模型驅(qū)動(dòng)創(chuàng)建的表單,不光可塑造性非常強(qiáng)苛败,而且還能夠?qū)?strong>單元測試满葛。
七、關(guān)于模態(tài)框
模態(tài)在應(yīng)用的地位還是很高的罢屈,但目前并沒有發(fā)現(xiàn)讓我用得很爽的嘀韧,所有難于復(fù)用的模態(tài)組件都是假的。特別是像我們項(xiàng)目中的訂單詳情缠捌,會(huì)在訂單列表中锄贷、結(jié)算列表中、支付列表中等曼月,需要一個(gè)能別復(fù)用的模態(tài)實(shí)在太重要了谊却。
這里有一個(gè)ng2-bootstrap-modal比較不錯(cuò)的,至少滿足兩個(gè):
- 可監(jiān)控哑芹。
- 模態(tài)組件可復(fù)用炎辨。
一個(gè)簡單的示例:
@Component({
selector: 'app-list',
template: `<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" (click)="close()" >×</button>
<h4 class="modal-title">Confirm</h4>
</div>
<div class="modal-body">
<p>Are you sure?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" (click)="confirm()">OK</button>
<button type="button" class="btn btn-default" (click)="close()" >Cancel</button>
</div>
</div>
</div>`
})
export class CancelComponent extends DialogComponent<any, boolean> {
constructor(dialogService: DialogService) {
super(dialogService);
}
confirm() {
this.result = true;
this.close();
}
}
this.dialogService.addDialog(CancelComponent, {}).subscribe((isConfirmed) => {
console.log(isConfirmed)
});
雖說無法設(shè)置窗體大小、沒有遮罩層绩衷,但至少可以復(fù)用蹦魔。
八、測試
TDD在其他前端框架中很應(yīng)該不那么容易咳燕,但在Angular中是一件非常簡單的事情勿决。這一節(jié)以 TDD 編程來了解 Angular 在可測試性方面有多么牛B。
angular-cli在初始化項(xiàng)目時(shí)招盲,就安裝Karma測試任務(wù)管理工具低缩、Jasmine單元測試框架、Protractor端對端模擬用戶交互工具曹货。
使用 ng test
可以啟用Karma控制臺(tái)咆繁,以下是我對前面示例中表單的測試代碼:
/* tslint:disable:no-unused-variable */
import { TestBed, async, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ViewComponent } from './view.component';
import { SharedModule } from "../../../shared/shared.module";
describe('Component: View', () => {
let comp: ViewComponent;
let fixture: ComponentFixture<ViewComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ViewComponent],
imports: [SharedModule],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(ViewComponent);
comp = fixture.componentInstance;
});
}))
it('初始化組件', () => {
expect(comp).toBeTruthy();
});
it('檢查:表單值變更后是否有更新', () => {
comp.form.controls['name'].setValue('admin');
comp.form.controls['pwd'].setValue('admin');
expect(comp.form.value).toEqual({ name: 'admin', pwd: 'admin' });
});
it('檢查:用戶名為[admin]時(shí),表單應(yīng)該是有效', (done) => {
comp.form.controls['name'].setValue('admin');
comp.form.controls['pwd'].setValue('admin');
setTimeout(() => {
expect(comp.form.controls['name'].valid).toEqual(true);
done();
}, 1000);
});
it('檢查:用戶名為[admin1]時(shí)顶籽,表單應(yīng)該是無效', (done) => {
comp.form.controls['name'].setValue('admin1');
comp.form.controls['pwd'].setValue('admin');
setTimeout(() => {
expect(comp.form.controls['name'].invalid).toEqual(true);
done();
}, 1000);
});
});
上面分別是表單的三個(gè)相對比較變態(tài)的測試用例玩般,對表單的測試在很多前端框架是很難做到的,但你看在Angular中很輕松礼饱。
不必在意坏为,我這里用了很猥瑣的 setTimeout
來解決異步請求等待問題;但我真的找不到怎么測試這種帶有異步檢驗(yàn)的方法 _镊绪。
Angular內(nèi)部還提供 @angular/core/testing
一些測試的輔助類匀伏,這樣更有利于寫異步方面的測試代碼。
覆蓋率
當(dāng)創(chuàng)建一個(gè)新組件時(shí) ng g component xx
會(huì)自動(dòng)生成一個(gè) *.spec.ts 的測試文件蝴韭,這簡直就是逼著我們100%測試覆蓋率够颠。
檢測覆蓋率可以使用 ng test --code-coverage
,會(huì)在根目錄下生成一個(gè) /coverage
文件夾榄鉴。
E2E
E2E是一種模擬用戶操作UI流程的測試方法履磨。把上面單元測試用例,改成E2E的測試寫法:模擬用戶點(diǎn)擊用戶列表-》點(diǎn)擊某個(gè)用戶詳情》在用戶編輯頁里某個(gè)輸入用戶名》檢查用戶輸入的值是否正確庆尘。
it('導(dǎo)航》用戶列表頁》用戶詳情》輸入【asdf】》結(jié)果表單無法提交', () => {
browser.get('/');
element(by.linkText('user list')).click();
element(by.linkText('to view')).click();
element(by.id('name')).sendKeys('asdf');
element(by.id('pwd')).sendKeys('admin');
browser.sleep(1000);
let submitEl = element(by.id('submit'));
expect(submitEl.getAttribute('disabled')).toBe('true');
});
it('導(dǎo)航》用戶列表頁》用戶詳情》輸入【admin】》結(jié)果表單無法提交', () => {
browser.get('/');
element(by.linkText('user list')).click();
element(by.linkText('to view')).click();
element(by.id('name')).sendKeys('admin');
element(by.id('pwd')).sendKeys('admin');
browser.sleep(1000);
let submitEl = element(by.id('submit'));
expect(submitEl.getAttribute('disabled')).toBe(null);
});
Protractor是專為Angular打造的端對端測試框架蹬耘,用法和WebDriver差不多,不過Protractor增加一些針對 Angular 的方法减余,比如根據(jù)ngModel獲取某個(gè)元素 by.model('ngModel Name')
综苔、從列表中選擇某一行 by.repeater('book in library').row(0)
等等一些很貼心的設(shè)計(jì)。
結(jié)論
其實(shí)使用angular-cli創(chuàng)建的項(xiàng)目已經(jīng)足夠清晰位岔,無非就是分而治之如筛。而大部分時(shí)難于駕馭Angular,我認(rèn)為最核心的問題是沒有對Angular的全面性了解抒抬。
Angular默認(rèn)采用TypeScript為編碼語言杨刨,“奇怪”語法讓大部分難于入手,建議在學(xué)習(xí)Angular前擦剑,先學(xué)習(xí)ts語言妖胀,這樣會(huì)事半功倍芥颈。
npm在國內(nèi)有很多限制,雖然 cnpm 良心淘寶有一個(gè)鏡像赚抡,但某些包還是需要從 gitraw 下載一些依賴爬坑,這倒置很多人失去信心。
Angular是數(shù)據(jù)驅(qū)動(dòng)DOM涂臣,這句話很重要盾计。
另外文章大部分代碼都是直接從項(xiàng)目中截取,為了方便我在github的一份完整的示例源碼赁遗。
希望大家都盡快駕馭Angular署辉。
** 引用 **