如何利用angular-cli組織項目結構

導語

Angular2(已經統一更名為Angular,而Angular1表示1.x版本,以下統稱Angular都是2.x版本以上)的目標是一套框架多個平臺,這是所有前端工作的理想目標。

angular-cli它是angular框架官方的一個構建工具,當你使用 ng new xxx 創建一個項目時,所自動生成的項目結構是很有良心的。

我會從它開始,以我們目前生產項目中的一些經驗,分享一些很基礎的東西,希望有助于你了解整個Angular。

注:angular-cli的項目更新很頻繁,但現在已經是rc0版本,所以以下不再探討任何bate版本的內容。

一、安裝注意項目

angular-cli的核心是webpack,以及npm做為依賴包。但往往在安裝過程中會遇到很多奇怪問題,我把這一切都追根于網絡問題

相信很多利用npm解決依賴包的人都知道淘寶有良心產品 cnpm,但這一次cnpm在安裝angular依賴包時可能會行不通。那么一個正確的安裝依賴包的姿勢應該是:

1、Windows下必須是【管理員模式】下運行CMD;再使用 ng 命令。
2、當 ng new xx 創建項目時會自動執行 npm install 下載依賴包。
3、如果你網絡沒有問題的情況下,此時 ng serve 就可以正常運行。

然,很多時候,你可能會收到像:

聲明文件安裝失敗.png

懵逼了吧,無從下手了吧。其實是因為所依賴的.d.ts聲明文件是存在rawgit里,靠腰啊,大部分網絡環境是被搶!!所以類似這種問題,建議解決你的網絡問題,那就是VPN。這也是前面我說cnpm也幫不了你的原因,無意黑cnpm!

UPDATE 2017-04-11 有一次我嘗試以下辦法完成:

npm install -g nrm
nrm use taobao
npm install

所以一個完整的創建項目步驟是:

-- windows下使用管理員模式CMD
-- 1、先安裝全局包
npm uninstall -g @angular/cli
npm cache clean
npm install -g @angular/cli@latest
-- 2、創建項目
ng new ng-article
cd ng-article
ng serve
-- 3、如果ng serve運行不起來,嘗試:
  + 刪除node_modules
  + npm install
--4、依然錯誤
  + 嘗試VPN,再循環第3步

升級老項目也比較簡單:

-- windows下使用管理員模式CMD
1、全局版本
npm uninstall -g @angular/cli
npm cache clean
npm install -g @angular/cli@latest
2、項目版本,先刪除node_modules
npm install --save-dev @angular/cli@latest
npm install

3、最麻煩就是可能會一些配置的變更,這個只能看CHANGELOG.md

二、IDE

"工欲善其事必先利器",別著急去看生成后的文件。因為我發現很多人使用webstorm來做開發angular,這樣要強烈抗議,vs code與Typescript才是最配的好嗎?

vs code默認對ts支持非常激進的,必須這兩樣都是M$的東西嘛。而且,還能再加點擴展,讓開發更高效。

1、Angular 2 TypeScript Snippets
一個Agnualr代碼片斷。
2、Path Intellisense
路徑感知,這讓我們在寫 import 路徑時更高效。
3、Auto Import
看圖不解釋。

auto import.gif

4、Angular Files
創建Angular文件,就是 ng 命令轉化成操作,減少cmd的打開次數;看圖不解釋。
Angular Files.gif

VS CODE執行ng serve

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

三、初始化目錄結構解讀

項目目錄結構.png

1、.angular-cli.json

stylesscripts

當需要引入用于全局作用域的類庫,就需要添加相應類庫的腳本和樣式,比如在使用 jQuerybootstrap 時:

"styles": [
  "../node_modules/bootstrap/dist/css/bootstrap.css",
  "styles.css"
],
"scripts": [
  "../node_modules/jquery/dist/jquery.js",
  "../node_modules/bootstrap/dist/js/bootstrap.js"
]

其實不光一些全局作用域類庫,有一些第三方(例如jQuery插件)插件,因為這類插件并不能被 TypeScript 識別,依然在npm安裝完相應插件包后,也需要引相應的js和css加入到這里面。

defaults 鍵

生成方式的相關配置,比如默認初始化的項目都是采用 css,前端如果不使用CSS預處理語言,就不要好意思說你懂前端。我就是Sass的重度依賴者,所以初始化項目的時候會把css換成scss。只需要簡單一步:

"defaults": {
  "styleExt": "scss"
}

因為angular-cli默認就支持sass/scss、less、stylus,你唯一要做的,就是把文件后綴由css變為scss即可。

支持JSON Schema

值得說明的是angular-cli.json配置文件支持JSON Schema,每一個鍵值都會智能提醒,以及完整的含義解釋(雖然是英文的)。

