Angular開發規範

目錄 css

1、         前言 html

1.1.       規範目的 前端

1.2.       侷限性 編程

2、         文件規範 json

2.1.       文件結構約定 api

2.2.       單一職責原則 緩存

2.2.1         單一規則 前端框架

2.2.2         小函數 服務器

3、         命名規範 前端工程師

3.1.       整體命名原則 

3.2.       使用點和橫槓來分隔文件名 

3.3.       符號名與文件名 

3.4.       服務名 

3.5.       引導程序 

3.6.       組件選擇器 

3.7.       組件的自定義前綴 

3.8.       指令選擇器 

3.9.       指令的自定義前綴 

3.10.         管道名 

3.11.     單元測試文件名 

3.12.         端到端(E2E)測試的文件名 

3.13.         Angular NgModule 命名 

4、         編程規範 

4.1.       類 

4.2.       常量 

4.3.       接口 

4.4.       屬性和方法 

4.5.       導入語句中的空行 

4.6.       註釋 

4.7.       優化做用域鏈 

4.8.       合併http請求 

4.9.       業務分離 

5、         應用程序結構與 NgModule 

5.1.       LIFT 

5.1.1         定位 

5.1.2         識別 

5.1.3         扁平 

5.1.4         T-DRY 

5.2.       整體結構的指導原則 

5.3.       按特性組織的目錄結構 

5.4.       應用的根模塊 

5.5.       特性模塊 

5.6.       共享特性模塊 

5.7.       核心特性模塊 

5.8.       防止屢次導入 CoreModule 

5.9.       惰性加載的目錄 

5.10.         不要直接導入惰性加載的目錄 

6、         Components 

6.1.       把組件當作元素 

6.2.       把模板和樣式從組件中分離 

6.3.       內聯輸入和輸出屬性裝飾器 

6.4.       避免爲輸入和輸出屬性指定別名 

6.5.       成員順序 

6.6.       把邏輯放到服務裏 

6.7.       不要給輸出屬性加前綴 

6.8.       把表現層邏輯放到組件類裏 

7、         指令 

7.1.       使用指令來加強已有元素 

7.2.       HostListener、HostBinding 裝飾器 和 組件元數據 host 

8、         服務 

8.1.       服務老是單例的 

8.2.       單一職責 

8.3.       提供一個服務 

8.4.       使用 @Injectable() 類裝飾器 

9、         數據服務 

9.1.       經過服務與 Web 服務器通信 

10、         生命週期鉤子 

10.1.         實現生命週期鉤子接口 

 

 

 

 

1、      前言 

1.1.規範目的 

爲提升團隊協做效率,提升代碼的可讀性、可複用性和可移植性,便於前端輸出高質量的代碼以及後期優化維護,特制訂此文檔。 

1.2.侷限性 

本文着重結合了Angular(CLI 7.2.2)這一前端框架(版本)進行規範,雖然部分規範對於原生JavaScript開發或其餘前端框架依然適用,但也有部分規範並不通用,所以本文主要面向基於 Angular (CLI 7.2.2)框架進行開發的Web前端工程師。

2、      文件規範 

2.1.文件結構約定 

在後述代碼例子中,有的文件有一個或多個類似名字的配套文件。(例如 hero.component.ts 和 hero.component.html)。 

本文將會使用像 hero.component.ts|html|css|spec 的簡寫來表示上面描述的多個文件,目的是保持本指南的簡潔性,增長描述文件結構時的可讀性。 

2.2.單一職責原則 

對全部的組件、服務等等應用單一職責原則 (single responsibility principle,SRP)。這樣可讓應用更乾淨、更易讀、更易維護、更易測試。 

2.2.1      單一規則 

  • 堅持每一個文件只定義同樣東西(例如服務或組件),考慮把文件大小限制在 400 行代碼之內。 

由於: 

1)        單組件文件很是容易閱讀、維護,並能防止在版本控制系統裏與團隊衝突。 

2)        單組件文件能夠防止一些隱蔽的程序缺陷,當把多個組件合寫在同一個文件中時,可能形成共享變量、建立意外的閉包,或者與依賴之間產生意外耦合等狀況。 

3)        單獨的組件一般是該文件默認的導出,能夠用路由器實現按需加載。 

4)        最關鍵的是,能夠加強代碼可重用性和閱讀性,減小出錯的可能性。 

2.2.2      小函數 

  • 堅持定義簡單函數,考慮限制在 75 行以內。 

由於: 

1)        簡單函數更易於測試,特別是當它們只作一件事,只爲一個目的服務時。 

2)        簡單函數促進代碼重用。 

3)        簡單函數更易於閱讀。 

4)        簡單函數更易於維護。 

5)        簡單函數可避免易在大函數中產生的隱蔽性錯誤,例如與外界共享變量、建立意外的閉包或與依賴之間產生意外耦合等。 

3、      命名規範 

命名約定對可維護性和可讀性很是重要。本文爲文件名和符號名制定了一套命名約定。 

3.1.整體命名原則 

  • 堅持全部符號使用一致的命名規則,堅持遵循同一個模式來描述符號的特性和類型,推薦的模式爲 feature.type.ts。 

由於: 

1)        命名約定提供了一致的方式來查找內容,讓你一眼就能找到。 項目的一致性是相當重要的。團隊內的一致性也很重要。整個公司的一致性會提供驚人的效率。 

2)        命名約定幫助你更快得找到想找的代碼,也更容易理解它。 

3)        目錄名和文件名應該清楚的傳遞它們的意圖。例如,app/heroes/hero-list.component.ts 包含了一個用來管理英雄列表的組件。 

3.2.使用點和橫槓來分隔文件名 

  • 堅持在描述性名字中,用橫槓來分隔單詞。 
  • 堅持使用點來分隔描述性名字和類型。 
  • 堅持遵循先描述組件特性,再描述它的類型的模式,對全部組件使用一致的類型命名規則。推薦的模式爲 feature.type.ts。 
  • 堅持使用慣用的後綴來描述類型,包括 *.service、*.component、*.pipe、.module、.directive。 必要時能夠建立更多類型名,但必須注意,不要建立太多。 

由於: 

1)        類型名字提供一致的方式來快速的識別文件中有什麼。 

