JavaScript 之 Angular 设计风格指南

简要说明:本文主要摘录于 Angular 官网中 JavaScript 的设计风格指南。本风格指南介绍了提倡的约定,更重要的是,解释了为什么。个人觉得对于 JavaScript 开发时的规范约束、以及编程风格等讲解得十分透彻。不愧为大公司出品,对于学习乃至项目的研发,都大有裨益。

一、风格词汇

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

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

  1. 坚持意味着总是应该遵循的约定。 总是可能有点太强了。应该总是遵循的指导原则非常少。 但是,只有遇到非常不寻常的情况才能打破坚持的原则。
  2. 考虑标志着通常应该遵循的指导原则。
    如果能完全理解指导原则背后的含义,并且很好的理由背离它,那就可以那么做。但是请保持一致。
  3. 避免标志着我们决不应该做的事。

二、文件结构约定

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

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

三、单一职责

所有组件、服务和其它符号都要遵循单一职责原则。 这会使应用程序更干净,易于阅读和维护,提高可测试性。

3.1 单一法则
  1. 坚持每个文件只定义一样东西(例如服务或组件)。
  2. 考虑把文件大小限制在 400 行代码以内。

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

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

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

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

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

3.2 简单函数
  1. 坚持定义简单函数
  2. 考虑限制在 75 行之内。

简单函数更易于测试,特别是当它们只做一件事,只为一个目的服务时。
简单函数促进代码重用,更易于阅读,更易于维护。
简单函数可避免易在大函数中产生的隐蔽性错误,例如与外界共享变量、创建意外的闭包或与依赖之间产生意外耦合等。

四、命名

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

4.1 总体命名指导原则
  1. 坚持所有符号使用一致的命名规则。
  2. 坚持遵循同一个模式来描述符号的特性和类型。推荐的模式为feature.type.ts

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

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

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

4.2 使用点和横杠来分隔文件名
  1. 坚持在描述性名字中,用横杠来分隔单词。
  2. 坚持使用来分隔描述性名字和类型。
  3. 坚持遵循先描述组件特性,再描述它的类型的模式,对所有组件使用一致的类型命名规则。推荐的模式为feature.type.ts
  4. 坚持使用惯用的后缀来描述类型,包括*.service*.component*.pipe*.module*.directive。 必要时可以创建更多类型名,但必须注意,不要创建太多。

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

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

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

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

4.3 符号名与文件名
  1. 坚持为所有东西使用一致的命名约定,以它们所代表的东西命名。
  2. 坚持使用大写驼峰命名法来命名类。符号名匹配它所在的文件名。
  3. 坚持在符号名后面追加约定的类型后缀(例如Component、Directive、Module、Pipe、Service)。
  4. 坚持在文件名后面追加约定的类型后缀(例如.component.ts、.directive.ts、.module.ts、.pipe.ts、.service.ts)。

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

为何?大驼峰命名法用于标识可以通过构造函数实例化的对象。

符号名
文件名

export class AppComponent { }
app.component.ts

export class HeroesComponent { }
heroes.component.ts

export class HeroListComponent { }
hero-list.component.ts

export class ValidationDirective { }
validation.directive.ts

export class AppModule
app.module.ts

export class InitCapsPipe implements PipeTransform { }
init-caps.pipe.ts

export class UserProfileService { }
user-profile.service.ts
4.4 服务名
  1. 坚持使用一致的规则命名服务,以它们的特性来命名。
  2. 坚持使用大写驼峰命名法命名服务。
  3. 坚持添加Service后缀,当不清楚它们是什么时(例如当它们是名词时)。

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

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

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

符号名
文件名

export class HeroDataService { }
hero-data.service.ts

export class CreditService { }
credit.service.ts

export class Logger { }
logger.service.ts
4.5 引导
  1. 坚持把应用的引导程序和平台相关的逻辑放到名为main.ts的文件里。
  2. 坚持在引导逻辑中包含错误处理代码
  3. 避免把应用逻辑放在main.ts中,而应放在组件或服务里。

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

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

// main.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule }              from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
  .then(success => console.log(`Bootstrap success`))
  .catch(err => console.error(err));
4.6 指令选择器
  1. 坚持使用小驼峰命名法来命名指令的选择器。

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

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

4.7 单元测试文件名
  1. 坚持测试规格文件名与被测试组件文件名相同。
  2. 坚持测试规格文件名添加.spec后缀。

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

为何?提供一个与 karma 或者其它测试运行器相配的命名模式。

五、代码约定

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

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

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

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

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

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

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

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

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

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

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

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