2、tsconfig.json

TypeScript的配置基類,為什么說基類,這是因為ts配置文件是允許被繼承的,有沒有發現 src/tsconfig.app.jsonsrc/tsconfig.spce.json 這兩個分別針對APP和測試環境的TS配置文件。那么angular-cli在執行tsc時會把 tsconfig.json + src/tsconfig.app.json 作為真正的配置文件。

有關更多細節點tsconfig.json

3、src/polyfills.ts

用于解決瀏覽器兼容問題的,比如像為了支持IE11以下可能你還可以導入一些ES6相應的polyfill。

如果你需要讓一些pipe支持i18n的話,需要額外的安裝相應intl。

Zone.js

之所以特意在這提一下zone.js,是因為TA對于angular來說非常重要,應該說像 (click) 這些操作和zone.js息息相關,這是angular團隊專用angular開發用來解決異步任務間在上下文間傳遞數據的解決方案。有關這個話題另文在探討。

四、NgModule與路由

Angular引導啟動時是從根模塊開始;而模塊(NgModule)定義著組件、指令、服務、管道等等的訪問權限,這樣會使得每一個模塊更內聚,這也是軟件設計工程里面一直提倡且所追求的“高內聚、低耦合”。

@NgModule({
  // 聲明組件和指令
  declarations: [
    AppComponent
  ],
  // 導入其他模塊,這樣本模塊可以使用暴露出來的組件、指令、管道等
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  // 服務依賴注入
  providers: [],
  // 暴露本模塊的組件、指令、管道等
  exports: [],
  entryComponents: [],
  // APP啟動的根組件
  bootstrap: [AppComponent]
})

在代碼中已經在大概的描述,更詳細見參考

1、entryComponents

描述 entryComponents 時,我們需要先談angular-cli的搖樹優化,什么意思呢?當編譯生產環境代碼時 ng build --prod,angular-cli會自動對那一些完全沒有被用到模板里的組件、管道等等自動排除掉,那怕是你在 declarations 聲明過,這樣才可以很大幅度減少文件大小。

所以有一些組件的確不會出現在模板中,但又會用到,比如某個組件是放在模態框里面,而模態框則是通過動態加載的方式來初始化組件,這個時候這些組件如果不在 entryComponents 中定義的話就會被排除掉。

2、模塊在項目結構中的應用

前面說過模塊可以讓代碼工程更內聚,利在“模塊”,而器在“人”;因此,每個人如何去組織代碼結構都會不一樣,那我是怎么做的呢?

假設應用我們都會有一個布局,比如上左右結構,而正常上用戶登錄信息,左為菜單,右為內容。而唯一的特點是上左是通用的,右是根據路由來確定內容。

那么基于此,我的模塊分布會是這樣:

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 // 訂單明細組件目錄
    └─user // 會員
        │  user.module.ts
        ├─list
        └─view

layout模塊里面包含我上左的組件信息,這個模塊與trade/user完全無關的;而對于trade的模塊會有相應的list/view兩個組件。而對于 routes.module.ts 是會導入 trade/user 兩個模塊一些通用的模塊。

路由寫在模塊里

整個結構中,只出現一個 routes.ts 文件來管理路由,但它并不是用來管理所有應用的路由,只是路由一些根級路由的配置,比如登錄、未找到路由時處理方式。

export const routes = [
    {
        path: '',
        component: LayoutComponent, // 這個組件會在每個路由中優先加載 
        children: [
        ]
    },
    { path: 'login', component: LoginComponent },
    // Not found
    { path: '**', redirectTo: 'dashboard' }
]

路由就是一個帶有層次結構的,這點和URI地址一樣,用/來表示區隔。

等等,那我們后面的訂單、用戶的怎么辦?怎么關聯?

模塊懶加載

模塊間的導入與導出,其實從代碼的角度來講還是很依賴的,但是我們有一種辦法可以讓這種依賴變得更模糊。比如說讓路由來幫忙加載,而不是通過模塊與模塊間的編碼方式。

因此,只需要在 routes.tschildren 配置路徑。

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

3、最佳實踐

@NgModule 的信息量就幾個屬性而已,本沒有什么特殊之處,而官網也提供了一些最佳實踐的方法供借鑒。

共享模塊

所謂共享是指在每個模塊中可能都需要用到的,比如表單模塊、Http模塊、路由模塊等等,這樣的模塊你想用必須手動導入。

因此,創建一個 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服務不應該放在共享模塊中,這是因為Service是依靠DI來實現,只有DI才能保證Service是單一實例。

核心模塊

如果你希望有些東西只是在Angular啟動時初始化,然后在任何地方都可以用到,那么把這些東西放在這最適宜的。

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