2)        利用編輯器或者 IDE 的模糊搜索功能,能夠很容易地找到特定文件。 

3)        像 .service 這樣的沒有簡寫過的類型名字,描述清楚,絕不含糊。 像 .srv, .svc, 和 .serv 這樣的簡寫可能使人困惑。 

4)        爲自動化任務提供模式匹配。 

3.3.符號名與文件名 

  • 堅持爲全部東西使用一致的命名約定,以它們所表明的東西命名。 
  • 堅持使用大寫駝峯命名法來命名類。符號名匹配它所在的文件名。 
  • 堅持在符號名後面追加約定的類型後綴(例如 Component、Directive、Module、Pipe、Service)。 
  • 堅持在符號名後面追加約定的類型後綴(例如 .component.ts、.directive.ts、.module.ts、.pipe.ts、.service.ts)。 
  • 堅持在文件名後面追加約定的類型後綴(例如 .component.ts、.directive.ts、.module.ts、.pipe.ts、.service.ts)。 

由於: 

1)        遵循一致的約定能夠快速識別和引用不一樣類型的資源。 

符號名 

文件名 

@Component({ ... })

export class AppComponent { }

app.component.ts

@Component({ ... })

export class HeroesComponent { }

heroes.component.ts

@Component({ ... })

export class HeroListComponent { }

hero-list.component.ts

@Component({ ... })

export class HeroDetailComponent { }

hero-detail.component.ts

@Directive({ ... })

export class ValidationDirective { }

validation.directive.ts

@NgModule({ ... })

export class AppModule

app.module.ts

@Pipe({ name: 'initCaps' })

export class InitCapsPipe implements PipeTransform { }

init-caps.pipe.ts

@Injectable()

export class UserProfileService { }

user-profile.service.ts

 

3.4.服務名 

  • 堅持使用一致的規則命名服務,以它們的特性來命名。 
  • 堅持爲服務的類名加上 Service 後綴。 例如,獲取數據或圖表列表的服務應該命名爲 DataService 或 ChartService。 
  • 有些詞彙顯然就是服務,好比那些以「-er」後綴結尾的。好比把記日誌的服務命名爲 Logger 就比 LoggerService 更好些。須要在你的項目中決定這種特例是否能夠接受。 但不管如何,都要儘可能保持一致。 

由於: 

1)        提供一致的方式來快速識別和引用服務。 

2)        像 Logger 這樣的清楚的服務名不須要後綴。 

3)        像 Credit 這樣的,服務名是名詞,須要一個後綴。當不能明顯分辨它是服務仍是其它東西時,應該添加後綴。 

符號名 

文件名 

@Injectable()

export class HeroDataService { }

hero-data.service.ts

@Injectable()

export class CreditService { }

credit.service.ts

@Injectable()

export class Logger { }

logger.service.ts

 

3.5.引導程序 

  • 堅持把應用的引導程序和平臺相關的邏輯放到名爲 main.ts 的文件裏。 
  • 堅持在引導邏輯中包含錯誤處理代碼。 
  • 避免把應用邏輯放在 main.ts 中,而應放在組件或服務裏。 

由於: 

1)        應用的啓動邏輯遵循一致的約定。 

2)        這是從其它技術平臺借鑑的經常使用約定。 

3.6.組件選擇器 

堅持使用短橫線命名法(dashed-case)或叫烤串命名法(kebab-case)來命名組件的元素選擇器,讓元素名和自定義元素規範保持一致。 

3.7.組件的自定義前綴 

  • 堅持使用帶連字符的小寫元素選擇器值(例如 admin-users)。 
  • 堅持爲組件選擇器添加自定義前綴。例如,toh 前綴表示 Tour of Heroes(英雄指南),而前綴 admin 表示管理特性區。 
  • 堅持使用前綴來識別特性區或者應用程序自己。

由於: 

1)        防止與其它應用中的組件和原生 HTML 元素髮生命名衝突。 

2)        更容易在其它應用中推廣和共享組件。 

3)        組件在 DOM 中更容易被區分出來。

@Component({
 selector: 'toh-hero-button',
 templateUrl: './hero-button.component.html'
})
export class HeroButtonComponent {}

 

3.8.指令選擇器 

  • 堅持使用小駝峯形式命名指令的選擇器。 

由於: 

1)        可讓指令中的屬性名與視圖中綁定的屬性名保持一致。 

2)        Angular 的 HTML 解析器是大小寫敏感的,能夠識別小駝峯形式。 

3.9.指令的自定義前綴 

  • 堅持爲指令的選擇器添加自定義前綴(例如前綴 toh 來自 Tour of Heroes)。 
  • 堅持用小駝峯形式拼寫非元素選擇器,除非該選擇器用於匹配原生 HTML 屬性。 

由於: 

1)        防止名字衝突。 

2)        指令更加容易被識別。

@Directive({
    selector: '[tohValidate]'
})
export class ValidateDirective {}

 

3.10.       管道名 

  • 堅持爲全部管道使用一致的命名約定,用它們的特性來命名。 

由於: 

1)        提供一致方式快速識別和引用管道。 

符號名 

文件名 

@Pipe({ name: 'ellipsis' })

export class EllipsisPipe implements PipeTransform { }

ellipsis.pipe.ts

@Pipe({ name: 'initCaps' })

export class InitCapsPipe implements PipeTransform { }

init-caps.pipe.ts

3.11.       單元測試文件名 

  • 堅持測試規格文件名與被測試組件文件名相同。 
  • 堅持測試規格文件名添加 .spec 後綴。 

由於: 

1)        提供一致的方式來快速識別測試。 

2)        提供一個與 karma 或者其它測試運行器相配的命名模式。 

測試類型 

文件名 

組件 

heroes.component.spec.ts

hero-list.component.spec.ts

hero-detail.component.spec.ts

服務 

logger.service.spec.ts

hero.service.spec.ts

filter-text.service.spec.ts

管道 

ellipsis.pipe.spec.ts

init-caps.pipe.spec.ts

 

3.12.       端到端(E2E)測試的文件名 

  • 堅持端到端測試規格文件和它們所測試的特性同名,添加 .e2e-spec 後綴。 

由於: 

1)        提供一致的方式快速識別端到端測試文件。 