// app/shared/data.service.ts
export const mockHeroes   = ['Sam', 'Jill']; // prefer
export const heroesUrl    = 'api/heroes';    // prefer
export const VILLAINS_URL = 'api/villains';  // tolerate
5.3 接口
  1. 坚持使用大写驼峰命名法来命名接口。
  2. 考虑不要在接口名字前面加I前缀。
  3. 考虑用类代替接口。

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

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

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

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

// app/shared/hero-collector.service.ts
/* avoid */
import { Injectable } from '@angular/core';
import { IHero } from './hero.model.avoid';
@Injectable()
export class HeroCollectorService {
  hero: IHero;
  constructor() { }
}

// app/shared/hero-collector.service.ts
import { Injectable } from '@angular/core';
import { Hero } from './hero.model';
@Injectable()
export class HeroCollectorService {
  hero: Hero;
  constructor() { }
}
5.4 属性和方法
  1. 坚持使用 小写驼峰命名法 来命名属性和方法。
  2. 避免为私有属性和方法添加_前缀。

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

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

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

5.5 导入语句中的空行
  1. 坚持在第三方导入和应用导入之间留一个空行。
  2. 考虑按模块名字的字母顺排列导入行。
  3. 考虑在解构表达式中按字母顺序排列导入的东西。

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

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

// app/heroes/shared/hero.service.ts
/* avoid */
import { SpinnerService, ExceptionService, ToastService } from '../../core';
import { Http, Response } from '@angular/http';
import { Injectable } from '@angular/core';
import { Hero } from './hero.model';

// app/heroes/shared/hero.service.ts
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';

import { Hero } from './hero.model';
import { ExceptionService, SpinnerService, ToastService } from '../../core';

六、应用程序结构与 Angular 模块

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

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

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

6.1 LIFT
  1. 坚持组织应用的结构,达到这些目的:快速定位 (Locate) 代码、一眼识别 (Identify) 代码、 尽量保持扁平结构 (Flattest) 和尝试 (Try) 遵循DRY (Do Not Repeat Yourself, 不重复自己) 原则。
  2. 坚持四项基本原则定义文件结构,上面的原则是按重要顺序排列的。

为何?LIFT 提供了一致的结构,它具有扩展性强、模块化的特性。因为容易快速锁定代码,提高了开发者的效率。

另外,检查应用结构是否合理的方法是问问自己:我们能快速打开与此特性有关的所有文件并开始工作吗?

6.2 定位

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

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

6.3 识别
  1. 坚持命名文件到这个程度:看到名字立刻知道它包含了什么,代表了什么。
  2. 坚持文件名要具有说明性,确保文件中只包含一个组件。
  3. 避免创建包含多个组件、服务或者混合体的文件。

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

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

6.4 扁平
  1. 坚持尽可能保持扁平的目录结构。
  2. 考虑当同一目录下达到 7 个或更多个文件时创建子目录。
  3. 考虑配置 IDE,以隐藏无关的文件,例如生成出来的.js文件和.js.map文件等。

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

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

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

6.5 T-DRY (尝试不重复自己)
  1. 坚持 DRY(Don't Repeat Yourself,不重复自己)。
  2. 避免过度 DRY,以致牺牲了阅读性。

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

6.6 总体结构指导原则
  1. 坚持从零开始,但要考虑应用程序接下来的路往哪儿走。
  2. 坚持有一个近期实施方案和一个长期的愿景。
  3. 坚持把所有源代码都放到名为app的目录里。
  4. 坚持如果组件具有多个伴隨文件 (.ts、.html、.css和.spec),就为它创建一个文件夹。

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

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

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

6.7 按特性组织的目录结构
  1. 坚持根据特性区命名目录。

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

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

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

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

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

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

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

6.8 应用的根模块

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

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

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

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

6.9 特性模块
  1. 坚持为应用中每个明显的特性创建一个 Angular 模块。
  2. 坚持把特性模块放在与特性区同名的目录中(例如app/heroes)。
  3. 坚持特性模块的文件名应该能反映出特性区的名字和目录(例如app/heroes/heroes.module.ts)。
  4. 坚持特性模块的符号名应该能反映出特性区、目录和文件名(例如在app/heroes/heroes.module.ts中定义HeroesModule)。

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

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

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

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

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

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

6.10 核心特性模块
  1. 坚持把那些“只用一次”的类收集到CoreModule中,并对外隐藏它们的实现细节。简化的AppModule会导入CoreModule,并且把它作为整个应用的总指挥。
  2. 坚持在core目录下创建一个名叫CoreModule的特性模块(例如在app/core/core.module.ts中定义CoreModule)。
  3. 坚持把一个要共享给整个应用的单例服务放进CoreModule中(例如ExceptionServiceLoggerService)。
  4. 坚持导入CoreModule中的资产所需要的全部模块(例如CommonModuleFormsModule)。

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

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

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

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

  1. 避免在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
...
app/app.module.ts app/core/core.module.ts app/core/logger.service.ts app/core/nav/nav.component.ts app/core/nav/nav.component.html app/core/spinner/spinner.component.ts app/core/spinner/spinner.component.html app/core/spinner/spinner.service.ts
COPY CODE
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent }   from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
import { CoreModule }    from './core/core.module';
@NgModule({
  imports: [
    BrowserModule,
    CoreModule,
  ],
  declarations: [
    AppComponent,
    HeroesComponent
  ],
  exports: [ AppComponent ],
  entryComponents: [ AppComponent ]
})
export class AppModule {}

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