既然是允許根模塊才需要的核心模塊,就不允許在其他地方被導入。所以還需要一個防止不小心的人。

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 已經提供非常豐富的HTTP請求方法,但實際實踐中發現很麻煩,特別是在REST請求時,總感覺在寫很多很委屈的代碼。

1、REST

只談REST,不會談別的,因為這樣才最配,沒有之一。正常我們需要這么來寫:

return this.http.get(this.url)
            .map(this.extractData)
            .subscribe(res => {
                this.list = res;
            })
            .catch(this.handleError);

這是一個很標準的請求寫法,走四步:請求>提取數據>訂閱結果>異常。然而問題來了,Token?統一處理異常消息?401時跳轉登錄?這幾個問題我們當然可以對上面代碼加工后得以滿足,但不可能每一次請求,都要在做寫同樣的,哪怕是多一行代碼,也無法忍受。

我找到了一個捷徑,ng2-resource-rest,TA和大部分REST客戶端沒有太多的區別(可以查閱TA的源碼,沒有幾行,很簡單),只不過做了很不錯的封裝,但又能解決我上面提出的幾個問題。

REST特征

一個REST URI包含了最簡單的CRUD操作,只需要簡單是幾行可以編寫一個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。

自定義基類

可以自定義一個 Resource 來解決我們上面提中的幾個問題。

export class AuthResource extends Resource {
  getHeaders(methodOptions: any): any { 
    // 在這里重寫header,加入token
  }

  responseInterceptor(observable: Observable<any>, request: Request, methodOptions: ResourceActionBase): Observable<any> {
    // 對結果統一處理 401、API中錯誤消息、Http Status等
  }
}

更多方法,可以參考github,作者寫了很多直觀的DEMO。

Service文件位置

如前面說過在 Core Module 中,把需要通過的 Service 放在里面。但,對于一些并特別針對某個組件,最好放在和 .module.ts 同等的位置,當然這取決于你對粒度的一種控制。

比如,我們項目大部分會在這樣放置REST Service。

│  user.memory.service.ts
│  user.module.ts
│  user.service.ts
├─list
│      list.component.ts
└─view
        view.component.ts

list & view 雖然是兩個不同的組件,但對于他們來說都使用著相同的Service服務,但也不能把粒度做得太細,比如 list 和 view 分別有一個 service。這看起來像是在男人的房間。

2、Observable

RxJS是Reactive編程庫,提供了強大的數據流組合與控制能力,而Observable就是其中之一;RxJS在Angular里非常有地位,網上很多人把他拿 Promise 相比,個人認為是不合理的,壓根就沒法比。RxJS有豐富的組合和控制能力,而Promise只能告訴你是與不是。

數據控制

如果單純認為Observable和Promise有實際中的運用沒有什么區別,那說明你out了。來看一個我們真實的示例(適當做了簡化):

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

這是一個很簡單的文本框過濾列表的功能,但區區幾行代碼,帶著很不簡單的功能。有400ms的抖動、去重、新值的監控、HTTP請求。怎么樣,這是Promise無法做到的吧。

這樣的功能在我們項目里面,大部分列表頁都有。

Async Pipe

在用法上面是否采用Observable或Promise沒有太多區別,很多人依然還是很依賴Promise,可能因為學習成本低一點。而Observable更可以通過一些組合和控制,達到更好的編碼體驗。看一個隔2秒生成一數據的示例:

--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 來訂閱結果,而只是在模塊中添加了 async Pipe。這樣的好處是代碼量減少了點、值變更時自動檢查組件的變化、當組件被銷毀時自動取消訂閱避免內存泄露

toPromise()

很多人在通過Http請求一個數據時,會使用 toPromise(),這簡直就是多此一舉好嗎?

-- promise
this.http.get(``).toPromise().then();
-- Observate
this.http.get(``).subscribe();

使用 Promise 的好處是多寫幾個字母,翻閱 toPromise 源碼這檢查就是脫褲子放屁。

3、代理請求API

這里代理是指angular-cli在開發過程中,原因是解決跨域請求問題。非常簡單的,根目錄創建 proxy.conf.json 文件,內容:

{
  "/api": {
    "target": "http://localhost:3000",
    "secure": false
  }
}

原先 ng serve 改為 ng serve --proxy-config proxy.conf.json

不過,建議還是使用CROS來解決跨域問題,只需要簡單的后端配置,安全、靠譜、方便!

六、表單

Angular提供模板和模型不同的驅動方式來構建復雜表單,二者編寫方式完全不同。表單我最關心就是校驗問題,目的盡可能讓后端接受到的是一個合理的數據,了解他們的風格才能更好的掌握表單。