2)        提供一個與測試運行器和構建自動化匹配的模式。 

測試類型 

文件名 

端到端測試 

app.e2e-spec.ts

heroes.e2e-spec.ts

3.13.       Angular NgModule 命名 

  • 堅持爲符號名添加 Module 後綴。 
  • 堅持爲文件名添加 .module.ts 擴展名。 
  • 堅持用特性名和所在目錄命名模塊。 

由於: 

1)        提供一致的方式來快速標識和引用模塊。 

2)        大駝峯命名法是一種命名約定,用來標識可用構造函數實例化的對象。 

 

  • 很容易就能看出這個模塊是同名特性的根模塊。 
  • 堅持爲 RoutingModule 類名添加 RoutingModule 後綴。 
  • 堅持爲 RoutingModule 的文件名添加 -routing.module.ts 後綴。 

由於: 

1)        RoutingModule 是一種專門用來配置 Angular 路由器的模塊。 「類名和文件名保持一致」的約定使這些模塊易於發現和驗證。 

符號名 

文件名 

@NgModule({ ... })

export class AppModule { }

app.module.ts

@NgModule({ ... })

export class HeroesModule { }

heroes.module.ts

@NgModule({ ... })

export class VillainsModule { }

villains.module.ts

@NgModule({ ... })

export class AppRoutingModule { }

app-routing.module.ts

@NgModule({ ... })

export class HeroesRoutingModule { }

heroes-routing.module.ts

 

4、      編程規範 

堅持一致的編程、命名和空格的約定。 

4.1.類 

堅持使用大寫駝峯命名法來命名類。 

由於: 

1)        遵循類命名傳統約定。 

2)        類能夠被實例化和構造實例。根據約定,用大寫駝峯命名法來標識可構造的東西。

export class ExceptionService {
 constructor() { }
}

 

4.2.常量 

  • 堅持用 const 聲明變量,除非它們的值在應用的生命週期內會發生變化。 

由於: 

1)        告訴讀者這個值是不可變的。 

2)        TypeScript 會要求在聲明時當即初始化,並阻止再次賦值,以幫助確保你的設計意圖。 

 

  • 考慮把常量名拼寫爲小駝峯格式。 

由於: 

1)        小駝峯變量名 (heroRoutes) 比傳統的大寫蛇形命名法 (HERO_ROUTES) 更容易閱讀和理解。 

2)        把常量命名爲大寫蛇形命名法的傳統源於現代 IDE 出現以前, 以便閱讀時能夠快速發現那些 const 定義。 TypeScript 自己就可以防止意外賦值。 

 

  • 堅持允許現存的const 常量沿用大寫蛇形命名法。 

由於: 

1)        傳統的大寫蛇形命名法仍然很流行、很廣泛,特別是在第三方模塊中。 修改它們沒多大價值,還會有破壞現有代碼和文檔的風險。

export const mockHeroes = ['Sam', 'Jill']; // prefer
export const heroesUrl = 'api/heroes'; // prefer
export const VILLAINS_URL = 'api/villains'; // tolerate

 

4.3.接口 

  • 堅持使用大寫駝峯命名法來命名接口。 
  • 考慮不要在接口名字前面加 I 前綴。 
  • 考慮在服務和可聲明對象(組件、指令和管道)中用類代替接口。 
  • 考慮用接口做爲數據模型。 

由於: 

1)        TypeScript 指導原則不建議使用 「I」 前綴。 

2)        單獨一個類的代碼量小於類+接口。 

3)        類能夠做爲接口使用(只是用 implements 代替 extends 而已)。 

4)        在 Angular 依賴注入系統中,接口類(譯註:指寫成類的形式,可是隻當作接口使用)能夠做爲服務提供商的查找令牌。

import { Injectable } from '@angular/core';
import { Hero } from './hero.model';
 
@Injectable()
export class HeroCollectorService {
  hero: Hero;
  constructor() { }
}

 

4.4.屬性和方法 

  • 堅持使用小寫駝峯命名法來命名屬性和方法。 
  • 避免爲私有屬性和方法添加下劃線前綴。 

由於: 

1)        遵循傳統屬性和方法的命名約定。 

2)        JavaScript 不支持真正的私有屬性和方法。 

3)        TypeScript 工具讓識別私有或公有屬性和方法變得很簡單。

import { Injectable } from '@angular/core';
 
@Injectable()
export class ToastService {
  message: string;
 
  private toastCount: number;
 
  hide() {
    this.toastCount--;
    this.log();
  }
 
  show() {
    this.toastCount++;
    this.log();
  }
 
  private log() {
    console.log(this.message);
  }
}

 

4.5.導入語句中的空行 

  • 堅持在第三方導入和應用導入之間留一個空行。 
  • 考慮按模塊名字的字母順排列導入行。 
  • 考慮在解構表達式中按字母順序排列導入的東西。 

由於: 

1)        空行可讓閱讀和定位本地導入更加容易。 

2)        按字母順序排列可讓閱讀和定位本地導入更加容易。

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
 
import { ExceptionService, SpinnerService, ToastService } from '../../core';
import { Hero } from './hero.model';

 

4.6.註釋 

對於函數定義以及比較複雜的邏輯,要寫必要的註釋。對於函數定義的註釋以及函數內部的連續多行註釋使用塊註釋「/*…*/」,對於函數內部的單行註釋使用行註釋「//…」。 

4.7.優化做用域鏈 

循環內部引用的對象,若是層級很深,要在循環外部定義在循環內部直接使用的層級變量,以便提升JS引擎對循環內的標識符解析速度。好比有以下對象:

const fruit = {
  name: 'Apple',
  color: 'red',
  provider: {
    name: 'Jaffray',
    phones: [
      '01088888888',
      '18812345678'
    ]
  }
};
 
let dummy;
//方法一,效率較低
for(let phone of fruit.provider.phones) {
  dummy = phone;
}
 
//方法二,效率較高
const phones = fruit.provider.phones;
for(let phone of phones) {
  dummy = phone;
}

 

經測試發現,方法二比方法一少用40%的時間。在代碼量大的系統,代碼的執行效率對整個系統性能的影響很是大。 

