风格指南

如果你正在寻找关于 Angular 语法、约定和应用组织结构的官方指南,那你就来对了。 本风格指南介绍了提倡的约定,更重要的是,解释了为什么。

目录

Contents

风格词汇

每个指导原则都会描述好的或者坏的做法,所有指导原则风格一致。

指导原则中使用的词汇表明推荐的程度。

坚持意味着总是应该遵循的约定。 总是可能有点太强了。应该总是遵循的指导原则非常少。 但是,只有遇到非常不寻常的情况才能打破坚持的原则。

考虑标志着通常应该遵循的指导原则。

如果能完全理解指导原则背后的含义,并且很好的理由背离它,那就可以那么做。但是请保持一致。

避免标志着我们决不应该做的事。需要避免的代码范例会有明显的红色标题。

为何?会给出随后的建议的理由。

文件结构约定

在一些代码例子中,有的文件有一个或多个相似名字的伴随文件。(例如 hero.component.ts 和 hero.component.html)。

本指南将会使用像hero.component.ts|html|css|spec的简写来表示上面描述的多个文件,目的是保持本指南的简洁性,增加描述文件结构时的可读性。

单一职责

对所有的组件、服务等等应用单一职责原则 (SRP)

单一法则

风格 01-01

坚持每个文件只定义一样东西(例如服务或组件)。

考虑把文件大小限制在 400 行代码以内。

为何?单组件文件非常容易阅读、维护,并能防止在版本控制系统里与团队冲突。

为何?单组件文件可以防止一些隐蔽的程序缺陷,当把多个组件合写在同一个文件中时,可能造成共享变量、创建意外的闭包,或者与依赖之间产生意外耦合等情况。

为何?单独的组件通常是该文件默认的导出,可以用路由器实现按需加载。

最关键的是,可以增强代码可重用性和阅读性,减少出错的可能性。

下面的负面例子定义了AppComponent,它来引导应用程序,定义了Hero模型对象,并从服务器加载了英雄 ... 所有都在同一个文件。 不要这么做

app/heroes/hero.component.ts

  1. /* avoid */
  2. import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
  3. import { BrowserModule } from '@angular/platform-browser';
  4. import { NgModule, Component, OnInit } from '@angular/core';
  5. class Hero {
  6. id: number;
  7. name: string;
  8. }
  9. @Component({
  10. selector: 'my-app',
  11. template: `
  12. <h1>{{title}}</h1>
  13. <pre>{{heroes | json}}</pre>
  14. `,
  15. styleUrls: ['app/app.component.css']
  16. })
  17. class AppComponent implements OnInit {
  18. title = 'Tour of Heroes';
  19. heroes: Hero[] = [];
  20. ngOnInit() {
  21. getHeroes().then(heroes => this.heroes = heroes);
  22. }
  23. }
  24. @NgModule({
  25. imports: [ BrowserModule ],
  26. declarations: [ AppComponent ],
  27. exports: [ AppComponent ],
  28. bootstrap: [ AppComponent ]
  29. })
  30. export class AppModule { }
  31. platformBrowserDynamic().bootstrapModule(AppModule);
  32. const HEROES: Hero[] = [
  33. {id: 1, name: 'Bombasto'},
  34. {id: 2, name: 'Tornado'},
  35. {id: 3, name: 'Magneta'},
  36. ];
  37. function getHeroes(): Promise<Hero[]> {
  38. return Promise.resolve(HEROES); // TODO: get hero data from the server;
  39. }

最好将组件及其支撑部件重新分配到独立的文件。

  1. import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
  2. import { AppModule } from './app/app.module';
  3. platformBrowserDynamic().bootstrapModule(AppModule);

随着应用程序的成长,本法则会变得越来越重要。

回到顶部

简单函数

风格 01-02

坚持定义简单函数

考虑限制在 75 行之内。

为何?简单函数更易于测试,特别是当它们只做一件事,只为一个目的服务时。

为何?简单函数促进代码重用。

为何?简单函数更易于阅读。

为何?简单函数更易于维护。

为何?简单函数可避免易在大函数中产生的隐蔽性错误,例如与外界共享变量、创建意外的闭包或与依赖之间产生意外耦合等。

回到顶部

命名

命名约定对可维护性和可读性非常重要。本指南为文件名和符号名推荐了一套命名约定。

总体命名指导原则

风格 02-01

坚持所有符号使用一致的命名规则。

坚持遵循同一个模式来描述符号的特性和类型。推荐的模式为feature.type.ts

为何?命名约定提供了一致的方式来查找内容,让我们一眼就能锁定。 项目的一致性是至关重要的。团队内的一致性也很重要。整个公司的一致性会提供惊人的效率。

为何?命名约定帮助我们更快得找到不在手头的代码,更容易理解它。

为何?目录名和文件名应该清楚的传递它们的意图。 例如,app/heroes/hero-list.component.ts包含了一个用来管理英雄列表的组件。

回到顶部

使用点和横杠来分隔文件名

风格 02-02

坚持 在描述性名字中,用横杠来分隔单词。

坚持使用点来分隔描述性名字和类型。

坚持遵循先描述组件特性,再描述它的类型的模式,对所有组件使用一致的类型命名规则。推荐的模式为feature.type.ts

坚持使用惯用的后缀来描述类型,包括*.service*.component*.pipe.module.directive。 必要时可以创建更多类型名,但必须注意,不要创建太多。

为何?类型名字提供一致的方式来快速的识别文件中有什么。

为何? 利用编辑器或者 IDE 的模糊搜索功能,可以很容易地找到特定文件。

为何?.service这样的没有简写过的类型名字,描述清楚,毫不含糊。 像.srv, .svc, 和 .serv这样的简写可能令人困惑。

为何?为自动化任务提供模式匹配。

回到顶部

符号名与文件名

风格 02-03

坚持为所有东西使用一致的命名约定,以它们所代表的东西命名。

坚持使用大写驼峰命名法来命名类。符号名匹配它所在的文件名。

坚持在符号名后面追加约定的类型后缀(例如ComponentDirectiveModulePipeService)。

坚持在符号名后面追加约定的类型后缀(例如.component.ts.directive.ts.module.ts.pipe.ts.service.ts)。

坚持在文件名后面追加约定的类型后缀(例如.component.ts.directive.ts.module.ts.pipe.ts.service.ts)。

为何?遵循一致的约定可以快速识别和引用不同类型的资产。

符号名

文件名

@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

回到顶部

服务名

风格 02-04