1、模板驅動

模板表單最核心的是 ngModel 指令,依賴雙向綁定讓表單與數據同步,一個簡單的示例:

<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表單屬性的檢驗,比如 required 必填項,并以不同CSS樣式來表達狀態,所有跟校驗有關全都在模板中完成。

很明顯非常簡單,但無法完成復雜的檢驗,比如:用戶名是否重復;而且無法寫單元測試。

2、模型驅動

把上面示例改成模型驅動。

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

相同的功能,雖代碼量上升了,但模型驅動的可塑造性非常強。示例中使用了內置檢驗對象 Validators(其實這些模型和模板驅動所采用的模型完全一置),以及自定義了一個異步檢查用戶名是否有效的檢驗。

細心,你會發現模板中連 ngModel 也不見了,因為 this.form 已經自帶完整的數據模型,雖然你依然可以寫上來支持雙向綁定,但這看起來會非常奇怪不建議這樣子做

3、如何選擇?

很明顯二者在可塑造性有很大的區別,當然二者不一定非要二選一,你完全可以混合著用。

但我建議整個項目最好只采用其中一種形式。特別是基于模型驅動創建的表單,不光可塑造性非常強,而且還能夠寫單元測試

七、關于模態框

模態在應用的地位還是很高的,但目前并沒有發現讓我用得很爽的,所有難于復用的模態組件都是假的。特別是像我們項目中的訂單詳情,會在訂單列表中、結算列表中、支付列表中等,需要一個能別復用的模態實在太重要了。

這里有一個ng2-bootstrap-modal比較不錯的,至少滿足兩個:

  • 可監控。
  • 模態組件可復用。

一個簡單的示例:

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

雖說無法設置窗體大小、沒有遮罩層,但至少可以復用

八、測試

TDD在其他前端框架中很應該不那么容易,但在Angular中是一件非常簡單的事情。這一節以 TDD 編程來了解 Angular 在可測試性方面有多么牛B。

angular-cli在初始化項目時,就安裝Karma測試任務管理工具、Jasmine單元測試框架、Protractor端對端模擬用戶交互工具。

使用 ng test 可以啟用Karma控制臺,以下是我對前面示例中表單的測試代碼:

/* 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]時,表單應該是有效', (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]時,表單應該是無效', (done) => {
        comp.form.controls['name'].setValue('admin1');
        comp.form.controls['pwd'].setValue('admin');
        setTimeout(() => {
            expect(comp.form.controls['name'].invalid).toEqual(true);
            done();
        }, 1000);
    });
});

上面分別是表單的三個相對比較變態的測試用例,對表單的測試在很多前端框架是很難做到的,但你看在Angular中很輕松。

不必在意,我這里用了很猥瑣的 setTimeout 來解決異步請求等待問題;但我真的找不到怎么測試這種帶有異步檢驗的方法 _

Angular內部還提供 @angular/core/testing 一些測試的輔助類,這樣更有利于寫異步方面的測試代碼。

覆蓋率

當創建一個新組件時 ng g component xx 會自動生成一個 *.spec.ts 的測試文件,這簡直就是逼著我們100%測試覆蓋率。

檢測覆蓋率可以使用 ng test --code-coverage,會在根目錄下生成一個 /coverage 文件夾。

E2E

E2E是一種模擬用戶操作UI流程的測試方法。把上面單元測試用例,改成E2E的測試寫法:模擬用戶點擊用戶列表-》點擊某個用戶詳情》在用戶編輯頁里某個輸入用戶名》檢查用戶輸入的值是否正確。

it('導航》用戶列表頁》用戶詳情》輸入【asdf】》結果表單無法提交', () => {
    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('導航》用戶列表頁》用戶詳情》輸入【admin】》結果表單無法提交', () => {
    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 的方法,比如根據ngModel獲取某個元素 by.model('ngModel Name')、從列表中選擇某一行 by.repeater('book in library').row(0) 等等一些很貼心的設計。

結論

其實使用angular-cli創建的項目已經足夠清晰,無非就是分而治之。而大部分時難于駕馭Angular,我認為最核心的問題是沒有對Angular的全面性了解。

  • Angular默認采用TypeScript為編碼語言,“奇怪”語法讓大部分難于入手,建議在學習Angular前,先學習ts語言,這樣會事半功倍。

  • npm在國內有很多限制,雖然 cnpm 良心淘寶有一個鏡像,但某些包還是需要從 gitraw 下載一些依賴,這倒置很多人失去信心。

  • Angular是數據驅動DOM,這句話很重要。

另外文章大部分代碼都是直接從項目中截取,為了方便我在github的一份完整的示例源碼。

希望大家都盡快駕馭Angular。

** 引用 **

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容