1. 程式人生 > >【薦】Angular 最佳實踐

【薦】Angular 最佳實踐

推薦文章

推薦理由

作者根據自身的專案實踐,總結出了一些Angular的最佳實踐。主要包涵了TypeScript型別的最佳使用,元件的合理使用,通用服務的封裝,模版的合理定義。

文章概要

首先作者推薦在閱讀原文之間先閱讀官方的Angular風格指南,裡面包涵了一些常見的設計模式和實用的實踐。而文中提到的建議是在《風格指南》中找不到的。最佳實踐建議如下:

利用好TypeScript型別

  1. 利用好型別的並集/交集
interface User {
  fullname: string;
  age: number;
  createdDate: string | Date
; }

此處的createdDate即可以是string型別,也可以是Date型別。

2.限制類型

interface Order {
  status: 'pending' | 'approved' | 'rejected';
}

可以指定status的數值只能是上述三者之一。

當然也可以通過列舉型別來代替這種方式:

enum Statuses {
  Pending = 1,
  Approved = 2,
  Rejected = 3
}

interface Order {
  status: Statuses;
}

3.設定“noImplicitAny”: true

在專案的tsconfig.json檔案中,建議設定“noImplicitAny”: true。這樣,所有未明確宣告型別的變數都會丟擲錯誤。

元件最佳實踐

合理地利用元件,可以有效地減少程式碼冗餘。

  1. 設定基礎元件類
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 }

如果有很多元件都需要Statuses這個列舉介面的話,每次都需要重複宣告。把它抽象成基礎類,就不需要啦。

enum Statuses {
  Unread = 0,
  Read = 1
}

abstract class AbstractBaseComponent {
  statuses = Statuses;
  someOtherEnum = SomeOtherEnum;
  ... // 其他可複用的
}

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

再進一步,對於Angular表單,不同元件通常有共同的方法。一個常規的表單類如下:

@Component({
  selector: 'component-with-form',
  template: `...omitted for the sake of brevity`
})
class ComponentWithForm extends AbstractBaseComponent {
  form: FormGroup;
  submitted: boolean = false; // 標記使用者是否嘗試提交表單

  resetForm() {
    this.form.reset();
  }

  onSubmit() {
    this.submitted = true;
    if (!this.form.valid) {
      return;
    }
    // 執行具體的提交邏輯
  }
}

如果有很多地方用到表單,表單類中就會重複很多次上述的程式碼。
是不是把這些基礎的方法抽象一下會更好呢?

abstract class AbstractFormComponent extends AbstractBaseComponent {
  form: FormGroup;
  submitted: boolean = false; 

  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();
    // 繼續執行具體的提交邏輯
  }

}

2.善用容器元件

這點作者覺得可能有點爭議,關鍵在於你要找到合適你的場景。具體是指,建議把頂級元件處理成容器元件,然後再定義一個接受資料的展示元件。這樣的好處是,將獲取輸入資料的邏輯和元件內部業務操作的邏輯分開了,也有利於展示元件的複用。

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);
    /* 獲取傳遞到真正的view元件的資料 */
  }

}

@Component({
  selector: 'app-user-component',
  template: `...displays the user info and some controls maybe`
})
class UserComponent {
  @Input() user;
}

3.元件化迴圈模板

在使用*ngFor指令時,建議將待迴圈的模板處理成元件:

<-- 不推薦 -->
<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>

<-- 推薦 -->

<user-detail-component *ngFor="let user of users" [user]="user"></user-detail-component>

這樣做的好處在於減少父元件的程式碼,同時也將程式碼可能存在的業務邏輯移到子元件中。

封裝通用的服務

提供合理結構的服務很重要,因為服務可以訪問資料,處理資料,或者封裝其他重複的邏輯。作者推薦的實踐有以下幾點:

  1. 統一封裝API的基礎服務

將基礎的HTTP服務封裝成一個基礎的服務類:

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
  }

}

真正呼叫服務的程式碼就會顯示很簡單清晰:

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

}

2.封裝通用工具服務

專案中總有一些通用的方法,跟展示無關,跟業務邏輯無關,這些方法建議封裝成一個通用的工具服務。

3.統一定義API的url

相對於直接在函式中寫死,統一定義的方法更加利於處理:

enum UserApiUrls {
  getAllUsers = 'users/getAll',
  getActiveUsers = 'users/getActive',
  deleteUser = 'users/delete'
}

4.儘可能快取請求結果

有些請求結果你可能只需要請求一次,比如地址庫,某些欄位的列舉值等。這時Rx.js的可訂閱物件就發揮作用了。

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

}

模板

將複雜一點的邏輯移至類中,不推薦直接寫在模版中。

比如表單中有個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]
    });
  }


}

但這裡的判斷邏輯很複雜,如果有多個控制元件的話,你就需要重複多次這個冗長的判斷。建議處理成:


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


}

大概就是這些啦。作者說他還沒總結關於指令和管道部分的一些實踐經驗,他想專門再寫一篇文章來說清楚Angular的DOM。我們拭目以待吧!

本文首發知乎野草。如有不當之處,歡迎指正。