深圳ICT項目中有部分代碼中循環中的代碼做用域鏈有待優化: 

 

4.8.合併http請求 

對於一些性質類似且同一時期會併發訪問的http請求,應與接口開發者溝通,將接口合併,以減小發送請求次數、加快併發請求執行,進而提升性能。 

好比分別獲取性別和年齡的請求,合併在一個接口完成更高效;獲取省份和市區的請求,合併在一個接口完成更高效。 

4.9.業務分離 

在構建組件的過程當中,要將業務邏輯從組件的渲染中分離出來,組件只管對指定格式的數據進行渲染,程序根據業務須要,建立不一樣的服務來完成組件須要的數據的獲取和組裝等處理,最終將處理好的數據傳遞給組件去渲染,以提升組件的可複用性能和可移植性能。 

5、      應用程序結構與 NgModule 

全部應用程序的源代碼都放到名叫 src 的目錄裏。 全部特性區都在本身的文件夾中,帶有它們本身的 NgModule。 

全部內容都遵循每一個文件一個特性的原則。每一個組件、服務和管道都在本身的文件裏。全部第三方程序包保存到其它目錄裏,而不是 src 目錄,由於你幾乎不會修改它們,因此不但願它們弄亂你的應用程序。使用本規範介紹的文件命名約定。 

5.1.LIFT 

  • 堅持組織應用的結構,力求:快速定位 (Locate) 代碼、一眼識別 (Identify) 代碼、 儘可能保持扁平結構 (Flattest) 和嘗試 (Try) 遵循 DRY (Do Not Repeat Yourself, 不重複本身) 原則。 
  • 堅持四項基本原則定義文件結構,上面的原則是按重要順序排列的。 

由於: 

1)        LIFT 提供了一致的結構,它具備擴展性強、模塊化的特性。由於容易快速鎖定代碼,提升了開發者的效率。 另外,檢查應用結構是否合理的方法是問問本身:我能快速打開與此特性有關的全部文件並開始工做嗎? 

5.1.1      定位 

  • 堅持直觀、簡單和快速地定位代碼。 

由於: 

1)        要想高效的工做,就必須能迅速找到文件,特別是當不知道(或不記得)文件名時。 把相關的文件一塊兒放在一個直觀的位置能夠節省時間。 富有描述性的目錄結構會讓你和後面的維護者眼前一亮。 

5.1.2      識別 

  • 堅持命名文件到這個程度:看到名字馬上知道它包含了什麼,表明了什麼。 
  • 堅持文件名要具備說明性,確保文件中只包含一個組件。 
  • 避免建立包含多個組件、服務或者混合體的文件。 

由於: 

1)        花費更少的時間來查找和琢磨代碼,就會變得更有效率。 較長的文件名遠勝於較短卻容易混淆的縮寫名。 

2)        當你有一組小型、緊密相關的特性時,違反一物一文件的規則可能會更好, 這種狀況下單一文件可能會比多個文件更容易發現和理解。注意這個例外。 

5.1.3      扁平 

  • 堅持儘量保持扁平的目錄結構。 
  • 考慮當同一目錄下達到 7 個或更多個文件時建立子目錄。 
  • 考慮配置 IDE,以隱藏無關的文件,例如生成出來的 .js 文件和 .js.map 文件等。 

由於: 

1)        沒人想要在超過七層的目錄中查找文件。扁平的結構有利於搜索。 

2)        另外一方面,心理學家們相信, 當關注的事物超過 9 個時,人類就會開始感到吃力。 因此,當一個文件夾中的文件有 10 個或更多個文件時,可能就是建立子目錄的時候了。 

3)        仍是根據你本身的溫馨度而定吧。 除非建立新文件夾能有顯著的價值,不然儘可能使用扁平結構。 

5.1.4      T-DRY 

  • 嘗試(Try)堅持 DRY(Don't Repeat Yourself,不重複本身)。 
  • 避免過分 DRY,以至犧牲了閱讀性。 

由於: 

1)        雖然 DRY 很重要,但若是要以犧牲 LIFT 的其它原則爲代價,那就不值得了。 這也就是爲何它被稱爲 T-DRY。 例如,把組件命名爲 hero-view.component.html 是多餘的,由於帶有 .html 擴展名的文件顯然就是一個視圖 (view)。 但若是它不那麼顯著,或不符合常規,就把它寫出來。 

5.2.整體結構的指導原則 

  • 堅持把全部源代碼都放到名爲 src 的目錄裏。 
  • 堅持若是組件具備多個伴生文件 (.ts、.html、.css 和 .spec),就爲它建立一個文件夾。 

由於: 

1)        在早期階段可以幫助保持應用的結構小巧且易於維護和移植,這樣當應用增加時就容易進化了。 

2)        組件一般有四個文件 (*.html、 *.css、 *.ts 和 *.spec.ts),它們很容易把一個目錄弄亂。 

  

5.3.按特性組織的目錄結構 

  • 堅持根據特性區命名目錄。 

由於: 

1)        開發人員能夠快速定位代碼,掃一眼就能知道每一個文件表明什麼,目錄儘量保持扁平,既沒有重複也沒有多餘的名字。 

 

2)        LIFT 原則中包含了全部這些。 

3)        遵循 LIFT 原則精心組織內容,避免應用變得雜亂無章。 

4)        當有不少文件時(例如 10 個以上),在專用目錄型結構中定位它們會比在扁平結構中更容易。 

 

  • 堅持爲每一個特性區建立一個 NgModule。 

由於: 

1)        NgModule 使惰性加載可路由的特性變得更容易。 

2)        NgModule 隔離、測試和複用特性更容易。 

5.4.應用的根模塊 

  • 堅持在應用的根目錄建立一個 NgModule(例如 /src/app)。 

由於: 

1)        每一個應用都至少須要一個根 NgModule。 

 

  • 考慮把根模塊命名爲 app.module.ts。 

由於: 

1)        能讓定位和識別根模塊變得更容易。 

app/app.module.ts:

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
  
import { AppComponent }    from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
  
@NgModule({
  imports: [
    BrowserModule,
  ],
  declarations: [
    AppComponent,
    HeroesComponent
  ],
  exports: [ AppComponent ],
  entryComponents: [ AppComponent ]
})
export class AppModule {}

 

