Angular 開發者常犯的錯誤

閱讀 Angular 6/RxJS 最新教程,請訪問 前端修仙之路

本文基於 Top Common Mistakes of Angular Developers 這篇文章的內容進行整理和擴展,建議有興趣的讀者直接閱讀原文。若是你剛接觸 Angular,也能夠參考一下 Angular 常見問題彙總 這篇文章。javascript

Angular vs Angular 2 vs Angular 4

Angular 1.x 版本統稱爲 AngularJS,Angular 2+ (4/5) 統稱爲 Angular。前端

第三方庫的命名也有必定的規則。假設早期版本的命名以 ng- 做爲前綴,當 Angular 2 發佈後,該庫名稱會使用 ng2- 做爲前綴。但當 Angular 4 發佈之後,新的命名規則就隨之出現了。新的術語是使用 ngx- 做爲前綴,由於 Angular 使用語義版本,每六個月會發佈一個新版本。所以,舉個例子,當咱們把 ng2-bootstrap 改名爲 ngx-bootstrap 後,從此就不須要再頻繁更換庫的名稱了。java

ngOnChanges vs ngDoCheck

AngularJS 使用 watcherlistener 的概念。watcher 是一個函數,返回被監測的值。一般狀況下,這些值是對象模型的屬性值。但也不老是數據模型的屬性 - 咱們能夠跟蹤組件的狀態、計算新值等。若是該函數返回的值與前一次的值不同,Angular 就會調用 listener,一般它用來更新 UI 狀態。jquery

Angular 移除了 watchscope,如今咱們將使用組件的輸入屬性。除此以外,Angular 爲咱們提供了 ngOnChanges 生命週期鉤子。爲了提升變化檢測的性能,對於對象比較,Angular 內部直接使用 === 運算符進行值比較。所以當輸入屬性是引用類型,當改變對象內部屬性時,是不會調用 ngOnChanges 生命週期鉤子的。git

// JS has NaN !== NaN
export function looseIdentical(a: any, b: any): boolean {
  return a === b || typeof a === 'number' && typeof b === 'number'
    && isNaN(a) && isNaN(b);
}

許多開發人員不知道這一點,陷入這個陷阱。爲了解決這個問題,有各類解決方案:github

  • 使用 ngDoCheck 生命週期鉤子
  • 使用不可變的數據結構
  • 把輸入對象拆分爲多個輸入 (即不是直接傳遞引用對象,而是把內部屬性抽離成獨立的字段)
  • 使用 subscriptions

使用 ngDoCheck 生命週期掛鉤是解決此問題的經常使用方法。當變化檢測運行時會自動調用此鉤子。在使用今生命週期鉤子時,你要當心控制該鉤子的內部邏輯,由於一般每分鐘會觸發屢次變化檢測 (能夠參考下面的源碼)。typescript

ngStyle 指令內部也實現了 DoCheck 接口,而後利用 KeyValueDiffer 對象來檢測對象的變化 (如內部屬性的新增、修改、移除操做)。
// packages/core/src/view/provider.ts
// 變化檢測: 
// checkAndUpdateView -> Services.updateDirectives(view, CheckType.CheckAndUpdate)
function checkAndUpdateDirectiveInline(
    view: ViewData, 
    def: NodeDef, 
    v0: any, v1: any, v2: any,
    v3: any, v4: any, v5: any, 
    v6: any, v7: any, v8: any, 
    v9: any): boolean {
  const providerData = asProviderData(view, def.index);
  const directive = providerData.instance;
  let changed = false;
  let changes: SimpleChanges = undefined !;
  const bindLen = def.bindings.length;
  // 判斷輸入屬性值是否改變,若發生改變則更新changes對象相應的屬性。 
  if (bindLen > 0 && checkBinding(view, def, 0, v0)) {
    changed = true;
    changes = updateProp(view, providerData, def, 0, v0, changes);
  }
  // ...
  if (bindLen > 9 && checkBinding(view, def, 9, v9)) {
    changed = true;
    changes = updateProp(view, providerData, def, 9, v9, changes);
  }
  // 若輸入屬性發生變化纔會調用ngOnChanges生命週期鉤子
  if (changes) {
    directive.ngOnChanges(changes);
  }
  // 若首次執行變化檢測及實現OnInit生命週期鉤子,則調用ngOnInit生命週期鉤子 
  if ((view.state & ViewState.FirstCheck) && (def.flags & NodeFlags.OnInit)) {
    directive.ngOnInit();
  }
  // 若實現DoCheck接口,則調用ngDoCheck生命週期鉤子
  if (def.flags & NodeFlags.DoCheck) {
    directive.ngDoCheck();
  }
  return changed; // 返回SimpleChanges對象
}