坚持使用一致的规则命名服务,以它们的特性来命名。

坚持为服务的类名加上Service后缀。 例如,获取数据或英雄列表的服务应该命名为DataServiceHeroService

有些词汇显然就是服务,比如那些以“-er”后缀结尾的。比如把记日志的服务命名为Logger就比LoggerService更好些。需要在你的项目中决定这种特例是否可以接受。 但无论如何,都要尽量保持一致。

为何?提供一致的方式来快速识别和引用服务。

为何?Logger这样的清楚的服务名不需要后缀。

为何?Credit这样的,服务名是名词,需要一个后缀。当不能明显分辨它是服务还是其它东西时,应该添加后缀。

符号名

文件名

@Injectable()
export class HeroDataService { }

hero-data.service.ts

@Injectable()
export class CreditService { }

credit.service.ts

@Injectable()
export class Logger { }

logger.service.ts

回到顶部

引导

风格 02-05

坚持把应用的引导程序和平台相关的逻辑放到名为main.ts的文件里。

坚持在引导逻辑中包含错误处理代码。

避免把应用逻辑放在main.ts中,而应放在组件或服务里。

为何?应用的启动逻辑遵循一致的约定。

为何?这是从其它技术平台借鉴的常用约定。

main.ts

  1. import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
  2. import { AppModule } from './app/app.module';
  3. platformBrowserDynamic().bootstrapModule(AppModule)
  4. .then(success => console.log(`Bootstrap success`))
  5. .catch(err => console.error(err));
回到顶部

指令选择器

风格 02-06

坚持使用小驼峰命名法来命名指令的选择器。

为何?保持指令中定义的属性名与绑定的视图 HTML 属性名字一致。

为何?Angular HTML 解析器是大小写敏感的,它识别小写驼峰写法。

回到顶部

为组件添加自定义前缀

风格 02-07

坚持使用带连字符的小写元素选择器值(例如admin-users)。