5.5.特性模塊 

  • 堅持爲應用中每一個明顯的特性建立一個 NgModule。 
  • 堅持把特性模塊放在與特性區同名的目錄中(例如 app/heroes)。 
  • 堅持特性模塊的文件名應該能反映出特性區的名字和目錄(例如 app/heroes/heroes.module.ts)。 
  • 堅持特性模塊的符號名應該能反映出特性區、目錄和文件名(例如在 app/heroes/heroes.module.ts 中定義 HeroesModule)。 

由於: 

1)        特性模塊能夠對其它模塊暴露或隱藏本身的實現。 

2)        特性模塊標記出組成該特性分區的相關組件集合。 

3)        方便路由到特性模塊 —— 不管是用主動加載仍是惰性加載的方式。 

4)        特性模塊在特定的功能和其它應用特性之間定義了清晰的邊界。 

5)        特性模塊幫助澄清開發職責,以便於把這些職責指派給不一樣的項目組。 

6)        特性模塊易於隔離,以便測試。 

5.6.共享特性模塊 

  • 堅持在 shared 目錄中建立名叫 SharedModule 的特性模塊(例如在 app/shared/shared.module.ts 中定義 SharedModule)。 
  • 堅持在共享模塊中聲明那些可能被特性模塊引用的可複用組件、指令和管道。 
  • 考慮把可能在整個應用中處處引用的模塊命名爲 SharedModule。 
  • 考慮 不要在共享模塊中提供服務。服務一般是單例的,應該在整個應用或一個特定的特性模塊中只有一份。 不過也有例外,好比,在下面的範例代碼中,注意 SharedModule 提供了 FilterTextService。這裏能夠這麼作,由於該服務是無狀態的,也就是說,該服務的消費者不會受到這些新實例的影響。 
  • 堅持在 SharedModule 中導入全部模塊都須要的資產(例如 CommonModule 和 FormsModule)。 

由於: 

1)        SharedModule 中包含的組件、指令和管道可能須要來自其它公共模塊的特性(例如來自 CommonModule 中的 ngFor 指令)。 

 

  • 堅持在 SharedModule 中聲明全部組件、指令和管道。 
  • 堅持從 SharedModule 中導出其它特性模塊所需的所有符號。 

由於: 

1)        SharedModule 的存在,能讓經常使用的組件、指令和管道在不少其它模塊的組件模板中都自動可用。 

 

  • 避免在 SharedModule 中指定應用級的單例服務提供商。若是是刻意要獲得多個服務單例也行,不過仍是要當心。 

由於: 

1)        惰性加載的特性模塊若是導入了這個共享模塊,會建立一份本身的服務副本,這可能會致使意料以外的後果。 

2)        對於單例服務,你不但願每一個模塊都有本身的實例。 而若是 SharedModule 提供了一個服務,那就有可能發生這種狀況。 

 

5.7.核心特性模塊 

  • 考慮把那些數量龐大、輔助性的、只用一次的類收集到核心模塊中,讓特性模塊的結構更清晰簡明。 
  • 堅持把那些「只用一次」的類收集到 CoreModule 中,並對外隱藏它們的實現細節。簡化的 AppModule 會導入 CoreModule,而且把它做爲整個應用的總指揮。 
  • 堅持在 core 目錄下建立一個名叫 CoreModule 的特性模塊(例如在 app/core/core.module.ts 中定義 CoreModule)。 
  • 堅持把要共享給整個應用的單例服務放進 CoreModule 中(例如 ExceptionService 和 LoggerService)。 
  • 堅持導入 CoreModule 中的資產所須要的所有模塊(例如 CommonModule 和 FormsModule)。 

由於: 

1)        CoreModule 提供了一個或多個單例服務。Angular 使用應用的根注入器註冊這些服務提供商,讓每一個服務的這個單例對象對全部須要它們的組件都是可用的,而不用管該組件是經過主動加載仍是惰性加載的方式加載的。 

2)        CoreModule 將包含一些單例服務。而若是是由惰性加載模塊來導入這些服務,它就會獲得一個新實例,而不是所指望的全應用級單例。 

 

  • 堅持把應用級、只用一次的組件收集到 CoreModule 中。 只在應用啓動時從 AppModule 中導入它一次,之後不再要導入它(例如 NavComponent 和 SpinnerComponent)。 

由於: 

1)        真實世界中的應用會有不少只用一次的組件(例如加載動畫、消息浮層、模態框等),它們只會在 AppComponent 的模板中出現。 不會在其它地方導入它們,因此沒有共享的價值。 然而它們又太大了,放在根目錄中就會顯得亂七八糟的。 

 

  • 避免在 AppModule 以外的任何地方導入 CoreModule。 

由於: 

1)        若是惰性加載的特性模塊直接導入 CoreModule,就會建立它本身的服務副本,並致使意料以外的後果。 

2)        主動加載的特性模塊已經準備好了訪問 AppModule 的注入器,所以也能取得 CoreModule 中的服務。 

 

  • 堅持從 CoreModule 中導出 AppModule 需導入的全部符號,使它們在全部特性模塊中可用。 

由於: 

1)        CoreModule 的存在就讓經常使用的單例服務在全部其它模塊中可用。 

2)        你但願整個應用都使用這個單例服務。 你不但願每一個模塊都有這個單例服務的單獨的實例。 然而,若是 CoreModule 中提供了一個服務,就可能偶爾致使這種後果。 

 

AppModule 變得更小了,由於不少應用根部的類都被移到了其它模塊中。 AppModule 變得穩定了,由於你將會往其它模塊中添加特性組件和服務提供者,而不是這個 AppModule。 AppModule 把工做委託給了導入的模塊,而不是親力親爲。 AppModule 聚焦在它本身的主要任務上:做爲整個應用的總指揮。 

5.8.防止屢次導入 CoreModule 

  • 應該只有 AppModule 才容許導入 CoreModule。 
  • 堅持防範屢次導入 CoreModule,並經過添加Guards邏輯來儘快失敗。 

由於: 

1)        Guards能夠阻止對 CoreModule 的屢次導入。 

2)        Guards會禁止建立單例服務的多個實例。 

app/core/module-import-guard.ts:

export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) {
  if (parentModule) {
    throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`);
  }
}

 

app/core/core.module.ts:

import { NgModule, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';
 
import { LoggerService } from './logger.service';
import { NavComponent } from './nav/nav.component';
import { throwIfAlreadyLoaded } from './module-import-guard';
 
@NgModule({
  imports: [
    CommonModule // we use ngFor
  ],
  exports: [NavComponent],
  declarations: [NavComponent],
  providers: [LoggerService]
})
 
export class CoreModule {
  constructor( @Optional() @SkipSelf() parentModule: CoreModule) {
    throwIfAlreadyLoaded(parentModule, 'CoreModule');
  }
}

 

5.9.惰性加載的目錄 

  • 某些邊界清晰的應用特性或工做流能夠作成惰性加載或按需加載的,而不用老是隨着應用啓動。 
  • 堅持把惰性加載特性下的內容放進惰性加載目錄中。 典型的惰性加載目錄包含路由組件及其子組件以及與它們有關的那些資產和模塊。 

由於: 

1)        這種目錄讓標識和隔離這些特性內容變得更輕鬆。 

5.10.       不要直接導入惰性加載的目錄 

  • 避免讓兄弟模塊和父模塊直接導入惰性加載特性中的模塊。 

由於: 

1)        直接導入並使用此模塊會當即加載它,而本來的設計意圖是按需加載它。 

6、      Components 

6.1.把組件當作元素 

  • 考慮給組件一個元素選擇器,而不是屬性或類選擇器。 

由於: 

1)        組件有不少包含 HTML 以及可選 Angular 模板語法的模板。 它們顯示內容。開發人員會把組件像原生 HTML 元素和 WebComponents 同樣放進頁面中。 

2)        查看組件模板的 HTML 時,更容易識別一個符號是組件仍是指令。 

少數狀況下,你要爲組件使用屬性選擇器,好比你要增強某個內置元素時。 好比,Material Design 組件庫就會對 <button mat-button> 使用這項技術。不過,你不該該在自定義組件上使用這項技術。 

  • 反例: 

app/heroes/hero-button/hero-button.component.ts: 

@Component({
  selector: '[tohHeroButton]',
  templateUrl: './hero-button.component.html'
})
export class HeroButtonComponent {}

 

app/app.component.html: 

<div tohHeroButton></div>

 

  • 正例: 

app/heroes/hero-button/hero-button.component.ts: 

@Component({
  selector: 'toh-hero-button',
  templateUrl: './hero-button.component.html'
})
export class HeroButtonComponent {}

 

app/app.component.html: 

<toh-hero-button></toh-hero-button>

 

6.2.把模板和樣式從組件中分離 

  • 堅持當超過 3 行時,把模板和樣式提取到一個單獨的文件。 
  • 堅持把模板文件命名爲 [component-name].component.html,其中,[component-name] 是組件名。 
  • 堅持把樣式文件命名爲 [component-name].component.css,其中,[component-name] 是組件名。 
  • 堅持指定相對於模塊的 URL ,給它加上 ./ 前綴。 

由於: 

1)        巨大的、內聯的模板和樣式表會遮蓋組件的意圖和實現方式,削弱可讀性和可維護性。 

2)        在多數編輯器中,編寫內聯的模板和樣式表時都沒法使用語法提示和代碼片斷功能。 Angular 的 TypeScript 語言服務(即將到來)能夠幫助那些編輯器在編寫 HTML 模板時克服這一缺陷,但對 CSS 樣式沒有幫助。 

3)        當你移動組件文件時,相對於組件的 URL 不須要修改,由於這些文件始終會在一塊兒。 

4)        「./ 」 前綴是相對 URL 的標準語法,沒必要依賴 Angular 的特殊處理,若是沒有前綴則不行。 

 

6.3.內聯輸入和輸出屬性裝飾器 

  • 堅持 使用 @Input() 和 @Output(),而非 @Directive 和 @Component 裝飾器的 inputs 和 outputs 屬性。 
  • 堅持把 @Input() 或者 @Output() 放到所裝飾的屬性的同一行。 

由於: 

1)        易於在類裏面識別哪些屬性是輸入屬性或輸出屬性。 

2)        若是須要重命名與 @Input 或者 @Output 關聯的屬性或事件名,你能夠在一個位置修改。 

3)        依附到指令的元數據聲明會比較簡短,更易於閱讀。 

4)        把裝飾器放到同一行能夠精簡代碼,同時更易於識別輸入或輸出屬性。 

  • 反例: 

app/heroes/shared/hero-button/hero-button.component.ts: 

@Component({
  selector: 'toh-hero-button',
  template: `<button></button>`,
  inputs: [
    'label'
  ],
  outputs: [
    'change'
  ]
})
export class HeroButtonComponent {
  change = new EventEmitter<any>();
  label: string;
}

 

  • 正例: 

app/heroes/shared/hero-button/hero-button.component.ts: 

@Component({
  selector: 'toh-hero-button',
  template: `<button>{{label}}</button>`
})
export class HeroButtonComponent {
  @Output() change = new EventEmitter<any>();
  @Input() label: string;
}

 

6.4.避免爲輸入和輸出屬性指定別名 

  • 避免除非有重要目的,不然不要爲輸入和輸出指定別名。 

由於: 

1)        同一個屬性有兩個名字(一個對內一個對外)很容易致使混淆。 

2)        若是指令名也同時用做輸入屬性,並且指令名沒法準確描述這個屬性的用途時,應該使用別名。 

 

  • 反例: 

app/heroes/shared/hero-button/hero-button.component.ts: 

@Component({
  selector: 'toh-hero-button',
  template: `<button>{{label}}</button>`
})
export class HeroButtonComponent {
  // Pointless aliases
  @Output('changeEvent') change = new EventEmitter<any>();
  @Input('labelAttribute') label: string;
}

 

app/app.component.html: 

<toh-hero-button labelAttribute="OK" (changeEvent)="doSomething()">
</toh-hero-button>

 

  • 正例: 

app/heroes/shared/hero-button/hero-button.component.ts: 

@Component({
  selector: 'toh-hero-button',
  template: `<button>{{label}}</button>`
})
export class HeroButtonComponent {
  // No aliases
  @Output() change = new EventEmitter<any>();
  @Input() label: string;
}

 

app/heroes/shared/hero-button/hero-highlight.directive.ts

import { Directive, ElementRef, Input, OnChanges } from '@angular/core';
  
@Directive({ selector: '[heroHighlight]' })
export class HeroHighlightDirective implements OnChanges {
  
  // Aliased because `color` is a better property name than `heroHighlight`
  @Input('heroHighlight') color: string;
  
  constructor(private el: ElementRef) {}
  
  ngOnChanges() {
    this.el.nativeElement.style.backgroundColor = this.color || 'yellow';
  }
}

 

app/app.component.html: 

<toh-hero-button label="OK" (change)="doSomething()">
</toh-hero-button>
 
<!-- `heroHighlight` is both the directive name and the data-bound aliased property name -->
<h3 heroHighlight="skyblue">The Great Bombasto</h3>

 

6.5.成員順序 

  • 堅持把屬性成員放在前面,方法成員放在後面。 
  • 堅持先放公共成員,再放私有成員,並按照字母順序排列。 

由於: 

1)        把類的成員按照統一的順序排列,易於閱讀,能當即識別出組件的哪一個成員服務於何種目的。 

  • 反例: 

app/shared/toast/toast.component.ts: 

export class ToastComponent implements OnInit {
  
  private defaults = {
    title: '',
    message: 'May the Force be with you'
  };
  message: string;
  title: string;
  private toastElement: any;
  
  ngOnInit() {
    this.toastElement = document.getElementById('toh-toast');
  }
  
  // private methods
  private hide() {
    this.toastElement.style.opacity = 0;
    window.setTimeout(() => this.toastElement.style.zIndex = 0, 400);
  }
  
  activate(message = this.defaults.message, title = this.defaults.title) {
    this.title = title;
    this.message = message;
    this.show();
  }
  
  private show() {
    console.log(this.message);
    this.toastElement.style.opacity = 1;
    this.toastElement.style.zIndex = 9999;
  
    window.setTimeout(() => this.hide(), 2500);
  }
}

 

  • 正例: 

app/shared/toast/toast.component.ts: 

export class ToastComponent implements OnInit {
  // public properties
  message: string;
  title: string;
  
  // private fields
  private defaults = {
    title: '',
    message: 'May the Force be with you'
  };
  private toastElement: any;
  
  // public methods
  activate(message = this.defaults.message, title = this.defaults.title) {
    this.title = title;
    this.message = message;
    this.show();
  }
  
  ngOnInit() {
    this.toastElement = document.getElementById('toh-toast');
  }
  
  // private methods
  private hide() {
    this.toastElement.style.opacity = 0;
    window.setTimeout(() => this.toastElement.style.zIndex = 0, 400);
  }
  
  private show() {
    console.log(this.message);
    this.toastElement.style.opacity = 1;
    this.toastElement.style.zIndex = 9999;
    window.setTimeout(() => this.hide(), 2500);
  }
}

 

6.6.把邏輯放到服務裏 

  • 堅持在組件中只包含與視圖相關的邏輯。全部其它邏輯都應該放到服務中。 
  • 堅持把可重用的邏輯放到服務中,保持組件簡單,聚焦於它們預期目的。 

由於: 

1)        當邏輯被放置到服務裏,並以函數的形式暴露時,能夠被多個組件重複使用。 

2)        在單元測試時,服務裏的邏輯更容易被隔離。當組件中調用邏輯時,也很容易被模擬。 

3)        從組件移除依賴並隱藏實施細節。 

4)        保持組件苗條、精簡和聚焦。 

6.7.不要給輸出屬性加前綴 

  • 堅持命名事件時,不要帶前綴 on。 
  • 堅持把事件處理器方法命名爲 on 前綴以後緊跟着事件名。 

由於: 

1)        與內置事件命名一致,例如按鈕點擊。 

2)        Angular 容許另外一種備選語法 on-*。若是事件的名字自己帶有前綴 on,那麼綁定的表達式多是 on-onEvent。 

  • 反例: 

app/heroes/hero.component.ts: 

@Component({
  selector: 'toh-hero',
  template: `...`
})
export class HeroComponent {
  @Output() onSavedTheDay = new EventEmitter<boolean>();
}

 

app/app.component.html: 

<toh-hero (onSavedTheDay)="onSavedTheDay($event)"></toh-hero>

 

  • 正例: 

app/heroes/hero.component.ts: 

export class HeroComponent {
  @Output() savedTheDay = new EventEmitter<boolean>();
}

 

app/app.component.html: 

<toh-hero (savedTheDay)="onSavedTheDay($event)"></toh-hero>

 

6.8.把表現層邏輯放到組件類裏 

  • 堅持把表現層邏輯放進組件類中,而不要放在模板裏。 

由於: 

1)        邏輯應該只出如今一個地方(組件類裏)而不該分散在兩個地方。 

2)        將組件的表現層邏輯放到組件類而非模板裏,能夠加強測試性、維護性和重複使用性。 

  • 反例: 

app/heroes/hero-list/hero-list.component.ts: 

@Component({
  selector: 'toh-hero-list',
  template: `
    <section>
      Our list of heroes:
      <hero-profile *ngFor="let hero of heroes" [hero]="hero">
      </hero-profile>
      Total powers: {{totalPowers}}<br>
      Average power: {{totalPowers / heroes.length}}
    </section>
  `
})
export class HeroListComponent {
  heroes: Hero[];
  totalPowers: number;
}

 

  • 正例: 

app/heroes/hero-list/hero-list.component.ts: 

@Component({
  selector: 'toh-hero-list',
  template: `
    <section>
      Our list of heroes:
      <toh-hero *ngFor="let hero of heroes" [hero]="hero">
      </toh-hero>
      Total powers: {{totalPowers}}<br>
      Average power: {{avgPower}}
    </section>
  `
})
export class HeroListComponent {
  heroes: Hero[];
  totalPowers: number;
  
  get avgPower() {
    return this.totalPowers / this.heroes.length;
  }
}

 

7、      指令 

7.1.使用指令來加強已有元素 

  • 堅持當你須要有表現層邏輯,但沒有模板時,使用屬性型指令。 

由於: 

1)        屬性型指令沒有模板。 

2)        一個元素可使用多個屬性型指令。 

  • 範例: 

app/shared/highlight.directive.ts: 

@Directive({
  selector: '[tohHighlight]'
})
export class HighlightDirective {
  @HostListener('mouseover') onMouseEnter() {
    // do highlight work
  }
}

 

app/app.component.html: 

<div tohHighlight>Bombasta</div>

 

7.2.HostListener、HostBinding 裝飾器 和 組件元數據 host 

  • 考慮優先使用 @HostListener 和 @HostBinding,而不是 @Directive 和 @Component 裝飾器的 host 屬性。 
  • 堅持讓你的選擇保持一致。 

由於: 

1)        對於關聯到 @HostBinding 的屬性或關聯到 @HostListener 的方法,要修改時,只需在指令類中的一個地方修改。 若是使用元數據屬性 host,你就得在組件類中修改屬性聲明的同時修改相關的元數據。 

  • 範例: 

app/shared/validator.directive.ts: 

import { Directive, HostBinding, HostListener } from '@angular/core';
 
@Directive({
  selector: '[tohValidator]'
})
export class ValidatorDirective {
  @HostBinding('attr.role') role = 'button';
  @HostListener('mouseenter') onMouseEnter() {
    // do work
  }
}

 

  • 不推薦的範例: 
import { Directive } from '@angular/core';
  
@Directive({
  selector: '[tohValidator2]',
  host: {
    '[attr.role]': 'role',
    '(mouseenter)': 'onMouseEnter()'
  }
})
export class Validator2Directive {
  role = 'button';
  onMouseEnter() {
    // do work
  }
}

 

8、      服務 

8.1.服務老是單例的 

  • 堅持在同一個注入器內,把服務當作單例使用。用它們來共享數據和功能。 

由於: 

1)        服務是在特性範圍或應用內共享方法的理想載體。 

2)        服務是共享狀態性內存數據的理想載體。 

  • 範例: 

app/heroes/shared/hero.service.ts: 

export class HeroService {
  constructor(private http: Http) { }
 
  getHeroes() {
    return this.http.get('api/heroes').pipe(
      map((response: Response) => <Hero[]>response.json()));
  }
}

 

8.2.單一職責 

  • 堅持建立單一職責的服務,用職責封裝在它的上下文中。 
  • 堅持當服務成長到超出單一用途時,建立一個新服務。 

由於: 

1)        當服務有多個職責時,它很難被測試。 

2)        當某個服務有多個職責時,每一個注入它的組件或服務都會承擔這些職責的所有開銷。 

8.3.提供一個服務 

  • 堅持在服務的 @Injectable 裝飾器上指定經過應用的根注入器提供服務。 

由於: 

1)        Angular 注入器是層次化的。 

2)        當你在根注入器上提供該服務時,該服務實例在每一個須要該服務的類中是共享的。當服務要共享方法或狀態時,這是最理想的選擇。 

3)        當你在服務的 @Injectable 中註冊服務時,Angular CLI 生產環境構建時使用的優化工具能夠進行搖樹優化,從而移除那些你的應用中從未用過的服務。 

4)        當不一樣的兩個組件須要一個服務的不一樣的實例時,上面的方法這就不理想了。在這種狀況下,對於須要嶄新和單獨服務實例的組件,最好在組件級提供服務。 

  • 範例: 

src/app/treeshaking/service.ts: 

@Injectable({
  providedIn: 'root',
})
export class Service {
}

 

8.4.使用 @Injectable() 類裝飾器 

  • 堅持當使用類型做爲令牌來注入服務的依賴時,使用 @Injectable() 類裝飾器,而非 @Inject() 參數裝飾器。 

由於: 

1)        Angular 的 DI 機制會根據服務的構造函數參數的聲明類型來解析服務的全部依賴。 

2)        當服務只接受類型令牌相關的依賴時,比起在每一個構造函數參數上使用 @Inject(),@Injectable() 的語法簡潔多了。 

  • 反例: 

app/heroes/shared/hero-arena.service.ts: 

export class HeroArena {
  constructor(
      @Inject(HeroService) private heroService: HeroService,
      @Inject(Http) private http: Http) {}
}

 

  • 正例: 

app/heroes/shared/hero-arena.service.ts: 

@Injectable()
export class HeroArena {
  constructor(
    private heroService: HeroService,
    private http: Http) {}
}

 

9、      數據服務 

9.1.經過服務與 Web 服務器通信 

  • 堅持把數據操做和與數據交互的邏輯重構到服務裏。 
  • 堅持讓數據服務來負責 XHR 調用、本地儲存、內存儲存或者其它數據操做。 

由於: 

1)        組件的職責是爲視圖展現或收集信息。它不該該關心如何獲取數據,它只須要知道向誰請求數據。把如何獲取數據的邏輯移動到數據服務裏,簡化了組件,讓其聚焦於視圖。 

2)        在測試使用數據服務的組件時,可讓數據調用更容易被測試(模擬或者真實)。 

3)        數據管理的詳情,好比頭信息、方法、緩存、錯誤處理和重試邏輯,不是組件和其它的數據消費者應該關心的事情。 

4)        數據服務應該封裝這些細節。這樣,在服務內部修改細節,就不會影響到它的消費者。而且更容易經過實現一個模擬服務來對消費者進行測試。 

10、      生命週期鉤子 

10.1.       實現生命週期鉤子接口 

  • 堅持實現生命週期鉤子接口。 

由於: 

1)        若是使用強類型的方法簽名,編譯器和編輯器能夠幫你揪出拼寫錯誤。 

  • 反例: 

app/heroes/shared/hero-button/hero-button.component.ts: 

@Component({
  selector: 'toh-hero-button',
  template: `<button>OK<button>`
})
export class HeroButtonComponent {
  onInit() { // misspelled
    console.log('The component is initialized');
  }
}

 

  • 正例: 

app/heroes/shared/hero-button/hero-button.component.ts: 

@Component({
  selector: 'toh-hero-button',
  template: `<button>OK</button>`
})
export class HeroButtonComponent implements OnInit {
  ngOnInit() {
    console.log('The component is initialized');
  }
}
相關文章
相關標籤/搜索