未及時釋放資源

你可能知道當你訂閱 Observable 對象或設置事件監聽時,在某個時間點,你須要執行取消訂閱操做,進而釋放操做系統的內存。不然,你的應用程序可能會出現內存泄露。json

@Component({ ... })
export class HeroComponent implements OnInit, OnDestroy {
  heroForm: FormGroup;
  valueChanges$: Observable;

  ngOnInit() {
    this.valueChanges$ = this.heroForm.valueChanges.subscribe(...);
  }

  ngOnDestroy() {
    this.valueChanges$.unsubscribe();
  }
}

大多數狀況下,當你在組件類中執行訂閱操做,你能夠在 ngOnDestroy 生命週期鉤子中,執行取消訂閱的操做。bootstrap

額外取消訂閱操做

上面介紹了在某些場景下須要手動執行取消訂閱操做,進而釋放相應的資源。但有些場景下,無需咱們開發者手動執行額外的取消訂閱操做。由於在這些場景下,Angular 內部會自動執行取消訂閱操做。好比,使用 async 的場景:segmentfault

@Component({
  selector: 'heroes-garden',
  template: `<hero [hero]="heroes$ | async"></todos>`
})
export class HeroesGardenComponent implements OnInit, OnDestroy {
  heroesChanged$: Observable;

  ngOnInit() {
    this.heroesChanged$ = this.store.select('heroes');
  }

  ngOnDestroy() {
    this.heroesChanged$.unsubscribe();
  }
}

除了使用 async 的場景外,還有如下場景會自動取消訂閱:

  • Observer.timer(1000).subscribe(...)
  • http.get('https://segmentfault.com/u/').subscribe(...)
  • RxJS 中的 take()、takeWhile()、first() 等操做符

若想進一步瞭解手動釋放資源和自動釋放資源的場景,能夠參考專欄 Angular 中什麼時候取消訂閱 這篇文章。

@Component.providers vs @NgModule.providers

分層依賴注入做爲 Angular 的新機制的一部分,讓咱們能夠靈活地控制依賴注入。在 AngularJS 中,服務都是單例的,而 Angular 2.x 以上的版本,咱們能夠屢次實例化一個服務。

假設咱們已經定義了一個 HeroesService 服務,用來獲取英雄信息:

@Injectable()
export class HeroesService {
  heroes: Hero[] = [];
  
  constructor(private http: Http) {
    this.http.get('http://give-me-heroes.com')
      .map(res => res.json())
      .subscribe((heroes: Hero[]) => {
         this.heroes = heroes;
    });
  }

  getHeroes() {
    return this.heroes;
  }
}

正如你所見,咱們在構造函數中獲取英雄的數據,此外咱們定義了 getHeroes() 方法,用來獲取英雄信息。

如今咱們來使用剛建立的 HeroesService 服務:

  • 在組件中聲明服務
@Component({
  selector: 'hero',
  template: '...',
  providers: [HeroesService]
})
export class HeroComponent {
  constructor(private heroesService: HeroesService) {}
}