坚持为组件选择器添加自定义前缀。 例如,toh前缀表示 Tour of Heroes(英雄指南),而前缀`admin表示管理特性区。

坚持使用前缀来识别特性区或者应用程序本身。

为何?防止与其它应用中的组件和原生 HTML 元素发生命名冲突。

为何?更容易在其它应用中推广和共享组件。

为何?组件在 DOM 中更容易被区分出来。

app/heroes/hero.component.ts

  1. /* avoid */
  2. // HeroComponent is in the Tour of Heroes feature
  3. @Component({
  4. selector: 'hero'
  5. })
  6. export class HeroComponent {}

app/users/users.component.ts

  1. /* avoid */
  2. // UsersComponent is in an Admin feature
  3. @Component({
  4. selector: 'users'
  5. })
  6. export class UsersComponent {}

app/heroes/hero.component.ts

  1. @Component({
  2. selector: 'toh-hero'
  3. })
  4. export class HeroComponent {}

app/users/users.component.ts

  1. @Component({
  2. selector: 'admin-users'
  3. })
  4. export class UsersComponent {}

为指令添加自定义前缀

风格 02-08

坚持为指令的选择器添加自定义前缀(例如前缀toh来自Tour of Heroes)。

坚持用小驼峰形式拼写非元素选择器,除非该选择器用于匹配原生 HTML 属性。

为何?防止名字冲突。

为何?指令更加容易被识别。

app/shared/validate.directive.ts

  1. /* avoid */
  2. @Directive({
  3. selector: '[validate]'
  4. })
  5. export class ValidateDirective {}

app/shared/validate.directive.ts

  1. @Directive({
  2. selector: '[tohValidate]'
  3. })
  4. export class ValidateDirective {}
回到顶部

管道名

风格 02-09

坚持为所有管道使用一致的命名约定,用它们的特性来命名。

为何?提供一致方式快速识别和引用管道。

符号名

文件名

@Pipe({ name: 'ellipsis' })
export class EllipsisPipe implements PipeTransform { }

ellipsis.pipe.ts

@Pipe({ name: 'initCaps' })
export class InitCapsPipe implements PipeTransform { }

init-caps.pipe.ts

回到顶部

单元测试文件名

风格 02-10

坚持测试规格文件名与被测试组件文件名相同。

坚持测试规格文件名添加.spec后缀。

为何?提供一致的方式来快速识别测试。

为何?提供一个与 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

回到顶部

端到端测试文件名

风格 02-11

坚持端到端测试规格文件和它们所测试的特性同名,添加.e2e-spec后缀。

为何?提供一致的方式快速识别端到端测试文件。

为何?提供一个与测试运行器和构建自动化匹配的模式。

符号名

文件名

端到端测试

app.e2e-spec.ts

heroes.e2e-spec.ts

回到顶部

Angular NgModule 命名

风格 02-12

坚持为符号名添加Module后缀

坚持为文件名添加.module.ts扩展名。

坚持用特性名和所在目录命名模块。

为何?提供一致的方式来快速标识和引用模块。

为何?大驼峰命名法是一种命名约定,用来标识可用构造函数实例化的对象。

为何?很容易就能看出这个模块是同名特性的根模块。

坚持RoutingModule 类名添加RoutingModule后缀。

坚持RoutingModule 的文件名添加-routing.module.ts后缀。

为何?RoutingModule是一种专门用来配置 Angular 路由器的模块。 “类名和文件名保持一致”的约定使这些模块易于发现和验证。

Symbol NameFile Name
@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

回到顶部

编程约定

坚持一致的编程、命名和空格的约定。

风格 03-01

坚持使用大写驼峰命名法来命名类。

为何?遵循类命名传统约定。

为何?类可以被实例化和构造实例。根据约定,用大写驼峰命名法来标识可构造的东西。

app/shared/exception.service.ts

  1. /* avoid */
  2. export class exceptionService {
  3. constructor() { }
  4. }

app/shared/exception.service.ts

  1. export class ExceptionService {
  2. constructor() { }
  3. }
回到顶部

常量

风格 03-02

坚持const声明变量,除非它们的值在应用的生命周期内会发生变化。

为何?告诉读者这个值是不可变的。

为何? TypeScript 会要求在声明时立即初始化,并阻止再次赋值,以确保达成我们的意图。

考虑 把常量名拼写为小驼峰格式。

为何?小驼峰变量名 (heroRoutes) 比传统的大写蛇形命名法 (HERO_ROUTES) 更容易阅读和理解。

为何? 把常量命名为大写蛇形命名法的传统源于现代 IDE 出现之前, 以便阅读时可以快速发现那些const定义。 TypeScript 本身就能够防止意外赋值。

坚持容许现存的const常量沿用大写蛇形命名法。

为何?传统的大写蛇形命名法仍然很流行、很普遍,特别是在第三方模块中。 修改它们没多大价值,还会有破坏现有代码和文档的风险。

app/shared/data.service.ts

  1. export const mockHeroes = ['Sam', 'Jill']; // prefer
  2. export const heroesUrl = 'api/heroes'; // prefer
  3. export const VILLAINS_URL = 'api/villains'; // tolerate
回到顶部

接口

风格 03-03

坚持使用大写驼峰命名法来命名接口。

考虑不要在接口名字前面加I前缀。

考虑用类代替接口。

为何?TypeScript 指导原则不建议使用 “I” 前缀。

为何?单独一个类的代码量小于类+接口

为何?类可以作为接口使用(只是用implements代替extends而已)。

为何?在 Angular 依赖注入系统中,接口类可以作为服务提供商的查找令牌。

app/shared/hero-collector.service.ts

  1. /* avoid */
  2. import { Injectable } from '@angular/core';
  3. import { IHero } from './hero.model.avoid';
  4. @Injectable()
  5. export class HeroCollectorService {
  6. hero: IHero;
  7. constructor() { }
  8. }

app/shared/hero-collector.service.ts

  1. import { Injectable } from '@angular/core';
  2. import { Hero } from './hero.model';
  3. @Injectable()
  4. export class HeroCollectorService {
  5. hero: Hero;
  6. constructor() { }
  7. }
回到顶部

属性和方法

样式 03-04

坚持使用小写驼峰命名法来命名属性和方法。

避免为私有属性和方法添加下划线前缀。

为何?遵循传统属性和方法的命名约定。

为何? JavaScript 不支持真正的私有属性和方法。

为何? TypeScript 工具让识别私有或公有属性和方法变得很简单。

app/shared/toast.service.ts

  1. /* avoid */
  2. import { Injectable } from '@angular/core';
  3. @Injectable()
  4. export class ToastService {
  5. message: string;
  6. private _toastCount: number;
  7. hide() {
  8. this._toastCount--;
  9. this._log();
  10. }
  11. show() {
  12. this._toastCount++;
  13. this._log();
  14. }
  15. private _log() {
  16. console.log(this.message);
  17. }
  18. }

app/shared/toast.service.ts

  1. import { Injectable } from '@angular/core';
  2. @Injectable()
  3. export class ToastService {
  4. message: string;
  5. private toastCount: number;
  6. hide() {
  7. this.toastCount--;
  8. this.log();
  9. }
  10. show() {
  11. this.toastCount++;
  12. this.log();
  13. }
  14. private log() {
  15. console.log(this.message);
  16. }
  17. }
回到顶部

导入语句中的空行

风格 03-06

坚持在第三方导入和应用导入之间留一个空行。

考虑按模块名字的字母顺排列导入行。

考虑在解构表达式中按字母顺序排列导入的东西。

为何?空行可以让阅读和定位本地导入更加容易。

为何?按字母顺序排列可以让阅读和定位本地导入更加容易。

app/heroes/shared/hero.service.ts

  1. /* avoid */
  2. import { ExceptionService, SpinnerService, ToastService } from '../../core';
  3. import { Http } from '@angular/http';
  4. import { Injectable } from '@angular/core';
  5. import { Hero } from './hero.model';

app/heroes/shared/hero.service.ts

  1. import { Injectable } from '@angular/core';
  2. import { Http } from '@angular/http';
  3. import { Hero } from './hero.model';
  4. import { ExceptionService, SpinnerService, ToastService } from '../../core';
回到顶部

应用程序结构与 Angular 模块

准备一个近期实施方案和一个长期的愿景。从零开始,但要考虑应用程序接下来的路往哪儿走。

所有应用程序的源代码都放到名叫src的目录里。 所有特性区都在自己的文件夹中,带有它们自己的 Angular 模块。

所有内容都遵循每个文件一个特性的原则。每个组件、服务和管道都在自己的文件里。 所有第三方程序包保存到其它目录里,而不是src目录。 你不会修改它们,所以不希望它们弄乱我们的应用程序。 使用本指南介绍的文件命名约定。

回到顶部

LIFT

风格 04-01

坚持组织应用的结构,达到这些目的:快速定位 (Locate) 代码、一眼识别 (Identify) 代码、 尽量保持扁平结构 (Flattest) 和尝试 (Try) 遵循DRY (Do Not Repeat Yourself, 不重复自己) 原则。

坚持四项基本原则定义文件结构,上面的原则是按重要顺序排列的。

为何?LIFT提供了一致的结构,它具有扩展性强、模块化的特性。因为容易快速锁定代码,提高了开发者的效率。 另外,检查应用结构是否合理的方法是问问自己:我们能快速打开与此特性有关的所有文件并开始工作吗?

回到顶部

定位

风格04-02

坚持直观、简单和快速地定位代码。

为何? 要想高效的工作,就必须能迅速找到文件,特别是当不知道(或不记得)文件时。 把相关的文件一起放在一个直观的位置可以节省时间。 富有描述性的目录结构会让你和后面的维护者眼前一亮。

回到顶部

识别

风格 04-03

坚持命名文件到这个程度:看到名字立刻知道它包含了什么,代表了什么。

坚持文件名要具有说明性,确保文件中只包含一个组件。

避免创建包含多个组件、服务或者混合体的文件。

为何?花费更少的时间来查找和琢磨代码,就会变得更有效率。 较长的文件名远胜于较短却容易混淆的缩写名。

当你有一组小型、紧密相关的特性时,违反一物一文件的规则可能会更好, 这种情况下单一文件可能会比多个文件更容易发现和理解。注意这个例外。

回到顶部

扁平

风格 04-04

坚持尽可能保持扁平的目录结构。

考虑当同一目录下达到 7 个或更多个文件时创建子目录。

考虑配置 IDE,以隐藏无关的文件,例如生成出来的.js文件和.js.map文件等。

为何?没人想要在超过七层的目录中查找文件。扁平的结构有利于搜索。

另一方面,心理学家们相信, 当关注的事物超过 9 个时,人类就会开始感到吃力。 所以,当一个文件夹中的文件有 10 个或更多个文件时,可能就是创建子目录的时候了。

还是根据你自己的舒适度而定吧。 除非创建新文件夹能有显著的价值,否则尽量使用扁平结构。

回到顶部

T-DRY (尝试不重复自己)

风格 04-05

坚持 DRY(Don't Repeat Yourself,不重复自己)。

避免过度 DRY,以致牺牲了阅读性。

为何?虽然 DRY 很重要,但如果要以牺牲 LIFT 的其它原则为代价,那就不值得了。 这也就是为什么它被称为 T-DRY。 例如,把组件命名为hero-view.component.html是多余的,因为带有.html扩展名的文件显然就是一个视图 (view)。 但如果它不那么显著,或不符合常规,就把它写出来。

回到顶部

总体结构指导原则

风格 04-06

坚持从零开始,但要考虑应用程序接下来的路往哪儿走。

坚持有一个近期实施方案和一个长期的愿景。

坚持把所有源代码都放到名为src的目录里。

坚持如果组件具有多个伴隨文件 (.ts.html.css.spec),就为它创建一个文件夹。

为何? 在早期阶段能够帮助保持应用的结构小巧且易于维护,这样当应用增长时就容易进化了。

为何? 组件通常有四个文件 (*.html*.css*.ts*.spec.ts),它们很容易把一个目录弄乱。

下面是符合规范的目录和文件结构

<project root>
src
app
core
core.module.ts
exception.service.ts|spec.ts
user-profile.service.ts|spec.ts
heroes
hero
hero.component.ts|html|css|spec.ts
hero-list
hero-list.component.ts|html|css|spec.ts
shared
hero-button.component.ts|html|css|spec.ts
hero.model.ts
hero.service.ts|spec.ts
heroes.component.ts|html|css|spec.ts
heroes.module.ts
heroes-routing.module.ts
shared
shared.module.ts
init-caps.pipe.ts|spec.ts
text-filter.component.ts|spec.ts
text-filter.service.ts|spec.ts
villains
villain
...
villain-list
...
shared
...
villains.component.ts|html|css|spec.ts
villains.module.ts
villains-routing.module.ts
app.component.ts|html|css|spec.ts
app.module.ts
app-routing.module.ts
main.ts
index.html
...
node_modules/...
...

把组件放在专用目录中的方式广受欢迎,对于小型应用,还可以保持组件扁平化(而不是放在专用目录中)。 这样会把四个文件放在现有目录中,也会减少目录的嵌套。无论你如何选择,请保持一致。

回到顶部

按特性组织的目录结构

风格 04-07

坚持根据特性区命名目录。

为何?开发人员可以快速定位代码,扫一眼就能知道每个文件代表什么,目录尽可能保持扁平,既没有重复也没有多余的名字。

为何? LIFT 原则中包含了所有这些。

为何?遵循 LIFT 原则精心组织内容,避免应用变得杂乱无章。

为何?当有很多文件时(例如 10 个以上),在专用目录型结构中定位它们会比在扁平结构中更容易。

坚持为每个特性区创建一个 Angular 模块。

为何? Angular 模块使惰性加载可路由的特性变得更容易。

为何?Angular 模块隔离、测试和复用特性更容易。

点这里查看目录和文件结构的范例
回到顶部

应用的根模块

风格 04-08

坚持在应用的根目录创建一个 Angular 模块(例如/src/app)。

为何?每个应用都至少需要一个根 Angular 模块。

考虑把根模块命名为app.module.ts

为何?能让定位和识别根模块变得更容易。

app/app.module.ts

  1. import { NgModule } from '@angular/core';
  2. import { BrowserModule } from '@angular/platform-browser';
  3. import { AppComponent } from './app.component';
  4. import { HeroesComponent } from './heroes/heroes.component';
  5. @NgModule({
  6. imports: [
  7. BrowserModule,
  8. ],
  9. declarations: [
  10. AppComponent,
  11. HeroesComponent
  12. ],
  13. exports: [ AppComponent ],
  14. entryComponents: [ AppComponent ]
  15. })
  16. export class AppModule {}
回到顶部

特性模块

风格 04-09

坚持为应用中每个明显的特性创建一个 Angular 模块。

坚持把特性模块放在与特性区同名的目录中(例如app/heroes)。

坚持特性模块的文件名应该能反映出特性区的名字和目录(例如app/heroes/heroes.module.ts)。

坚持特性模块的符号名应该能反映出特性区、目录和文件名(例如在app/heroes/heroes.module.ts中定义HeroesModule)。

为何?特性模块可以对其它模块暴露或隐藏自己的实现。

为何?特性模块标记出组成该特性分区的相关组件集合。

为何?方便路由到特性模块 —— 无论是用主动加载还是惰性加载的方式。

为何?特性模块在特定的功能和其它应用特性之间定义了清晰的边界。

为何?特性模块帮助澄清开发职责,以便于把这些职责指派给不同的项目组。

为何?特性模块易于隔离,以便测试。

回到顶部

共享特性模块

风格 04-10

坚持shared目录中创建名叫SharedModule的特性模块(例如在app/shared/shared.module.ts中定义SharedModule)。

坚持在共享模块中声明那些可能被特性模块引用的可复用组件、指令和管道。

考虑把可能在整个应用中到处引用的模块命名为SharedModule

避免 在共享模块中提供服务。服务通常是单例的,应该在整个应用或一个特定的特性模块中只有一份。

坚持SharedModule中导入所有模块都需要的资产(例如CommonModuleFormsModule)。

为何? SharedModule中包含的组件、指令和管道可能需要来自其它公共模块的特性(例如来自CommonModule中的ngFor指令)。

坚持SharedModule中声明所有组件、指令和管道。

坚持SharedModule中导出其它特性模块所需的全部符号。

为何? SharedModule的存在,能让常用的组件、指令和管道在很多其它模块的组件模板中都自动可用。

避免SharedModule中指定应用级的单例服务提供商。如果是刻意要得到多个服务单例也行,不过还是要小心。

为何?惰性加载的特性模块如果导入了这个共享模块,会创建一份自己的服务副本,这可能会导致意料之外的后果。

为何?对于单例服务,你不希望每个模块都有自己的实例。 而如果SharedModule提供了一个服务,那就有可能发生这种情况。

src
app
shared
shared.module.ts
init-caps.pipe.ts|spec.ts
text-filter.component.ts|spec.ts
text-filter.service.ts|spec.ts
app.component.ts|html|css|spec.ts
app.module.ts
app-routing.module.ts
main.ts
index.html
...
  1. import { NgModule } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { FormsModule } from '@angular/forms';
  4. import { FilterTextComponent } from './filter-text/filter-text.component';
  5. import { FilterTextService } from './filter-text/filter-text.service';
  6. import { InitCapsPipe } from './init-caps.pipe';
  7. @NgModule({
  8. imports: [CommonModule, FormsModule],
  9. declarations: [
  10. FilterTextComponent,
  11. InitCapsPipe
  12. ],
  13. providers: [FilterTextService],
  14. exports: [
  15. CommonModule,
  16. FormsModule,
  17. FilterTextComponent,
  18. InitCapsPipe
  19. ]
  20. })
  21. export class SharedModule { }
回到顶部

核心特性模块

风格04-11

考虑把那些数量庞大、辅助性的、只用一次的类收集到核心模块中,让特性模块的结构更清晰简明。

坚持把那些“只用一次”的类收集到CoreModule中,并对外隐藏它们的实现细节。简化的AppModule会导入CoreModule,并且把它作为整个应用的总指挥。

坚持core目录下创建一个名叫CoreModule的特性模块(例如在app/core/core.module.ts中定义CoreModule)。

坚持把要共享给整个应用的单例服务放进CoreModule中(例如ExceptionServiceLoggerService)。

坚持导入CoreModule中的资产所需要的全部模块(例如CommonModuleFormsModule)。

为何? CoreModule提供了一个或多个单例服务。Angular使用应用的根注入器注册这些服务提供商,让每个服务的这个单例对象对所有需要它们的组件都是可用的,而不用管该组件是通过主动加载还是惰性加载的方式加载的。

为何?CoreModule将包含一些单例服务。而如果是由惰性加载模块来导入这些服务,它就会得到一个新实例,而不是所期望的全应用级单例。

坚持把应用级、只用一次的组件收集到CoreModule中。 只在应用启动时从AppModule中导入它一次,以后再也不要导入它(例如NavComponentSpinnerComponent)。

为何?真实世界中的应用会有很多只用一次的组件(例如加载动画、消息浮层、模态框等),它们只会在AppComponent的模板中出现。 不会在其它地方导入它们,所以没有共享的价值。 然而它们又太大了,放在根目录中就会显得乱七八糟的。

避免AppModule之外的任何地方导入CoreModule

为何?如果惰性加载的特性模块直接导入CoreModule,就会创建它自己的服务副本,并导致意料之外的后果。

为何?主动加载的特性模块已经准备好了访问AppModule的注入器,因此也能取得CoreModule中的服务。

坚持CoreModule中导出AppModule需导入的所有符号,使它们在所有特性模块中可用。

为何?CoreModule的存在就让常用的单例服务在所有其它模块中可用。

为何?你希望整个应用都使用这个单例服务。 你不希望每个模块都有这个单例服务的单独的实例。 然而,如果CoreModule中提供了一个服务,就可能偶尔导致这种后果。

src
app
core
core.module.ts
logger.service.ts|spec.ts
nav
nav.component.ts|html|css|spec.ts
spinner
spinner.component.ts|html|css|spec.ts
spinner.service.ts|spec.ts
app.component.ts|html|css|spec.ts
app.module.ts
app-routing.module.ts
main.ts
index.html
...
  1. import { NgModule } from '@angular/core';
  2. import { BrowserModule } from '@angular/platform-browser';
  3. import { AppComponent } from './app.component';
  4. import { HeroesComponent } from './heroes/heroes.component';
  5. import { CoreModule } from './core/core.module';
  6. @NgModule({
  7. imports: [
  8. BrowserModule,
  9. CoreModule,
  10. ],
  11. declarations: [
  12. AppComponent,
  13. HeroesComponent
  14. ],
  15. exports: [ AppComponent ],
  16. entryComponents: [ AppComponent ]
  17. })
  18. export class AppModule {}

AppModule变得更小了,因为很多应用根部的类都被移到了其它模块中。 AppModule变得稳定了,因为你将会往其它模块中添加特性组件和服务提供商,而不是这个AppModuleAppModule把工作委托给了导入的模块,而不是亲力亲为。 AppModule聚焦在它自己的主要任务上:作为整个应用的总指挥。

回到顶部

防止多次导入CoreModule

风格 04-12

应该只有AppModule才允许导入CoreModule

坚持防范多次导入CoreModule,并通过添加守卫逻辑来尽快失败。

为何?守卫可以阻止对CoreModule的多次导入。

为何?守卫会禁止创建单例服务的多个实例。

  1. export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) {
  2. if (parentModule) {
  3. throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`);
  4. }
  5. }
