-
单一职责:对所有的组件、服务等等应用单一职责原则 (SRP)
- 单一规则 (风格 01-01)
- 坚持每个文件只定义一样东西(例如服务或组件)
- 考虑把每个文件大小限制在 400 行代码以内;
- 小函数 (风格 01-02)
- 坚持定义简单函数,考虑限制在 75 行之内。
-
命名
- 总体命名原则 (风格 02-01)
- 坚持所有符号使用一致的命名规则;
- 坚持遵循同一个模式来描述符号的特性和类型。推荐的模式为
feature.type.ts
- 使用点和横杠来分隔文件名 (风格 02-02)
- 坚持 在描述性名字中,用横杠来分隔单词;
- 坚持使用点来分隔描述性名字和类型;
- 坚持遵循先描述组件特性,再描述它的类型的模式,对所有组件使用一致的类型命名规则。推荐的模式为
feature.type.ts;
- 坚持使用惯用的后缀来描述类型,包括
*.service
、*.component
、*.pipe
、.module
、.directive
。 必要时可以创建更多类型名,但必须注意,不要创建太多
- 符号名与文件名 (风格 02-03)
- 服务名 (风格 02-04)
- 坚持使用一致的规则命名服务,以它们的特性来命名;
- 坚持为服务的类名加上
Service
后缀。 例如,获取数据或英雄列表的服务应该命名为DataService
或HeroService;
- 有些词汇显然就是服务,比如那些以“-er”后缀结尾的。比如把记日志的服务命名为
Logger
就比LoggerService
更好些。需要在你的项目中决定这种特例是否可以接受。 但无论如何,都要尽量保持一致
- 引导 (风格 02-05)
- 坚持把应用的引导程序和平台相关的逻辑放到名为
main.ts
的文件里; - 坚持在引导逻辑中包含错误处理代码;
- 避免把应用逻辑放在
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));
- 坚持把应用的引导程序和平台相关的逻辑放到名为
- 指令选择器 (风格 02-06)
- 坚持使用中线命名法(dashed-case)或叫烤串命名法(kebab-case)来命名组件的元素选择器;
- 为何?Angular HTML 解析器是大小写敏感的,它识别小写驼峰写法
- 为组件添加自定义前缀 (风格 02-07)
- 坚持使用带连字符的小写元素选择器值(例如
admin-users
); - 坚持为组件选择器添加自定义前缀。 例如,
toh
前缀表示 Tour of Heroes(英雄指南),而前缀 `admin 表示管理特性区; - 坚持使用前缀来识别特性区或者应用程序本身
- 坚持使用带连字符的小写元素选择器值(例如
- 指令选择器(风格 02-06)
- 坚持使用小驼峰形式命名指令的选择器;
- 为何?可以让指令中的属性名与视图中绑定的属性名保持一致;
- 为何? Angular 的 HTML 解析器是大小写敏感的,可以识别小驼峰形式
- 为指令添加自定义前缀 (风格 02-08)
- 坚持为指令的选择器添加自定义前缀(例如前缀
toh
来自 Tour of Heroes); - 坚持用小驼峰形式拼写非元素选择器,除非该选择器用于匹配原生 HTML 属性
- 坚持为指令的选择器添加自定义前缀(例如前缀
- 管道名 (风格 02-09)
- 坚持为所有管道使用一致的命名约定,用它们的特性来命名
- 单元测试文件名 (风格 02-10)
- 坚持测试规格文件名与被测试组件文件名相同;
- 坚持测试规格文件名添加
.spec
后缀
- 端到端(E2E)测试的文件名 (风格 02-11)
- 坚持端到端测试规格文件和它们所测试的特性同名,添加
.e2e-spec
后缀
- 坚持端到端测试规格文件和它们所测试的特性同名,添加
- Angular NgModule命名 (风格 02-12)
- 坚持为符号名添加
Module
后缀; - 坚持为文件名添加
.module.ts
扩展名; - 坚持用特性名和所在目录命名模块;
- 坚持为 RoutingModule 类名添加
RoutingModule
后缀; - 坚持为 RoutingModule 的文件名添加
-routing.module.ts
后缀
- 坚持为符号名添加
-
编程约定:坚持一致的编程、命名和空格的约定
- 类 (风格 03-01)
- 坚持使用大写驼峰命名法来命名类
- 常量 (风格 03-02)
- 坚持用
const
声明变量,除非它们的值在应用的生命周期内会发生变化; - 考虑 把常量名拼写为小驼峰格式
为何?小驼峰变量名 (heroRoutes
) 比传统的大写蛇形命名法 (HERO_ROUTES
) 更容易阅读和理解;
为何? 把常量命名为大写蛇形命名法的传统源于现代 IDE 出现之前, 以便阅读时可以快速发现那些const
定义。 TypeScript 本身就能够防止意外赋值 - 坚持容许现存的
const
常量沿用大写蛇形命名法
为何?传统的大写蛇形命名法仍然很流行、很普遍,特别是在第三方模块中。 修改它们没多大价值,还会有破坏现有代码和文档的风险
- 坚持用
- 接口 (风格 03-03)
- 坚持使用大写驼峰命名法来命名接口;
- 考虑不要在接口名字前面加
I
前缀; - 考虑在服务和可声明对象(组件、指令和管道)中用类代替接口;
- 考虑用接口作为数据模型
- 属性和方法 (风格 03-04)
- 坚持使用小写驼峰命名法来命名属性和方法;
- 避免为私有属性和方法添加下划线前缀
- 导入语句中的空行 (风格 03-06)
- 坚持在第三方导入和应用导入之间留一个空行;
- 考虑按模块名字的字母顺排列导入行;
- 考虑在解构表达式中按字母顺序排列导入的东西
-
应用程序结构与Angular模块
- LIFT (风格 04-01)
- 坚持组织应用的结构,达到这些目的:快速定位 (
L
ocate) 代码、一眼识别 (I
dentify) 代码、 尽量保持扁平结构 (F
lattest) 和尝试 (T
ry) 遵循 DRY (Do Not Repeat Yourself, 不重复自己) 原则; - 坚持四项基本原则定义文件结构,上面的原则是按重要顺序排列的
- 坚持组织应用的结构,达到这些目的:快速定位 (
- 定位 (风格 04-02)
- 识别 (风格 04-03)
- 坚持命名文件到这个程度:看到名字立刻知道它包含了什么,代表了什么;
- 坚持文件名要具有说明性,确保文件中只包含一个组件;
- 避免创建包含多个组件、服务或者混合体的文件
- 为何?花费更少的时间来查找和琢磨代码,就会变得更有效率。 较长的文件名远胜于较短却容易混淆的缩写名。
- 扁平 (风格 04-04)
- 坚持尽可能保持扁平的目录结构;
- 考虑当同一目录下达到 7 个或更多个文件时创建子目录;
为何?没人想要在超过七层的目录中查找文件。扁平的结构有利于搜索
另一方面,心理学家们相信, 当关注的事物超过 9 个时,人类就会开始感到吃力。 所以,当一个文件夹中的文件有 10 个或更多个文件时,可能就是创建子目录的时候了 - 考虑配置 IDE,以隐藏无关的文件,例如生成出来的
.js
文件和.js.map
文件等; - 还是根据你自己的舒适度而定吧。 除非创建新文件夹能有显著的价值,否则尽量使用扁平结构
- 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
),就为它创建一个文件夹
- 按特性组织的目录结构 (风格 04-07)
- 坚持根据特性区命名目录
为何?开发人员可以快速定位代码,扫一眼就能知道每个文件代表什么,目录尽可能保持扁平,既没有重复也没有多余的名字。 - 坚持为每个特性区创建一个 Angular 模块
- 坚持根据特性区命名目录
- 应用的根模块 (风格 04-08)
- 坚持在应用的根目录创建一个 Angular 模块(例如
/src/app
); - 考虑把根模块命名为
app.module.ts
- 坚持在应用的根目录创建一个 Angular 模块(例如
- 特性模块 (风格 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
提供了FilterTextService
。这里可以这么做,因为该服务是无状态的,也就是说,该服务的消费者不会受到这些新实例的影响; - 坚持在
SharedModule
中导入所有模块都需要的资产(例如CommonModule
和FormsModule
); - 坚持在
SharedModule
中声明所有组件、指令和管道; - 坚持从
SharedModule
中导出其它特性模块所需的全部符号; - 避免在
SharedModule
中指定应用级的单例服务提供商。如果是刻意要得到多个服务单例也行,不过还是要小心
- 坚持在
- 核心特性模块 (风格 04-11)
- 考虑把那些数量庞大、辅助性的、只用一次的类收集到核心模块中,让特性模块的结构更清晰简明;
- 坚持把那些“只用一次”的类收集到
CoreModule
中,并对外隐藏它们的实现细节。简化的AppModule
会导入CoreModule
,并且把它作为整个应用的总指挥; - 坚持在
core
目录下创建一个名叫CoreModule
的特性模块(例如在app/core/core.module.ts
中定义CoreModule
); - 坚持把要共享给整个应用的单例服务放进
CoreModule
中(例如ExceptionService
和LoggerService
); - 坚持导入
CoreModule
中的资产所需要的全部模块(例如CommonModule
和FormsModule
); - 坚持把应用级、只用一次的组件收集到
CoreModule
中。 只在应用启动时从AppModule
中导入它一次,以后再也不要导入它(例如NavComponent
和SpinnerComponent
);
为何?真实世界中的应用会有很多只用一次的组件(例如加载动画、消息浮层、模态框等),它们只会在AppComponent
的模板中出现。 不会在其它地方导入它们,所以没有共享的价值。 然而它们又太大了,放在根目录中就会显得乱七八糟的 - 避免在
AppModule
之外的任何地方导入CoreModule
为何?如果惰性加载的特性模块直接导入CoreModule
,就会创建它自己的服务副本,并导致意料之外的后果;
为何?主动加载的特性模块已经准备好了访问AppModule
的注入器,因此也能取得CoreModule
中的服务 - 坚持从
CoreModule
中导出AppModule
需导入的所有符号,使它们在所有特性模块中可用
为何?CoreModule
的存在就让常用的单例服务在所有其它模块中可用
- 防止多次导入CoreModule (风格 04-12)
- 应该只有
AppModule
才允许导入CoreModule;
- 坚持防范多次导入
CoreModule
,并通过添加守卫逻辑来尽快失败;
为何?守卫可以阻止对CoreModule
的多次导入;
为何?守卫会禁止创建单例服务的多个实例
- 应该只有
- 惰性加载的目录 (风格 04-13)
- 某些边界清晰的应用特性或工作流可以做成惰性加载或按需加载的,而不用总是随着应用启动;
- 坚持把惰性加载特性下的内容放进惰性加载目录中。 典型的惰性加载目录包含路由组件及其子组件以及与它们有关的那些资产和模块
- 永远不要直接导入惰性加载的目录 (风格 04-14)
- 避免让兄弟模块和父模块直接导入惰性加载特性中的模块
为何?直接导入并使用此模块会立即加载它,而原本的设计意图是按需加载它
- 避免让兄弟模块和父模块直接导入惰性加载特性中的模块
-
Components
- 把组件当作元素:考虑给组件一个元素选择器,而不是属性或类选择器 (风格 05-03)
- 把模板和样式提取到他们自己的文件 (风格 05-04)
- 坚持当超过 3 行时,把模板和样式提取到一个单独的文件;
- 坚持把模板文件命名为
[component-name].component.html
,其中,[component-name] 是组件名; - 坚持把样式文件命名为
[component-name].component.css
,其中,[component-name] 是组件名; - 坚持指定相对于模块的 URL ,给它加上
./
前缀
- 内联输入和输出属性装饰器 (风格 05-12)
- 避免为输入和输出属性指定别名 (风格 05-13)
- 避免除非有重要目的,否则不要为输入和输出指定别名
为何?同一个属性有两个名字(一个对内一个对外)很容易导致混淆;
为何?如果指令名也同时用作输入属性,而且指令名无法准确描述这个属性的用途时,应该使用别名
- 避免除非有重要目的,否则不要为输入和输出指定别名
- 成员顺序 (风格 05-14)
- 坚持把属性成员放在前面,方法成员放在后面;
- 坚持先放公共成员,再放私有成员,并按照字母顺序排列
- 把逻辑放到服务里 (风格 05-15)
- 坚持在组件中只包含与视图相关的逻辑。所有其它逻辑都应该放到服务中;
- 坚持把可重用的逻辑放到服务中,保持组件简单,聚焦于它们预期目的
为何?当逻辑被放置到服务里,并以函数的形式暴露时,可以被多个组件重复使用
为何?在单元测试时,服务里的逻辑更容易被隔离。当组件中调用逻辑时,也很容易被模拟
为何?从组件移除依赖并隐藏实施细节
为何?保持组件苗条、精简和聚焦
- 不要给输出属性加前缀 (风格 05-16)
- 坚持命名事件时,不要带前缀
on
为何?与内置事件命名一致,例如按钮点击 - 坚持把事件处理器方法命名为
on
前缀之后紧跟着事件名
为何?Angular 允许另一种备选语法on-*
。如果事件的名字本身带有前缀on
,那么绑定的表达式可能是on-onEvent
- 坚持命名事件时,不要带前缀
- 把表现层逻辑放到组件类里 (风格 05-17)
- 坚持把表现层逻辑放进组件类中,而不要放在模板里
为何?逻辑应该只出现在一个地方(组件类里)而不应分散在两个地方
为何?将组件的表现层逻辑放到组件类而非模板里,可以增强测试性、维护性和重复使用性
- 坚持把表现层逻辑放进组件类中,而不要放在模板里
-
指令
- 使用指令来增强已有元素 (风格 06-01)
- 坚持当你需要有表现层逻辑,但没有模板时,使用属性型指令
为何?属性型指令没有模板
为何?一个元素可以使用多个属性型指令
- 坚持当你需要有表现层逻辑,但没有模板时,使用属性型指令
-
HostListener 和 HostBinding 装饰器 vs. 组件元数据 host (风格 06-03)
-
考虑优先使用
@HostListener
和@HostBinding
,而不是@Directive
和@Component
装饰器的host
属性 -
坚持让你的选择保持一致
为何?对于关联到@HostBinding
的属性或关联到@HostListener
的方法,要修改时,只需在指令类中的一个地方修改。 如果使用元数据属性host
,你就得在组件类中修改属性声明的同时修改相关的元数据
-
-
服务
- 服务总是单例的 (风格 07-01)
- 坚持在同一个注入器内,把服务当做单例使用。用它们来共享数据和功能
为何?服务是在特性范围或应用内共享方法的理想载体
为何?服务是共享状态性内存数据的理想载体
- 坚持在同一个注入器内,把服务当做单例使用。用它们来共享数据和功能
- 单一职责 (风格 07-02)
- 坚持创建单一职责的服务,用职责封装在它的上下文中
为何?当服务有多个职责时,它很难被测试 - 坚持当服务成长到超出单一用途时,创建一个新服务
为何?当某个服务有多个职责时,每个注入它的组件或服务都会承担这些职责的全部开销
- 坚持创建单一职责的服务,用职责封装在它的上下文中
- 提供一个服务 (风格 07-03)
- 坚持在服务的
@Injectable
装饰器上指定通过应用的根注入器提供服务
- 坚持在服务的
- 使用@Injectable 类装饰器 (风格 07-04)
- 坚持当使用类型作为令牌来注入服务的依赖时,使用
@Injectable()
类装饰器,而非@Inject()
参数装饰器
- 坚持当使用类型作为令牌来注入服务的依赖时,使用
-
数据服务
- 通过服务与Web服务器通讯 (风格 08-01)
- 坚持把数据操作和与数据交互的逻辑重构到服务里;
为何?组件的职责是为视图展示或收集信息。它不应该关心如何获取数据,它只需要知道向谁请求数据。把如何获取数据的逻辑移动到数据服务里,简化了组件,让其聚焦于视图
为何?在测试使用数据服务的组件时,可以让数据调用更容易被测试(模拟或者真实) - 坚持让数据服务来负责 XHR 调用、本地储存、内存储存或者其它数据操作
为何?数据管理的详情,比如头信息、方法、缓存、错误处理和重试逻辑,不是组件和其它的数据消费者应该关心的事情 - 数据服务应该封装这些细节。这样,在服务内部修改细节,就不会影响到它的消费者。并且更容易通过实现一个模拟服务来对消费者进行测试
- 坚持把数据操作和与数据交互的逻辑重构到服务里;
-
生命周期钩子:使用生命周期钩子来介入到 Angular 暴露的重要事件里
- 实现生命周期钩子接口 (风格 09-01)
- 坚持实现生命周期钩子接口
-
附录:有用的 Angular 工具和小提示
- codelyzer (风格 A-01)
- 坚持使用 codelyzer 来实施本指南
- 考虑调整 codelyzer 的规则来满足你的需求
- 文档模板和代码片段 (风格A-02)
- 坚持使用文件模板或代码片段来帮助实现一致的风格和模式
- codelyzer (风格 A-01)
以上内容来自Angular中文网-文档-风格指南。