如何利用angular-cli組織項(xiàng)目結(jié)構(gòu)

導(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ì)收到像:

聲明文件安裝失敗.png

懵逼了吧咬摇,無從下手了吧伐蒂。其實(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
看圖不解釋熟嫩。

auto import.gif

4秦踪、Angular Files
創(chuàng)建Angular文件,就是 ng 命令轉(zhuǎn)化成操作掸茅,減少cmd的打開次數(shù)椅邓;看圖不解釋。
Angular Files.gif

VS CODE執(zhí)行ng serve

在Windows下不需要再開啟一個(gè)CMD命令窗口昧狮,只需要打開 TERMINAL(ctrl+`) 就可以直接在IDE里面使用 ng 命令希坚。

三、初始化目錄結(jié)構(gòu)解讀

項(xiàng)目目錄結(jié)構(gòu).png

1陵且、.angular-cli.json

stylesscripts

當(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.jsonsrc/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.tschildren 配置路徑拟赊。

children: [
  { path: 'trade', loadChildren: './trade/trade.module#TradeModule' },
  { path: 'user', loadChildren: './user/user.module#UserModule' }
]
完整示例.gif

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署辉。

** 引用 **

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市岩四,隨后出現(xiàn)的幾起案子哭尝,更是在濱河造成了極大的恐慌,老刑警劉巖剖煌,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刚夺,死亡現(xiàn)場離奇詭異,居然都是意外死亡末捣,警方通過查閱死者的電腦和手機(jī)侠姑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來箩做,“玉大人莽红,你說我怎么就攤上這事“畎睿” “怎么了安吁?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長燃辖。 經(jīng)常有香客問我鬼店,道長,這世上最難降的妖魔是什么黔龟? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任妇智,我火速辦了婚禮,結(jié)果婚禮上氏身,老公的妹妹穿的比我還像新娘巍棱。我一直安慰自己,他們只是感情好蛋欣,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布航徙。 她就那樣靜靜地躺著,像睡著了一般陷虎。 火紅的嫁衣襯著肌膚如雪到踏。 梳的紋絲不亂的頭發(fā)上杠袱,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機(jī)與錄音窝稿,去河邊找鬼楣富。 笑死,一個(gè)胖子當(dāng)著我的面吹牛讹躯,可吹牛的內(nèi)容都是我干的菩彬。 我是一名探鬼主播缠劝,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼潮梯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了惨恭?” 一聲冷哼從身側(cè)響起秉馏,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎脱羡,沒想到半個(gè)月后萝究,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡锉罐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年帆竹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脓规。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡栽连,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出侨舆,到底是詐尸還是另有隱情秒紧,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布挨下,位于F島的核電站熔恢,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏臭笆。R本人自食惡果不足惜叙淌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望愁铺。 院中可真熱鬧凿菩,春花似錦、人聲如沸帜讲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽似将。三九已至获黔,卻和暖如春蚀苛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背玷氏。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工堵未, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人盏触。 一個(gè)月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓渗蟹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親赞辩。 傳聞我的和親對象是個(gè)殘疾皇子雌芽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容