回到顶部

惰性加载的目录

样式 04-13

某些边界清晰的应用特性或工作流可以做成惰性加载按需加载的,而不用总是随着应用启动。

坚持把惰性加载特性下的内容放进惰性加载目录中。 典型的惰性加载目录包含路由组件及其子组件以及与它们有关的那些资产和模块。

为何?这种目录让标识和隔离这些特性内容变得更轻松。

回到顶部

永远不要直接导入惰性加载的目录

样式 04-14

避免让兄弟模块和父模块直接导入惰性加载特性中的模块。

为何?直接导入并使用此模块会立即加载它,而原本的设计意图是按需加载它。

回到顶部

组件

组件选择器命名

风格05-02

坚持使用中线 (dashed) 命名法烤串 (kebab) 命名法来命名组件中的元素选择器。

为何?保持元素命名与自定义元素命名规范一致。

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

  1. /* avoid */
  2. @Component({
  3. selector: 'tohHeroButton',
  4. templateUrl: './hero-button.component.html'
  5. })
  6. export class HeroButtonComponent {}
  1. @Component({
  2. selector: 'toh-hero-button',
  3. templateUrl: './hero-button.component.html'
  4. })
  5. export class HeroButtonComponent {}
回到顶部

把组件当做元素

风格 05-03

