導語
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
就可以正常運行。
然,很多時候,你可能會收到像:
懵逼了吧,無從下手了吧。其實是因為所依賴的.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
看圖不解釋。
4、Angular Files
創建Angular文件,就是
ng
命令轉化成操作,減少cmd的打開次數;看圖不解釋。VS CODE執行ng serve
在Windows下不需要再開啟一個CMD命令窗口,只需要打開 TERMINAL(ctrl+`) 就可以直接在IDE里面使用 ng
命令。
三、初始化目錄結構解讀
1、.angular-cli.json
styles
和 scripts
鍵
當需要引入用于全局作用域的類庫,就需要添加相應類庫的腳本和樣式,比如在使用 jQuery
、bootstrap
時:
"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.json 和 src/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.ts
的 children
配置路徑。
children: [
{ path: 'trade', loadChildren: './trade/trade.module#TradeModule' },
{ path: 'user', loadChildren: './user/user.module#UserModule' }
]
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。
** 引用 **