@NgModule({
  declarations: [HeroComponent]
}
export class HeroesModule { ... }

HeroComponent 中,咱們在 @Component.providers 數組中聲明 HeroesService 服務,而後在 HeroComponent 組件類的構造函數中注入該服務。使用這種方式會有問題,每當實例化新的 HeroComponent 組件時,都會建立一個新的 HeroService 實例,這會致使發送屢次 Http 請求。

解決上述問題的一種方案是在 @NgModule.providers 中聲明服務。

  • 在模塊中聲明服務
@Component({
  selector: 'hero',
  template: '...'
})
export class HeroComponent {
  constructor(private heroesService: HeroesService) {}
}

@NgModule({
  declarations: [HeroComponent],
  providers: [HeroesService]
}
export class HeroesModule { ... }

採用這種方式的話,對於多個 HeroComponent 組件,HeroesService 服務只會被實例化一次。由於,當在模塊中聲明 provider ,它所相關的依賴對象,將是單例的,其它的模塊都可以使用它。咱們不須要經過 @NgModule.exports 數組來導出對應的 provider,它會被自動導出。

直接操做 DOM

Angular 再也不是簡單的 Web 框架,Angular 是一個平臺,它的一個優勢是容許咱們將應用程序代碼與渲染器分離,從而編寫能夠在瀏覽器、服務器上運行的應用程序,甚至能夠編寫原生應用。

此外解耦後,也爲咱們提供更多的能力,如使用 AOT (Ahead of time) 或 Web Worker。AOT 意味着在構建階段進行模板編譯,AOT 編譯模式的開發流程:

  • 使用 TypeScript 開發 Angular 應用
  • 運行 ngc 編譯應用程序

    • 使用 Angular Compiler 編譯模板,通常輸出 TypeScript 代碼
    • 運行 tsc 編譯 TypeScript 代碼
  • 使用 Webpack 或 Gulp 等其餘工具構建項目,如代碼壓縮、合併等
  • 部署應用

除此以外 AOT 還有如下優勢:

  • 在客戶端咱們不須要導入體積龐大的 angular 編譯器,這樣能夠減小咱們 JavaScript 腳本庫的大小
  • 使用 AOT 編譯後的應用,再也不包含任何 HTML 片斷,取而代之的是編譯生成的 TypeScript 代碼,這樣的話 TypeScript 編譯器就能提早發現錯誤。總而言之,採用 AOT 編譯模式,咱們的模板是類型安全的。

若是咱們如今或未來要使用這種功能,咱們須要遵照必定的規則。其中一個規則是不能使用 jQuery,document 對象或 ElementRef.nativeElement 來直接操做 DOM。具體示例以下:

@Component({ ... })
export class HeroComponent {
  constructor(private _elementRef: ElementRef) {}

  doBadThings() {
    $('.bad-with-jquery').click();
    this._elementRef.nativeElement.xyz = 'bad with native element';
    document.getElementById('bad-with-document');
  }
}

如你所見,doBadThings() 方法中有三行代碼,這三行代碼演示了直接操做 DOM 的三種方式。在 Angular 中咱們推薦經過 Renderer2 服務執行 DOM 操做 (Angular 2 中使用 Renderer)。

@Component({ ... })
export class HeroComponent {
  constructor(
    private _renderer2: Renderer2,
    private _elementRef: ElementRef) {}

  doGoodThings() {
    this._renderer2.setElementProperty(this._elementRef,
      'some-property', true);
  }
}

上面代碼中,咱們經過依賴注入方式注入 Renderer2ElementRef 實例,而後在 doGoodThings() 方法中調用 Renderer2 實例提供的 setElementProperty() 方法來設置元素的屬性。 此外,爲了方便開發者獲取視圖中的元素,Angular 爲咱們提供了 @ViewChild@ViewChildren@ContentChild@ContentChildren 等裝飾器。

渲染器是視圖層的封裝。當咱們在瀏覽器中時,將使用默認渲染器。當應用程序在不一樣平臺 (如 WebWorker ) 上運行時,渲染器將被替換爲平臺對應的渲染器。此渲染器須要實現 Renderer2 抽象類,並利用 DI (依賴注入) 機制做爲默認的 Renderer 對象注入到組件或服務中。

若想深刻了解 Angular 渲染器,能夠參考專欄 Angular Renderer (渲染器) 這篇文章。

屢次聲明同一個組件

組件是 Angular 應用程序中的常見構建塊。每一個組件都須要在 @NgModule.declarations 數組中聲明,纔可以使用。

在 Angular 中是不容許在多個模塊中聲明同一個組件,若是一個組件在多個模塊中聲明的話,那麼 Angular 編譯器將會拋出異常。例如:

@Component({
  selector: 'hero',
  template: '...',
})
export class HeroComponent { ... }

@NgModule({
  declarations: [HeroComponent]
}
export class HeroesModule { ... }

@NgModule({
  declarations: [HeroComponent]
}
export class AnotherModule { ... }

如你所見,HeroComponent 組件在 HeroesModule 以及 AnotherModule 中進行聲明。在多個模塊中使用同一個組件是容許的。但當這種狀況發生時,咱們應該考慮模塊之間的關係是什麼。若是一個模塊做爲另外一個模塊的子模塊,那麼針對上面的場景解決方案將是:

  • 在子模塊的 @NgModule.declaration 中聲明 HeroComponent 組件
  • 經過子模塊的 @NgModule.exports 數組中導出該組件
  • 在父模塊的 @NgModule.imports 數組中導入子模塊

而對於其它狀況,咱們能夠建立一個新的模塊,如 SharedModule 模塊。具體步驟以下:

  • 在 SharedModule 中聲明和導出 HeroComponent
  • 在須要使用 HeroComponent 的模塊中導入 SharedModule
NgModule({
  declarations: [HeroComponent],
  exports: [HeroComponent]
}
export class SharedModule { ... }

NgModule({
  imports: [SharedModule]
}
export class HeroesModule { ... }

@NgModule({
  imports: [SharedModule]
}
export class AnotherModule { ... }

參考資源

相關文章
相關標籤/搜索