坚持给组件一个元素选择器,而不是属性选择器。

为何?组件有很多包含 HTML 以及可选 Angular 模板语法的模板。 它们显示内容。开发人员会把组件像原生HTML元素和WebComponents一样放进页面中。

为何?查看组件模板的 HTML 时,更容易识别一个符号是组件还是指令。

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

  1. /* avoid */
  2. @Component({
  3. selector: '[tohHeroButton]',
  4. templateUrl: './hero-button.component.html'
  5. })
  6. export class HeroButtonComponent {}

app/app.component.html

  1. <!-- avoid -->
  2. <div tohHeroButton></div>
  1. @Component({
  2. selector: 'toh-hero-button',
  3. templateUrl: './hero-button.component.html'
  4. })
  5. export class HeroButtonComponent {}
回到顶部

把模板和样式提取到它们自己的文件

风格 05-04

坚持当超过 3 行时,把模板和样式提取到一个单独的文件。

坚持把模板文件命名为[component-name].component.html,其中,[component-name] 是组件名。

坚持把样式文件命名为[component-name].component.css,其中,[component-name] 是组件名。

坚持指定相对于模块的 URL ,给它加上./前缀。

为何?巨大的、内联的模板和样式表会遮盖组件的意图和实现方式,削弱可读性和可维护性。

