目錄
一、 前言
1.1. 規范目的
1.2. 局限性
二、 文件規范
2.1. 文件結構約定
2.2. 單一職責原則
2.2.1 單一規則
2.2.2 小函數
三、 命名規范
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.1. 類
4.2. 常量
4.3. 接口
4.4. 屬性和方法
4.5. 導入語句中的空行
4.6. 注釋
4.7. 優化作用域鏈
4.8. 合並http請求
4.9. 業務分離
五、 應用程序結構與 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. 不要直接導入惰性加載的目錄
六、 Components
6.1. 把組件當做元素
6.2. 把模板和樣式從組件中分離
6.3. 內聯輸入和輸出屬性裝飾器
6.4. 避免為輸入和輸出屬性指定別名
6.5. 成員順序
6.6. 把邏輯放到服務里
6.7. 不要給輸出屬性加前綴
6.8. 把表現層邏輯放到組件類里
七、 指令
7.1. 使用指令來增強已有元素
7.2. HostListener、HostBinding 裝飾器 和 組件元數據 host
八、 服務
8.1. 服務總是單例的
8.2. 單一職責
8.3. 提供一個服務
8.4. 使用 @Injectable() 類裝飾器
九、 數據服務
9.1. 通過服務與 Web 服務器通訊
十、 生命周期鈎子
10.1. 實現生命周期鈎子接口
一、 前言
1.1.規范目的
為提高團隊協作效率,提高代碼的可讀性、可復用性和可移植性,便於前端輸出高質量的代碼以及后期優化維護,特制訂此文檔。
1.2.局限性
本文着重結合了Angular(CLI 7.2.2)這一前端框架(版本)進行規范,雖然部分規范對於原生JavaScript開發或其他前端框架依然適用,但也有部分規范並不通用,因此本文主要面向基於 Angular (CLI 7.2.2)框架進行開發的Web前端工程師。
二、 文件規范
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.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.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.業務分離
在構建組件的過程中,要將業務邏輯從組件的渲染中分離出來,組件只管對指定格式的數據進行渲染,程序根據業務需要,創建不同的服務來完成組件需要的數據的獲取和組裝等處理,最終將處理好的數據傳遞給組件去渲染,以提高組件的可復用性能和可移植性能。
五、 應用程序結構與 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) 直接導入並使用此模塊會立即加載它,而原本的設計意圖是按需加載它。
六、 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.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.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.1.通過服務與 Web 服務器通訊
- 堅持把數據操作和與數據交互的邏輯重構到服務里。
- 堅持讓數據服務來負責 XHR 調用、本地儲存、內存儲存或者其它數據操作。
因為:
1) 組件的職責是為視圖展示或收集信息。它不應該關心如何獲取數據,它只需要知道向誰請求數據。把如何獲取數據的邏輯移動到數據服務里,簡化了組件,讓其聚焦於視圖。
2) 在測試使用數據服務的組件時,可以讓數據調用更容易被測試(模擬或者真實)。
3) 數據管理的詳情,比如頭信息、方法、緩存、錯誤處理和重試邏輯,不是組件和其它的數據消費者應該關心的事情。
4) 數據服務應該封裝這些細節。這樣,在服務內部修改細節,就不會影響到它的消費者。並且更容易通過實現一個模擬服務來對消費者進行測試。
十、 生命周期鈎子
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'); } }