Angular: 最佳實踐
Note: 本文中,我將盡量避免官方在 Angular Style Guide 提及的模式和有用的實踐,而是專注我自己的經(jīng)驗得出的東西,我將用例子來說明。如果你還沒讀過官網(wǎng)指引,我建議你在閱讀本文之前讀一下。因為官網(wǎng)涵蓋了本文很多沒介紹的東西。
本文將分為幾個章節(jié)來講解,這些章節(jié)根據(jù)應(yīng)用核心需求和生命周期來拆分。現(xiàn)在,我們開始吧!
類型規(guī)范 Typing
我們主要是用 TypeScript 去編寫 Angular(也許你只是用 JavaScript 或者谷歌的 Dart 語言去寫),Angular 被稱為 TYPEScript 也是有原因的。我們應(yīng)該為我們數(shù)據(jù)添加類型限定,下面有些有用的知識點:
使用類型聯(lián)合和交集。官網(wǎng)解釋了如何使用 TS 編譯器組合類型以輕松工作。這在處理來自 RESTful API 數(shù)據(jù)的時非常有用。如下例子:
interface User {
fullname: string,
age: number,
createDate: string | Date
}
上面 createdDate 字段的類型不是 JS Date 就是字符串。這很有用,因為當(dāng)服務(wù)端提供一個 User 實例數(shù)據(jù)給你,它只能返回字符串類型的時間給你,但是你可能有一個 datepicker 控件,它將日期作為有效的 JS Date 對象返回,并且為了避免數(shù)據(jù)被誤解,我們需要在 interface 里面可選指明。
限制你的類型。在 TypeScript 中,你可以限制字段的值或者變量的值,比如:
interface Order {
status: 'pending' | 'approved' | 'rejected'
}
這實際上變成了一個標(biāo)志。如果我們有一個 Order 類型的變量,我們只能將這三個字符串中的一個分配給 status 字段,分配其他的類型 TS 編輯器都會跑出錯誤。
enum Statuses {
Pending = 1,
Approved = 2,
Rejected = 3
}
interface Order {
status: Statuses;
}
**考慮設(shè)置 noImplicitAny: true**。在應(yīng)用程序的 tsconfig.json 文件中,我們可以設(shè)置這個標(biāo)志,告訴編輯器在未明確類型時候拋出錯誤。否則,編輯器堅定它無法推斷變量的類型,而認(rèn)為是 any 類型。實際情況并非如此,盡管將該標(biāo)志設(shè)置為 true 會導(dǎo)致發(fā)生意想不到的復(fù)雜情況,當(dāng)會讓你的代碼管理得很好。
嚴(yán)格類型的代碼不容易出錯,而 TS 剛好提供了類型限制,那么我們得好好使用它。
組件 Component
組件是 Angular 的核心特性,如果你設(shè)法讓它們被組織得井井有條,你可以認(rèn)為你工作已經(jīng)完成了一半。
考慮擁有一個或者幾個基本組件類。如果你有很多重復(fù)使用的內(nèi)容,這將很好用,我們可不想講相同的代碼編寫多次吧。假設(shè)有這么一個場景:我們有幾個頁面,都要展示系統(tǒng)通知。每個通知都有已讀/未讀兩種狀態(tài),當(dāng)然,我們已經(jīng)枚舉了這兩種狀態(tài)。并且在模版中的每個地方都會顯示通知,你可以使用 ngClass 設(shè)置未通知的樣式。現(xiàn)在,我們想將通知的狀態(tài)與枚舉值進行比較,我們必須將枚舉導(dǎo)入組件。
enum Statuses {
Unread = 0,
Read = 1
}
@Component({
selector: 'component-with-enum',
template: `
<div *ngFor="notification in notifications"
[ngClass]="{'unread': notification.status == statuses.Unread}">
{{ notification.text }}
</div>
`
})
class NotificationComponent {
notifications = [
{text: 'Hello!', status: Statuses.Unread},
{text: 'Angular is awesome!', status: Statuses.Read}
];
statuses = Statuses
}
這里,我們?yōu)槊總€包含未讀通知的 HTML 元素添加了 unread 類。注意我們是怎么在組件類上創(chuàng)建一個 statuses 字段,以便我們可以在模版中使用這個枚舉。但是假如我們在多個組件中使用這個枚舉呢?或者假如我們要在不同的組件使用其他枚舉呢?我們需要不停創(chuàng)建這些字段?這似乎很多重復(fù)代碼。我們看看下面例子:
enum Statuses {
Unread = 0,
Read = 1
}
abstract class AbstractBaseComponent {
statuses = Statuses;
someOtherEnum = SomeOtherEnum;
... // lots of other reused stuff
}
@Component({
selector: 'component-with-enum',
template: `
<div *ngFor="notification in notifications"
[ngClass]="{'unread': notification.status == statuses.Unread}">
{{ notification.text }}
</div>
`
})
class NotificationComponent extends AbstractBaseComponent {
notifications = [
{text: 'Hello!', status: Statuses.Unread},
{text: 'Angular is awesome!', status: Statuses.Read}
];
}
所以,現(xiàn)在我們有一個基本組件(實際上就是一個容器),我們的組件可以從中派生以重用應(yīng)用程序的全局值和方法。
另一種情況經(jīng)常在 forms 表單中被發(fā)現(xiàn)。如果在你的 Angular 組件中有個表單,你可能有像這樣的字段或者方法:
@Component({
selector: 'component-with-form',
template: `...omitted for the sake of brevity`
})
class ComponentWithForm extends AbstractBaseComponent {
form: FormGroup;
submitted: boolean = false; // a flag to be used in template to indicate whether the user tried to submit the form
resetForm() {
this.form.reset();
}
onSubmit() {
this.submitted = true;
if (!this.form.valid) {
return;
}
// perform the actual submit logic
}
}
當(dāng)然,如果你正在大量組件中使用 Angular 表單,那么將這些邏輯移動到一個基礎(chǔ)類會更友好...但是你不需要繼承 AbstractBaseComponent,因為不是每個組件都有 form 表單。像下面這樣做比較好:
abstract class AbstractFormComponent extends AbstractBaseComponent {
form: FormGroup;
submitted: boolean = false; // a flag to be used in template to indicate whether the user tried to submit the form
resetForm() {
this.form.reset();
}
onSubmit() {
this.submitted = true;
if (!this.form.valid) {
return;
}
}
}
@Component({
selector: 'component-with-form',
template: `...omitted for the sake of brevity`
})
class ComponentWithForm extends AbstractFormComponent {
onSubmit() {
super.onSubmit();
// continue and perform the actual logic
}
}
現(xiàn)在,我們?yōu)槭褂帽韱蔚慕M件創(chuàng)建了一個單獨的類(注意:AbstractFormComponent 是如何繼承 AbstractBaseComponent ,因此我們不會丟失應(yīng)用程序的值)。這是一個不錯的示范,我們可以在真正需要的地方廣泛使用它。
容器組件。 這可能有些爭議,但是我們?nèi)匀豢梢钥紤]它是否適合我們。我們知道一個路由對應(yīng)一個 Angular 組件,但是我推薦你使用容器組件,它將處理數(shù)據(jù)(如果有數(shù)據(jù)需要傳遞的話)并將數(shù)據(jù)傳遞給另外一個組件,該組件將使用輸入所包含的真實視圖和 UI 邏輯。下面就是一個例子:
const routes: Routes = [
{path: 'user', component: UserContainerComponent}
];
@Component({
selector: 'user-container-component',
template: `<app-user-component [user]="user"></app-user-component>`
})
class UserContainerComponent {
constructor(userService: UserService) {}
ngOnInit(){
this.userService.getUser().subscribe(res => this.user = user);
/* get the user data only to pass it down to the actual view */
}
}
@Component({
selector: 'app-user-component',
template: `...displays the user info and some controls maybe`
})
class UserComponent {
@Input() user;
}
在這里,容器執(zhí)行數(shù)據(jù)的檢索(它也可能執(zhí)行一些其他常見的任務(wù))并將實際的工作委托給另外一個組件。當(dāng)你重復(fù)使用同一份 UI 并再次使用現(xiàn)有的數(shù)據(jù)時,這可能派上用場,并且是關(guān)注點分離的一個很好的例子。
小經(jīng)驗:當(dāng)我們在帶有子元素的 HTML 元素上編寫 ngFor 指令時,請考慮將該元素分離為單獨的組件,就像下面:
<-- instead of this -->
<div *ngFor="let user of users">
<h3 class="user_wrapper">{{user.name}}</h3>
<span class="user_info">{{ user.age }}</span>
<span class="user_info">{{ user.dateOfBirth | date : 'YYYY-MM-DD' }}</span>
</div>
<-- write this: -->
<user-detail-component *ngFor="let user of users" [user]="user"></user-detail-component>
這在父組件中寫更少的代碼,讓后允許委托任何重復(fù)邏輯到子組件。
服務(wù) Services
服務(wù)是 Angular 中業(yè)務(wù)邏輯存放和數(shù)據(jù)處理的方案。擁有提供數(shù)據(jù)訪問、數(shù)據(jù)操作和其他可重用邏輯的結(jié)構(gòu)良好的服務(wù)非常重要。所以,下面有幾條規(guī)則需要考慮下:
有一個 API 調(diào)用的基礎(chǔ)服務(wù)類。將簡單的 HTTP 服務(wù)邏輯放在基類中,并從中派生 API 服務(wù)。像下面這樣:
abstract class RestService {
protected baseUrl: 'http://your.api.domain';
constructor(private http: Http, private cookieService: CookieService){}
protected get headers(): Headers {
/*
* for example, add an authorization token to each request,
* take it from some CookieService, for example
* */
const token: string = this.cookieService.get('token');
return new Headers({token: token});
}
protected get(relativeUrl: string): Observable<any> {
return this.http.get(this.baseUrl + relativeUrl, new RequestOptions({headers: this.headers}))
.map(res => res.json());
// as you see, the simple toJson mapping logic also delegates here
}
protected post(relativeUrl: string, data: any) {
// and so on for every http method that your API supports
}
}
當(dāng)然,你可以寫得更加復(fù)雜,當(dāng)用法要像下面這么簡單:
@Injectable()
class UserService extends RestService {
private relativeUrl: string = '/users/';
public getAllUsers(): Observable<User[]> {
return this.get(this.relativeUrl);
}
public getUserById(id: number): Observable<User> {
return this.get(`${this.relativeUrl}${id.toString()}`);
}
}
現(xiàn)在,你只需要將 API 調(diào)用的邏輯抽象到基類中,現(xiàn)在就可以專注于你將接收哪些數(shù)據(jù)以及如何處理它。
考慮有方法(Utilites)服務(wù)。有時候,你會發(fā)現(xiàn)你的組件上有一些方法用于處理一些數(shù)據(jù),可能會對其進行預(yù)處理或者以某種方式進行處理。示例可能很多,比如,你的一個組件中可能具有上傳文件的功能,因此你需要將 JS File 對象的 Array 轉(zhuǎn)換為 FormData 實例來執(zhí)行上傳。現(xiàn)在,這些沒有涉及到邏輯,不會以任何的方式影響你的視圖,并且你的多個組件中都包含上傳文件功能,因此,我們要考慮創(chuàng)建 Utilities 方法或者 DataHelper 服務(wù)將此類功能移到那里。
使用 TypeScript 字符串枚舉規(guī)范 API url。你的應(yīng)用程序可以和不同的 API 端進行交互,因此我們希望將他們移動到字符串枚舉中,而不是在硬編碼中體現(xiàn),如下:
enum UserApiUrls {
getAllUsers = 'users/getAll',
getActiveUsers = 'users/getActive',
deleteUser = 'users/delete'
}
這能更好得了解你的 API 是怎么運作的。
盡可能考慮緩存我們的請求。Rx.js 允許你去緩存 HTTP 請求的結(jié)果(實際上,任何的 Observable 都可以,但是我們現(xiàn)在說的是 HTTP 這內(nèi)容),并且有一些示例你可能想要使用它。比如,你的 API 提供了一個接入點,返回一個 Country 對象 JSON 對象,你可以在應(yīng)用程序使用這列表數(shù)據(jù)實現(xiàn)選擇國家/地區(qū)的功能。當(dāng)然,國家不會每天都會發(fā)生變更,所以最好的做法就是拉取該數(shù)據(jù)并緩存,然后在應(yīng)用程序的生命周期內(nèi)使用緩存的版本,而不是每次都去調(diào)用 API 請求該數(shù)據(jù)。Observables 使得這變得很容易:
class CountryService {
constructor(private http: Http) {}
private countries: Observable<Country[]> = this.http.get('/api/countries')
.map(res => res.json())
.publishReplay(1) // this tells Rx to cache the latest emitted value
.refCount(); // and this tells Rx to keep the Observable alive as long as there are any Subscribers
public getCountries(): Observable<Country[]> {
return this.countries;
}
}
所以現(xiàn)在,不管什么時候你訂閱這個國家列表,結(jié)果都會被緩存,以后你不再需要發(fā)起另一個 HTTP 請求了。
模版 Templates
Angular 是使用 html 模版(當(dāng)然,還有組件、指令和管道)去渲染你應(yīng)用程序中的視圖
,所以編寫模版是不可避免的事情,并且要保持模版的整潔和易于理解是很重要的。
從模版到組件方法的委托比原始的邏輯更難。請注意,這里我用了比原始更難的詞語,而不是復(fù)雜這個詞。這是因為除了檢查直接的條件語句之外,任何邏輯都應(yīng)該寫在組件的類方法中,而不是寫在模版中。在模版中寫 *ngIf=”someVariable === 1” 是可以的,其他很長的判斷條件就不應(yīng)該出現(xiàn)在模版中。
比如,你想在模版中為未正確填寫表單控件添加 has-error 類(也就是說并非所有的校驗都通過)。你可以這樣做:
@Component({
selector: 'component-with-form',
template: `
<div [formGroup]="form"
[ngClass]="{
'has-error': (form.controls['firstName'].invalid && (submitted || form.controls['firstName'].touched))
}">
<input type="text" formControlName="firstName"/>
</div>
`
})
class SomeComponentWithForm {
form: FormGroup;
submitted: boolean = false;
constructor(private formBuilder: FormBuilder) {
this.form = formBuilder.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required]
});
}
}
上面 ngClass 聲明看起來很丑。如果我們有更多的表單控件,那么它會使得視圖更加混亂,并且創(chuàng)建了很多重復(fù)的邏輯。但是,我們也可以這樣做:
@Component({
selector: 'component-with-form',
template: `
<div [formGroup]="form" [ngClass]="{'has-error': hasFieldError('firstName')}">
<input type="text" formControlName="firstName"/>
</div>
`
})
class SomeComponentWithForm {
form: FormGroup;
submitted: boolean = false;
constructor(private formBuilder: FormBuilder) {
this.form = formBuilder.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required]
});
}
hasFieldError(fieldName: string): boolean {
return this.form.controls[fieldName].invalid && (this.submitted || this.form.controls[fieldName].touched);
}
}
現(xiàn)在,我們有了個不錯的模版,甚至可以輕松地測試我們的驗證是否與單元測試一起正常工作,而無需深入查看視圖。
讀者可能意識到我并沒有寫關(guān)于 Directives 和 Pipes 的相關(guān)內(nèi)容,那是因為我想寫篇詳細(xì)的文章,關(guān)于 Angular 中 DOM 是怎么工作的。所以本文著重介紹 Angular 應(yīng)用中的 TypeScript 的內(nèi)容。
希望本文能夠幫助你編寫更干凈的代碼,幫你更好組織你的應(yīng)用結(jié)構(gòu)。請記住,無論你做了什么決定,請保持前后一致(別鉆牛角尖...)。
本文是譯文,采用的是意譯的方式,其中加上個人的理解和注釋,原文地址是:https://medium.com/codeburst/angular-best-practices-4bed7ae1d0b7
往期精彩推薦
- Dart 知識點 - 數(shù)據(jù)類型
- Flutter 開發(fā)出現(xiàn)的那些 Bugs 和解決方案「持續(xù)更新... 」
如果讀者覺得文章還可以,不防一鍵三連:關(guān)注?點贊?收藏