为何?在多数编辑器中,编写内联的模板和样式表时都无法使用语法提示和代码片段功能。 Angular的TypeScript语言服务(即将到来)可以帮助那些编辑器在编写HTML模板时克服这一缺陷,但对CSS样式没有帮助。

为何?当你移动组件文件时,相对于组件的URL不需要修改,因为这些文件始终会在一起。

为何?./前缀是相对URL的标准语法,不必依赖Angular的特殊处理,如果没有前缀则不行。

app/heroes/heroes.component.ts

  1. /* avoid */
  2. @Component({
  3. selector: 'toh-heroes',
  4. template: `
  5. <div>
  6. <h2>My Heroes</h2>
  7. <ul class="heroes">
  8. <li *ngFor="let hero of heroes | async" (click)="selectedHero=hero">
  9. <span class="badge">{{hero.id}}</span> {{hero.name}}
  10. </li>
  11. </ul>
  12. <div *ngIf="selectedHero">
  13. <h2>{{selectedHero.name | uppercase}} is my hero</h2>
  14. </div>
  15. </div>
  16. `,
  17. styles: [`
  18. .heroes {
  19. margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em;
  20. }
  21. .heroes li {
  22. cursor: pointer;
  23. position: relative;
  24. left: 0;
  25. background-color: #EEE;
  26. margin: .5em;
  27. padding: .3em 0;
  28. height: 1.6em;
  29. border-radius: 4px;
  30. }
  31. .heroes .badge {
  32. display: inline-block;
  33. font-size: small;
  34. color: white;
  35. padding: 0.8em 0.7em 0 0.7em;
  36. background-color: #607D8B;
  37. line-height: 1em;
  38. position: relative;
  39. left: -1px;
  40. top: -4px;
  41. height: 1.8em;
  42. margin-right: .8em;
  43. border-radius: 4px 0 0 4px;
  44. }
  45. `]
  46. })
  47. export class HeroesComponent implements OnInit {
  48. heroes: Observable<Hero[]>;
  49. selectedHero: Hero;
  50. constructor(private heroService: HeroService) { }
  51. ngOnInit() {
  52. this.heroes = this.heroService.getHeroes();
  53. }
  54. }
  1. @Component({
  2. selector: 'toh-heroes',
  3. templateUrl: './heroes.component.html',
  4. styleUrls: ['./heroes.component.css']
  5. })
  6. export class HeroesComponent implements OnInit {
  7. heroes: Observable<Hero[]>;
  8. selectedHero: Hero;
  9. constructor(private heroService: HeroService) { }
  10. ngOnInit() {
  11. this.heroes = this.heroService.getHeroes();
  12. }
  13. }
回到顶部

内联输入和输出属性装饰器

风格 05-12

坚持 使用@Input()@Output(),而非@Directive@Component装饰器的inputsoutputs属性:

坚持@Input()或者@Output()放到所装饰的属性的同一行。

为何?易于在类里面识别哪些属性是输入属性或输出属性。

为何? 如果需要重命名与@Input或者@Output关联的属性或事件名,你可以在一个位置修改。

为何?依附到指令的元数据声明会比较简短,更易于阅读。

为何?把装饰器放到同一行可以精简代码,同时更易于识别输入或输出属性。

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

  1. /* avoid */
  2. @Component({
  3. selector: 'toh-hero-button',
  4. template: `<button></button>`,
  5. inputs: [
  6. 'label'
  7. ],
  8. outputs: [
  9. 'change'
  10. ]
  11. })
  12. export class HeroButtonComponent {
  13. change = new EventEmitter<any>();
  14. label: string;
  15. }

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

  1. @Component({
  2. selector: 'toh-hero-button',
  3. template: `<button>{{label}}</button>`
  4. })
  5. export class HeroButtonComponent {
  6. @Output() change = new EventEmitter<any>();
  7. @Input() label: string;
  8. }
回到顶部

避免为输入和输出属性指定别名

风格 05-13

避免除非有重要目的,否则不要为输入和输出指定别名。

为何?同一个属性有两个名字(一个对内一个对外)很容易导致混淆。

为何?如果指令名也同时用作输入属性,而且指令名无法准确描述这个属性的用途时,应该使用别名。

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

  1. /* avoid pointless aliasing */
  2. @Component({
  3. selector: 'toh-hero-button',
  4. template: `<button>{{label}}</button>`
  5. })
  6. export class HeroButtonComponent {
  7. // Pointless aliases
  8. @Output('changeEvent') change = new EventEmitter<any>();
  9. @Input('labelAttribute') label: string;
  10. }

app/app.component.html

  1. <!-- avoid -->
  2. <toh-hero-button labelAttribute="OK" (changeEvent)="doSomething()">
  3. </toh-hero-button>
  1. @Component({
  2. selector: 'toh-hero-button',
  3. template: `<button>{{label}}</button>`
  4. })
  5. export class HeroButtonComponent {
  6. // No aliases
  7. @Output() change = new EventEmitter<any>();
  8. @Input() label: string;
  9. }
回到顶部

成员顺序

风格 05-14

坚持把属性成员放在前面,方法成员放在后面。

坚持先放公共成员,再放私有成员,并按照字母顺序排列。

为何?把类的成员按照统一的顺序排列,易于阅读,能立即识别出组件的哪个成员服务于何种目的。

app/shared/toast/toast.component.ts

  1. /* avoid */
  2. export class ToastComponent implements OnInit {
  3. private defaults = {
  4. title: '',
  5. message: 'May the Force be with you'
  6. };
  7. message: string;
  8. title: string;
  9. private toastElement: any;
  10. ngOnInit() {
  11. this.toastElement = document.getElementById('toh-toast');
  12. }
  13. // private methods
  14. private hide() {
  15. this.toastElement.style.opacity = 0;
  16. window.setTimeout(() => this.toastElement.style.zIndex = 0, 400);
  17. }
  18. activate(message = this.defaults.message, title = this.defaults.title) {
  19. this.title = title;
  20. this.message = message;
  21. this.show();
  22. }
  23. private show() {
  24. console.log(this.message);
  25. this.toastElement.style.opacity = 1;
  26. this.toastElement.style.zIndex = 9999;
  27. window.setTimeout(() => this.hide(), 2500);
  28. }
  29. }