6.11 惰性加载的目录
  1. 某些边界清晰的应用特性或工作流可以做成惰性加载或按需加载的,而不用总是随着应用启动。
  2. 坚持把惰性加载特性下的内容放进惰性加载目录中。 典型的惰性加载目录包含路由组件及其子组件以及与它们有关的那些资产和模块。

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

七、组件

7.1 组件选择器命名

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

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

7.2 把组件当做元素

坚持通过选择器把组件定义为元素。

为何?组件有很多包含 HTML 以及可选 Angular 模板语法的模板。它们多数都与把内容放进页面有关,因而组件更接近于元素。

为何?组件代表页面上的一个可视元素。 把选择器定义成 HTML 元素标签可以与原生 HTML 元素和 WebComponent 保持一致。

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

7.3 把模板和样式提取到它们自己的文件
  1. 坚持当超过 3 行时,把模板和样式提取到一个单独的文件。
  2. 坚持把模板文件命名为[component-name].component.html,其中,[component-name] 是组件名。
  3. 坚持把样式文件命名为[component-name].component.css,其中,[component-name] 是组件名。

为何?在 (.js.ts) 代码里面内联模板时,某些编辑器不支持语法提示。

为何?当没有与内联模板和样式混合时,组件文件中的逻辑更易于阅读。

7.4 避免重命名输入和输出

避免重命名输入和输出。

为何?当指令的输入或输出属性的名字与导出的公共 API 名字不一样时,可能导致混乱。

7.5 成员顺序
  1. 坚持把属性成员放在前面,方法成员放在后面。
  2. 坚持先放公共成员,再放私有成员,并按照字母顺序排列。

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

7.6 把逻辑放到服务里
  1. 坚持在组件中只包含与视图相关的逻辑。所有其它逻辑都应该放到服务中。
  2. 坚持把可重用的逻辑放到服务中,保持组件简单,聚焦于它们预期目的。

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

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

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

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

// app/heroes/hero-list/hero-list.component.ts
/* avoid */
import { OnInit } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Hero } from '../shared/hero.model';
const heroesUrl = 'http://angular.io';
export class HeroListComponent implements OnInit {
  heroes: Hero[];
  constructor(private http: Http) {}
  getHeroes() {
    this.heroes = [];
    this.http.get(heroesUrl)
      .map((response: Response) => <Hero[]>response.json().data)
      .catch(this.catchBadResponse)
      .finally(() => this.hideSpinner())
      .subscribe((heroes: Hero[]) => this.heroes = heroes);
  }
  ngOnInit() {
    this.getHeroes();
  }
  private catchBadResponse(err: any, source: Observable<any>) {
    // log and handle the exception
    return new Observable();
  }
  private hideSpinner() {
    // hide the spinner
  }
}

// app/heroes/hero-list/hero-list.component.ts
import { Component, OnInit } from '@angular/core';
import { Hero, HeroService } from '../shared';
@Component({
  selector: 'toh-hero-list',
  template: `...`
})
export class HeroListComponent implements OnInit {
  heroes: Hero[];
  constructor(private heroService: HeroService) {}
  getHeroes() {
    this.heroes = [];
    this.heroService.getHeroes()
      .subscribe(heroes => this.heroes = heroes);
  }
  ngOnInit() {
    this.getHeroes();
  }
}
7.7 不要给输出属性加前缀
  1. 坚持命名事件时,不要带前缀on
  2. 坚持把事件处理器方法命名为on前缀之后紧跟着事件名。

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

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

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

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

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

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

app/heroes/hero-list/hero-list.component.ts
COPY CODE
/* avoid */
@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
COPY CODE
@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;
  }
}

八、数据服务(☆☆☆☆重要)

8.1 分离数据调用
  1. 坚持把数据操作和互动重构到服务里。
  2. 坚持让数据服务来负责 XHR 调用、本地储存、内存储存或者其它数据操作。

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

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

为何?数据服务的实现可能有非常具体的代码来处理数据仓库,包括数据头 (headers)、如何与数据交谈或者其它服务 (例如Http)。把逻辑分离到数据服务可以将该逻辑封装在一个地方,对外部使用者(例如组件)隐藏具体的实施细节。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容