angularjs styleguide (ES2015)
使用AngularJS 1.6最佳实践最新。架构,文件结构,组件,单向数据流,生命周期钩。
@toddmotto的团队的明智风格指导
该体系结构和StyleGuide已从ES2015进行了重写,AngularJS 1.5+的变化将来将您的应用程序升级为Angular。本指南包括单程数据流,事件授权,组件体系结构和组件路由的新最佳实践。
您可以在这里找到旧的styleguide,以及在这里新的推理。
加入Ultimate Angularjs学习体验,以完全掌握初学者和高级AngularJS功能,以构建快速且扩展的现实世界应用程序。
目录
- 模块化体系结构
- 理论
- 根模块
- 组件模块
- 通用模块
- 低级模块
- 文件命名约定
- 可扩展的文件结构
- 成分
- 理论
- 支持的属性
- 控制器
- 单向数据流和事件
- 状态组件
- 无状态组件
- 路由组件
- 指令
- 理论
- 推荐的属性
- 常数或类
- 服务
- 理论
- 服务课程
- 样式
- ES2015和工具
- 国家管理
- 资源
- 文档
- 贡献
模块化体系结构
Angular应用中的每个模块都是模块组件。模块组件是封装逻辑,模板,路由和子女组件的该模块的根定义。
模块理论
模块中的设计直接映射到我们的文件夹结构,这使事物可维护和可预测。理想情况下,我们应该有三个高级模块:根,组件和常见。根模块定义了引导我们的应用程序的基本模块和相应的模板。然后,我们将组件和公共模块导入根模块中,以包括我们的依赖项。然后,组件和通用模块需要较低级别的组件模块,该模块包含每个可重复使用功能的组件,控制器,服务,指令,过滤器和测试。
回到顶部
根模块
根模块以一个根组件开头,该根组件为整个应用程序定义了基本元素,并使用ui-router的ui-view显示了路由出口定义的示例。
// app.component.js export const AppComponent = { template : ` <header> Hello world </header> <div> <div ui-view></div> </div> <footer> Copyright MyApp 2016. </footer> ` } ;
然后创建一个根模块,并在AppComponent导入并在.component(\'app\', AppComponent)中注册并注册。提出的进一步的子模块(组件和公共模块)包括与应用程序相关的所有组件。您会注意到此处的样式也正在进口,我们将在本指南的稍后章节中进行此操作。
// app.module.js import angular from \'angular\' ; import uiRouter from \'angular-ui-router\' ; import { AppComponent } from \'./app.component\' ; import { ComponentsModule } from \'./components/components.module\' ; import { CommonModule } from \'./common/common.module\' ; import \'./app.scss\' ; export const AppModule = angular . module ( \'app\' , [ ComponentsModule , CommonModule , uiRouter ] ) . component ( \'app\' , AppComponent ) . name ;
回到顶部
组件模块
组件模块是所有可重用组件的容器参考。请参阅上面的内容,我们如何导入ComponentsModule并将它们注入根模块中,这为我们提供了一个单个地方,可以导入应用程序的所有组件。我们需要的这些模块与所有其他模块分离,因此可以轻松地将其移至任何其他应用程序中。
// components/components.module.js import angular from \'angular\' ; import { CalendarModule } from \'./calendar/calendar.module\' ; import { EventsModule } from \'./events/events.module\' ; export const ComponentsModule = angular . module ( \'app.components\' , [ CalendarModule , EventsModule ] ) . name ;
回到顶部
通用模块
通用模块是我们不想在另一个应用程序中使用的所有应用程序组件的容器参考。这可以是布局,导航和页脚。请参阅上面的方式,我们如何将CommonModule导入根模块中,这为我们提供了一个单个地方,可以将所有常见组件导入应用程序。
// common/common.module.js import angular from \'angular\' ; import { NavModule } from \'./nav/nav.module\' ; import { FooterModule } from \'./footer/footer.module\' ; export const CommonModule = angular . module ( \'app.common\' , [ NavModule , FooterModule ] ) . name ;
回到顶部
低级模块
低级模块是包含每个特征块逻辑的单个组件模块。这些每个人都将定义一个模块,将导入到一个高级模块,例如组件或公共模块,下面的一个示例。始终记住在创建新模块时,而不是引用一个导出时,将.name后缀添加到每个export时。您会注意到此处也存在路由定义,我们将在本指南的稍后章节中进行此操作。
// calendar/calendar.module.js import angular from \'angular\' ; import uiRouter from \'angular-ui-router\' ; import { CalendarComponent } from \'./calendar.component\' ; import \'./calendar.scss\' ; export const CalendarModule = angular . module ( \'calendar\' , [ uiRouter ] ) . component ( \'calendar\' , CalendarComponent ) . config ( ( $stateProvider , $urlRouterProvider ) => { \'ngInject\' ; $stateProvider . state ( \'calendar\' , { url : \'/calendar\' , component : \'calendar\' } ) ; $urlRouterProvider . otherwise ( \'/\' ) ; } ) . name ;
回到顶部
文件命名约定
保持简单且小写,使用组件名称,例如calendar.*.js* calendar-grid.*.js使用*.module.js作为模块定义文件,因为它使其保持冗长并与Angular保持一致。
calendar.module.js
calendar.component.js
calendar.service.js
calendar.directive.js
calendar.filter.js
calendar.spec.js
calendar.html
calendar.scss
回到顶部
可扩展的文件结构
文件结构非常重要,这描述了可扩展且可预测的结构。一个示例文件结构,以说明模块化组件体系结构。
├── app/
│ ├── components/
│ │ ├── calendar/
│ │ │ ├── calendar.module.js
│ │ │ ├── calendar.component.js
│ │ │ ├── calendar.service.js
│ │ │ ├── calendar.spec.js
│ │ │ ├── calendar.html
│ │ │ ├── calendar.scss
│ │ │ └── calendar-grid/
│ │ │ ├── calendar-grid.module.js
│ │ │ ├── calendar-grid.component.js
│ │ │ ├── calendar-grid.directive.js
│ │ │ ├── calendar-grid.filter.js
│ │ │ ├── calendar-grid.spec.js
│ │ │ ├── calendar-grid.html
│ │ │ └── calendar-grid.scss
│ │ ├── events/
│ │ │ ├── events.module.js
│ │ │ ├── events.component.js
│ │ │ ├── events.directive.js
│ │ │ ├── events.service.js
│ │ │ ├── events.spec.js
│ │ │ ├── events.html
│ │ │ ├── events.scss
│ │ │ └── events-signup/
│ │ │ ├── events-signup.module.js
│ │ │ ├── events-signup.component.js
│ │ │ ├── events-signup.service.js
│ │ │ ├── events-signup.spec.js
│ │ │ ├── events-signup.html
│ │ │ └── events-signup.scss
│ │ └── components.module.js
│ ├── common/
│ │ ├── nav/
│ │ │ ├── nav.module.js
│ │ │ ├── nav.component.js
│ │ │ ├── nav.service.js
│ │ │ ├── nav.spec.js
│ │ │ ├── nav.html
│ │ │ └── nav.scss
│ │ ├── footer/
│ │ │ ├── footer.module.js
│ │ │ ├── footer.component.js
│ │ │ ├── footer.service.js
│ │ │ ├── footer.spec.js
│ │ │ ├── footer.html
│ │ │ └── footer.scss
│ │ └── common.module.js
│ ├── app.module.js
│ ├── app.component.js
│ └── app.scss
└── index.html
高级文件夹结构仅包含index.html和app/ ,这是一个目录,其中我们所有的根,组件,常见和低级模块都可以使用每个组件的标记和样式。
回到顶部
成分
组成理论
组件本质上是带有控制器的模板。它们不是指令,除非您使用控制器升级“模板指令”,否则您也不应用组件替换指令,这些指令最适合作为组件。组件还包含定义数据和事件,生命周期钩子的输入和输出的绑定,以及使用单向数据流和事件对象的能力,将数据备份到父组件中。这些是AngularJS 1.5及更高版本中的新DefaTSO标准。我们创建的所有模板和控制器驱动的所有模板和控制器驱动可能都是一个状态,无状态或路由组件。您可以将“组件”视为完整的代码,而不仅仅是.component()定义对象。让我们探索一些组件的一些最佳实践和咨询,然后深入研究如何通过状态,无状态和路由组件概念进行构造。
回到顶部
支持的属性
这些是您可以/应该使用的.component()的支持属性:
| 财产 | 支持 |
|---|---|
| 绑定 | 是的,仅使用\'@\' , \'<\' , \'&\'
|
| 控制器 | 是的 |
| 控制器 | 是的,默认为$ctrl
|
| 要求 | 是(新对象语法) |
| 模板 | 是的 |
| TemplateUrl | 是的 |
| transclude | 是的 |
回到顶部
控制器
控制器只能与组件一起使用,从来没有其他任何地方。如果您觉得需要一个控制器,那么您真正需要的可能是管理该特定行为的无状态组件。
以下是一些用于控制器的Class的建议:
- 删除名称“控制器”,即使用
controller: class TodoComponent {...}来帮助未来的Angular迁移 - 始终将
constructor用于依赖注入目的 - 使用NG-Annotate的
\'ngInject\';$inject注释的语法 - 如果您需要访问词汇范围,请使用箭头功能
- 或者,对于箭头功能,
let ctrl = this;也可以接受,并且可能会根据用例更有意义 - 将所有公共功能直接绑定到
Class - 利用适当的生命周期钩,
$onInit,$onChanges,$postLink和$onDestroy- 注意:
$onChanges在$onInit之前被调用,请参阅资源部分,以获取更深入的详细信息
- 注意:
- 使用
require与$onInit一起参考任何继承的逻辑 - 不要覆盖
controllerAs语法的默认$ctrl别名,因此请勿在任何地方使用controllerAs
回到顶部
单向数据流和事件
在AngularJS 1.5中引入了单向数据流,并重新定义了组件通信。
以下是使用单向数据流的一些建议:
- 在接收数据的组件中,始终使用单向数据框语法
\'<\' -
不要再使用
\'=\'双向数据贴语语法 - 具有
bindings的组件应使用$onChanges克隆单向绑定数据,以通过参考并更新父级数据来破坏对象 - 在父方法中使用
$event作为函数参数(请参见$ctrl.addTodo($event)的状态示例) - 传递
$event: {}对象从无状态组件备份(请参阅this.onAddTodo的无状态示例。- 奖励:使用带有
.value()EventEmitter包装器来镜像Angular,避免手动$eventoksent创建
- 奖励:使用带有
- 为什么?这反映了角度并保持每个组件内部的一致性。这也使国家可预测。
回到顶部
状态组件
让我们定义我们称之为“状态组件”的内容。
- 获取状态,实质上通过服务与后端API通信
- 不直接突变状态
- 使儿童组件变异状态
- 也称为智能/容器组件
具有状态组件的一个示例,配有其低级模块定义(这仅用于演示,因此为简洁而省略了一些代码):
/* ----- todo/todo.component.js ----- */ import templateUrl from \'./todo.html\' ; export const TodoComponent = { templateUrl , controller : class TodoComponent { constructor ( TodoService ) { \'ngInject\' ; this . todoService = TodoService ; } $onInit ( ) { this . newTodo = { title : \'\' , selected : false } ; this . todos = [ ] ; this . todoService . getTodos ( ) . then ( response => this . todos = response ) ; } addTodo ( { todo } ) { if ( ! todo ) return ; this . todos . unshift ( todo ) ; this . newTodo = { title : \'\' , selected : false } ; } } } ; /* ----- todo/todo.html ----- */ < div class = \"todo\" > < todo-form todo = \"$ctrl.newTodo\" on-add-todo = \"$ctrl.addTodo($event);\" > </ todo-form > < todo-list todos = \"$ctrl.todos\" > </ todo-list > </ div > /* ----- todo/todo.module.js ----- */ import angular from \'angular\' ; import { TodoComponent } from \'./todo.component\' ; import \'./todo.scss\' ; export const TodoModule = angular . module ( \'todo\' , [ ] ) . component ( \'todo\' , TodoComponent ) . name ;
此示例显示了一个状态组件,该组件通过服务在控制器内部获取状态,然后将其传递到无状态的子女组件中。请注意,如何在模板中没有使用ng-repeat和朋友等指令。取而代之的是,数据和功能被委派成<todo-form>和<todo-list>无状态组件。
回到顶部
无状态组件
让我们定义所谓的“无状态组件”。
- 使用
bindings: {} - 数据通过属性绑定(输入)进入组件
- 数据通过事件(输出)离开组件
- 突变状态,按需传递数据(例如单击或提交事件)
- 不在乎数据的来源 – 它是无状态的
- 是高度可重复使用的组件
- 也称为愚蠢/呈现组件
一个无状态组件的示例(让我们使用<todo-form>作为一个示例),并使用其低级模块定义(这仅用于演示,因此为简短而省略了一些代码):
/* ----- todo/todo-form/todo-form.component.js ----- */ import templateUrl from \'./todo-form.html\' ; export const TodoFormComponent = { bindings : { todo : \'<\' , onAddTodo : \'&\' } , templateUrl , controller : class TodoFormComponent { constructor ( EventEmitter ) { \'ngInject\' ; this . EventEmitter = EventEmitter ; } $onChanges ( changes ) { if ( changes . todo ) { this . todo = Object . assign ( { } , this . todo ) ; } } onSubmit ( ) { if ( ! this . todo . title ) return ; // with EventEmitter wrapper this . onAddTodo ( this . EventEmitter ( { todo : this . todo } ) ) ; // without EventEmitter wrapper this . onAddTodo ( { $event : { todo : this . todo } } ) ; } } } ; /* ----- todo/todo-form/todo-form.html ----- */ < form name = \"todoForm\" ng-submit = \"$ctrl.onSubmit();\" > < input type = \"text\" ng-model = \"$ctrl.todo.title\" > < button type = \"submit\" > Submit </ button > </ form > /* ----- todo/todo-form/todo-form.module.js ----- */ import angular from \'angular\'; import { TodoFormComponent } from \'./todo-form.component\'; import \'./todo-form.scss\'; export const TodoFormModule = angular .module(\'todo.form\', []) .component(\'todoForm\', TodoFormComponent) .value(\'EventEmitter\', payload = > ( { $event : payload } )) .name;
请注意, <todo-form>组件如何获取状态,它只是接收它,通过与之关联的控制器逻辑来突变对象,并通过属性绑定将其传递回父组件。在此示例中, $onChanges生命周期钩使最初的this.todo绑定对象成为克隆,并重新分配它,这意味着直到我们提交表单,并与单向数据流新的绑定构想\'<\'一起提交表单,直到我们提交表单后才受到影响。
回到顶部
路由组件
让我们定义所谓的“路由组件”。
- 这本质上是一个具有路由定义的状态组件
- 没有更多
router.js。 - 我们使用路由组件来定义自己的路由逻辑
- 该组件的数据“输入”是通过路由解析完成的(可选,仍在控制器中可用服务调用)
在此示例中,我们将采用现有的<todo>组件,重构它在接收数据的组件上使用路由定义和bindings (在此处使用ui-router秘密是我们创建的resolve属性,在这种情况下, todoData直接映射到为我们绘制的bindings )。我们将其视为路由组件,因为它本质上是“视图”:
/* ----- todo/todo.component.js ----- */ import templateUrl from \'./todo.html\' ; export const TodoComponent = { bindings : { todoData : \'<\' } , templateUrl , controller : class TodoComponent { constructor ( ) { \'ngInject\' ; // Not actually needed but best practice to keep here incase dependencies needed in the future } $onInit ( ) { this . newTodo = { title : \'\' , selected : false } ; } $onChanges ( changes ) { if ( changes . todoData ) { this . todos = Object . assign ( { } , this . todoData ) ; } } addTodo ( { todo } ) { if ( ! todo ) return ; this . todos . unshift ( todo ) ; this . newTodo = { title : \'\' , selected : false } ; } } } ; /* ----- todo/todo.html ----- */ < div class = \"todo\" > < todo-form todo = \"$ctrl.newTodo\" on-add-todo = \"$ctrl.addTodo($event);\" > </ todo-form > < todo-list todos = \"$ctrl.todos\" > </ todo-list > </ div > /* ----- todo/todo.service.js ----- */ export class TodoService { constructor ( $http ) { \'ngInject\' ; this . $http = $http ; } getTodos ( ) { return this . $http . get ( \'/api/todos\' ) . then ( response => response . data ) ; } } /* ----- todo/todo.module.js ----- */ import angular from \'angular\' ; import uiRouter from \'angular-ui-router\' ; import { TodoComponent } from \'./todo.component\' ; import { TodoService } from \'./todo.service\' ; import \'./todo.scss\' ; export const TodoModule = angular . module ( \'todo\' , [ uiRouter ] ) . component ( \'todo\' , TodoComponent ) . service ( \'TodoService\' , TodoService ) . config ( ( $stateProvider , $urlRouterProvider ) => { \'ngInject\' ; $stateProvider . state ( \'todos\' , { url : \'/todos\' , component : \'todo\' , resolve : { todoData : TodoService => TodoService . getTodos ( ) } } ) ; $urlRouterProvider . otherwise ( \'/\' ) ; } ) . name ;
回到顶部
指令
指令理论
指令为我们提供template , scope绑定, bindToController , link和许多其他内容。在存在.component()的情况下,应仔细考虑这些用法。指令不应再声明模板和控制器,也不应通过绑定接收数据。指令应仅用于装饰DOM。这样,它意味着扩展使用.component()创建的现有HTML-。从简单意义上讲,如果您需要自定义的DOM事件/API和逻辑,请使用指令并将其绑定到组件内的模板。如果您需要明智的DOM操作,还需要考虑$postLink Lifecycle挂钩,但是,这不是迁移所有DOM操纵的地方,如果可以的话,请使用指令。
以下是使用指令的一些建议:
- 切勿使用模板,范围,绑定器或控制器
- 总是
restrict: \'A\' - 在必要时使用编译和链接
- 记住要在
$scope.$on(\'$destroy\', fn);
回到顶部
推荐的属性
由于指令支持.component()大部分内容(模板指令是原始组件),因此我建议将您的指令对象定义限制为仅这些属性,以免错误地使用指令:
| 财产 | 用它吗? | 为什么 |
|---|---|---|
| bindtocontroller | 不 | 在组件中使用bindings
|
| 编译 | 是的 | 用于预编译的DOM操纵/事件 |
| 控制器 | 不 | 使用组件 |
| 控制器 | 不 | 使用组件 |
| 链接功能 | 是的 | 用于预/后操作/事件 |
| 多元素 | 是的 | 见文档 |
| 优先事项 | 是的 | 见文档 |
| 要求 | 不 | 使用组件 |
| 限制 | 是的 | 定义指示用法,始终使用\'A\'
|
| 范围 | 不 | 使用组件 |
| 模板 | 不 | 使用组件 |
| TemplateNamespace | 是(如果必须) | 见文档 |
| TemplateUrl | 不 | 使用组件 |
| transclude | 不 | 使用组件 |
回到顶部
常数或类
有几种方法可以使用ES2015和指令,要么具有箭头功能和更容易的分配,要么使用ES2015 Class 。选择最适合您或您的团队的东西,请记住Angular用途Class 。
这是一个使用箭头函数常数的示例,一个expression wrapper () => ({})返回对象文字(请注意.directive()内部的用法差异()):
/* ----- todo/todo-autofocus.directive.js ----- */ import angular from \'angular\' ; export const TodoAutoFocus = ( $timeout ) => { \'ngInject\' ; return { restrict : \'A\' , link ( $scope , $element , $attrs ) { $scope . $watch ( $attrs . todoAutofocus , ( newValue , oldValue ) => { if ( ! newValue ) { return ; } $timeout ( ( ) => $element [ 0 ] . focus ( ) ) ; } ) ; } } } ; /* ----- todo/todo.module.js ----- */ import angular from \'angular\' ; import { TodoComponent } from \'./todo.component\' ; import { TodoAutofocus } from \'./todo-autofocus.directive\' ; import \'./todo.scss\' ; export const TodoModule = angular . module ( \'todo\' , [ ] ) . component ( \'todo\' , TodoComponent ) . directive ( \'todoAutofocus\' , TodoAutoFocus ) . name ;
或使用ES2015 Class (注册指令时手动调用new TodoAutoFocus )来创建对象:
下载源码
通过命令行克隆项目:
git clone https://github.com/toddmotto/angularjs-styleguide.git