app/shared/toast/toast.component.ts

  1. export class ToastComponent implements OnInit {
  2. // public properties
  3. message: string;
  4. title: string;
  5. // private fields
  6. private defaults = {
  7. title: '',
  8. message: 'May the Force be with you'
  9. };
  10. private toastElement: any;
  11. // public methods
  12. activate(message = this.defaults.message, title = this.defaults.title) {
  13. this.title = title;
  14. this.message = message;
  15. this.show();
  16. }
  17. ngOnInit() {
  18. this.toastElement = document.getElementById('toh-toast');
  19. }
  20. // private methods
  21. private hide() {
  22. this.toastElement.style.opacity = 0;
  23. window.setTimeout(() => this.toastElement.style.zIndex = 0, 400);
  24. }
  25. private show() {
  26. console.log(this.message);
  27. this.toastElement.style.opacity = 1;
  28. this.toastElement.style.zIndex = 9999;
  29. window.setTimeout(() => this.hide(), 2500);
  30. }
  31. }
回到顶部

把逻辑放到服务里

风格 05-15

坚持在组件中只包含与视图相关的逻辑。所有其它逻辑都应该放到服务中。

坚持把可重用的逻辑放到服务中,保持组件简单,聚焦于它们预期目的。

为何?当逻辑被放置到服务里,并以函数的形式暴露时,可以被多个组件重复使用。

为何?在单元测试时,服务里的逻辑更容易被隔离。当组件中调用逻辑时,也很容易被模拟。

为何?从组件移除依赖并隐藏实施细节。

为何?保持组件苗条、精简和聚焦。

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

  1. /* avoid */
  2. import { OnInit } from '@angular/core';
  3. import { Http, Response } from '@angular/http';
  4. import { Observable } from 'rxjs/Observable';
  5. import 'rxjs/add/operator/catch';
  6. import 'rxjs/add/operator/finally';
  7. import 'rxjs/add/operator/map';
  8. import { Hero } from '../shared/hero.model';
  9. const heroesUrl = 'http://angular.io';
  10. export class HeroListComponent implements OnInit {
  11. heroes: Hero[];
  12. constructor(private http: Http) {}
  13. getHeroes() {
  14. this.heroes = [];
  15. this.http.get(heroesUrl)
  16. .map((response: Response) => <Hero[]>response.json().data)
  17. .catch(this.catchBadResponse)
  18. .finally(() => this.hideSpinner())
  19. .subscribe((heroes: Hero[]) => this.heroes = heroes);
  20. }
  21. ngOnInit() {
  22. this.getHeroes();
  23. }
  24. private catchBadResponse(err: any, source: Observable<any>) {
  25. // log and handle the exception
  26. return new Observable();
  27. }
  28. private hideSpinner() {
  29. // hide the spinner
  30. }
  31. }

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

  1. import { Component, OnInit } from '@angular/core';
  2. import { Hero, HeroService } from '../shared';
  3. @Component({
  4. selector: 'toh-hero-list',
  5. template: `...`
  6. })
  7. export class HeroListComponent implements OnInit {
  8. heroes: Hero[];
  9. constructor(private heroService: HeroService) {}
  10. getHeroes() {
  11. this.heroes = [];
  12. this.heroService.getHeroes()
  13. .subscribe(heroes => this.heroes = heroes);
  14. }
  15. ngOnInit() {
  16. this.getHeroes();
  17. }
  18. }
回到顶部

不要给输出属性加前缀

风格 05-16

坚持命名事件时,不要带前缀on

坚持把事件处理器方法命名为on前缀之后紧跟着事件名。

为何?与内置事件命名一致,例如按钮点击。

为何?Angular 允许另一种备选语法 on-*。如果事件的名字本身带有前缀on,那么绑定的表达式可能是on-onEvent

app/heroes/hero.component.ts

  1. /* avoid */
  2. @Component({
  3. selector: 'toh-hero',
  4. template: `...`
  5. })
  6. export class HeroComponent {
  7. @Output() onSavedTheDay = new EventEmitter<boolean>();
  8. }

app/app.component.html

  1. <!-- avoid -->
  2. <toh-hero (onSavedTheDay)="onSavedTheDay($event)"></toh-hero>
  1. export class HeroComponent {
  2. @Output() savedTheDay = new EventEmitter<boolean>();
  3. }
回到顶部

把表现层逻辑放到组件类里

风格 05-17

坚持把表现层逻辑放进组件类中,而不要放在模板里。

为何?逻辑应该只出现在一个地方(组件类里)而不应分散在两个地方。

为何?将组件的表现层逻辑放到组件类而非模板里,可以增强测试性、维护性和重复使用性。

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

  1. /* avoid */
  2. @Component({
  3. selector: 'toh-hero-list',
  4. template: `
  5. <section>
  6. Our list of heroes:
  7. <hero-profile *ngFor="let hero of heroes" [hero]="hero">
  8. </hero-profile>
  9. Total powers: {{totalPowers}}<br>
  10. Average power: {{totalPowers / heroes.length}}
  11. </section>
  12. `
  13. })
  14. export class HeroListComponent {
  15. heroes: Hero[];
  16. totalPowers: number;
  17. }

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

  1. @Component({
  2. selector: 'toh-hero-list',
  3. template: `
  4. <section>
  5. Our list of heroes:
  6. <toh-hero *ngFor="let hero of heroes" [hero]="hero">
  7. </toh-hero>
  8. Total powers: {{totalPowers}}<br>
  9. Average power: {{avgPower}}
  10. </section>
  11. `
  12. })
  13. export class HeroListComponent {
  14. heroes: Hero[];
  15. totalPowers: number;
  16. get avgPower() {
  17. return this.totalPowers / this.heroes.length;
  18. }
  19. }
回到顶部

指令

使用指令来加强已有元素

风格 06-01

坚持当你需要有表现层逻辑,但没有模板时,使用属性型指令。

为何?属性型指令没有模板。

为何?一个元素可以使用多个属性型指令。

app/shared/highlight.directive.ts

  1. @Directive({
  2. selector: '[tohHighlight]'
  3. })
  4. export class HighlightDirective {
  5. @HostListener('mouseover') onMouseEnter() {
  6. // do highlight work
  7. }
  8. }

app/app.component.html

<div tohHighlight>Bombasta</div>
回到顶部

HostListenerHostBinding 装饰器 vs. 组件元数据 host

风格 06-03

考虑优先使用@HostListener@HostBinding,而不是@Directive@Component装饰器的host属性。

坚持让你的选择保持一致。

为何?对于关联到@HostBinding的属性或关联到@HostListener的方法,要修改时,只需在指令类中的一个地方修改。 如果使用元数据属性host,你就得在组件类中修改属性声明的同时修改相关的元数据。

app/shared/validator.directive.ts

  1. import { Directive, HostBinding, HostListener } from '@angular/core';
  2. @Directive({
  3. selector: '[tohValidator]'
  4. })
  5. export class ValidatorDirective {
  6. @HostBinding('attr.role') role = 'button';
  7. @HostListener('mouseenter') onMouseEnter() {
  8. // do work
  9. }
  10. }

与不推荐的方式(host元数据)比较一下。

为何?host元数据只是一个便于记忆的名字而已,并不需要额外的 ES 导入。

app/shared/validator2.directive.ts

  1. import { Directive } from '@angular/core';
  2. @Directive({
  3. selector: '[tohValidator2]',
  4. host: {
  5. 'attr.role': 'button',
  6. '(mouseenter)': 'onMouseEnter()'
  7. }
  8. })
  9. export class Validator2Directive {
  10. role = 'button';
  11. onMouseEnter() {
  12. // do work
  13. }
  14. }
回到顶部

服务

服务总是单例的

风格 07-01

坚持在同一个注入器内,把服务当做单例使用。用它们来共享数据和功能。

为何?服务是在特性范围或应用内共享方法的理想载体。

为何?服务是共享状态性内存数据的理想载体。

app/heroes/shared/hero.service.ts

  1. export class HeroService {
  2. constructor(private http: Http) { }
  3. getHeroes() {
  4. return this.http.get('api/heroes')
  5. .map((response: Response) => <Hero[]>response.json().data);
  6. }
  7. }
回到顶部

单一职责

风格 07-02

坚持创建单一职责的服务,用职责封装在它的上下文中。

坚持当服务成长到超出单一用途时,创建一个新服务。

为何?当服务有多个职责时,它很难被测试。

为何?当某个服务有多个职责时,每个注入它的组件或服务都会承担这些职责的全部开销。

回到顶部

提供一个服务

风格 07-03

坚持将服务提供到共享范围内的顶级组件的 Angular 注入器。

为何? Angular 注入器是层次化的。

为何?在顶层组件提供服务时,该服务实例在所有子组件中可见并共享。

为何?服务是共享方法或状态的理想载体。

为何?当不同的两个组件需要一个服务的不同的实例时,上面的方法这就不理想了。在这种情况下,对于需要崭新和单独服务实例的组件,最好在组件级提供服务。

  1. import { Component } from '@angular/core';
  2. import { HeroService } from './heroes';
  3. @Component({
  4. selector: 'toh-app',
  5. template: `
  6. <toh-heroes></toh-heroes>
  7. `,
  8. providers: [HeroService]
  9. })
  10. export class AppComponent {}
回到顶部

使用 @Injectable() 类装饰器

风格 07-04

坚持当使用类型作为令牌来注入服务的依赖时,使用@Injectable()类装饰器,而非@Inject()参数装饰器。

为何? Angular 的 DI 机制会根据服务的构造函数参数的声明类型来解析服务的所有依赖。

为何?当服务只接受类型令牌相关的依赖时,比起在每个构造函数参数上使用@Inject()@Injectable()的语法简洁多了。

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

  1. /* avoid */
  2. export class HeroArena {
  3. constructor(
  4. @Inject(HeroService) private heroService: HeroService,
  5. @Inject(Http) private http: Http) {}
  6. }

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

  1. @Injectable()
  2. export class HeroArena {
  3. constructor(
  4. private heroService: HeroService,
  5. private http: Http) {}
  6. }
回到顶部

数据服务

通过服务与Web服务器交互

风格 08-01

坚持把数据操作和与数据交互的逻辑重构到服务里。

坚持让数据服务来负责 XHR 调用、本地储存、内存储存或者其它数据操作。

为何?组件的职责是为视图展示或收集信息。它不应该关心如何获取数据,它只需要知道向谁请求数据。把如何获取数据的逻辑移动到数据服务里,简化了组件,让其聚焦于视图。

为何?在测试使用数据服务的组件时,可以让数据调用更容易被测试(模拟或者真实)。

为何?数据管理的详情,比如头信息、方法、缓存、错误处理和重试逻辑,不是组件和其它的数据消费者应该关心的事情。

数据服务应该封装这些细节。这样,在服务内部修改细节,就不会影响到它的消费者。并且更容易通过实现一个模拟服务来对消费者进行测试。

回到顶部

生命周期钩子

使用生命周期钩子来介入到 Angular 暴露的重要事件里。

回到顶部

实现生命周期钩子接口

风格 09-01

坚持实现生命周期钩子接口。

为何?如果使用强类型的方法签名,编译器和编辑器可以帮你揪出拼写错误。

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

  1. /* avoid */
  2. @Component({
  3. selector: 'toh-hero-button',
  4. template: `<button>OK<button>`
  5. })
  6. export class HeroButtonComponent {
  7. onInit() { // misspelled
  8. console.log('The component is initialized');
  9. }
  10. }

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

  1. @Component({
  2. selector: 'toh-hero-button',
  3. template: `<button>OK</button>`
  4. })
  5. export class HeroButtonComponent implements OnInit {
  6. ngOnInit() {
  7. console.log('The component is initialized');
  8. }
  9. }
回到顶部

附录

有用的 Angular 工具和小提示

回到顶部

Codelyzer

风格 A-01

坚持使用 codelyzer 来实施本指南。

考虑调整 codelyzer 的规则来满足你的需求。

回到顶部

文档模板和代码片段

风格 A-02

坚持使用文件模板或代码片段来帮助实现一致的风格和模式。下面是为一些网络开发编辑器和 IDE 准备的模板和/或代码片段:

考虑使用 Visual Studio Code代码片段 来实施本风格指南。

Use Extension

考虑使用 Atom代码片断来实施本风格指南。

考虑使用 Sublime Text代码片断 来实施本风格指南。

考虑使用 Vim代码片断来实施本风格指南。

回到顶部