# AngularJS 风格指南 (ES2015)
AngularJs1.6.x的最佳实践.涵盖体系结构,文件结构,组件,单向数据流和生命周期。
目录
模块化体系结构
Angular应用程序中的每个模块都应该是组件模块。组件模块用来封装逻辑、模板、路由和子组件。
模块概述
模块的设计直接反应了我们的文件结构,从而保持了可维护性和可预测性。我们最好有三个高级模块: root、component和common。root模块是用来启动我们应用和相应模板的基础模块,root模块中导入component和common模块作为依赖模块。component和common模块则负责引入Low-level modules,Low-level modules通常包含可重用的组件,控制器,服务,指令,过滤器和测试.
Root 模块
root模块中定义整个应用的根组件,根组件中通常包含路由组件,比如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> ` };
// 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;
使用 .component('app',AppComponent)
方法在root模块中注册根组件,你可能注意到样式也被引入到了根组件,我们将在后面的章节介绍这一点.
Components 模块
所有的可重用组件应该注册在component模块上。上面例子中展示了我们是如何导入 ComponentsModule
并将它们注册在root模块中。这样做可以轻松的将component模块移动到任何其他应用程序中,因为root模块与component模块是分离的。
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;
Common module
所有我们不希望用在其他应用中的组件应该注册在common模块上。比如布局、导航和页脚之类的内容。
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;
Low-level modules
Low-level module是包含独立功能的组件模块,将会被导入到像component module或者是 common module这样的higher-level module中,下面是一个例子。一定要记住在每个export
的模块最后添加.name
。你可能注意到路由定义也在这个模块中,我们将在本指南后面的章节中介绍这一点。
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-grid.*.js
。使用 *.component.js
标示组件,使用 *.module.js
标示模块
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
and app/
,其余的模块在app/
中
组件
组件概述
组件是带有控制器的模板,组件可以通过 bindings
定义数据或是事件的输入和输出,你可以将组件视为完整的代码段,而不仅仅是 .component()
定义的对象,让我们探讨一些关于组件的最佳实践和建议,然后深入了解如何通过有状态的、无状态的和路由组件的概念来构造它们。
支持的属性
Property | Support |
---|---|
bindings | Yes,use '@' ,'<' ,'&' only |
controller | Yes |
controllerAs | Yes,default is $ctrl |
require | Yes (new Object Syntax) |
template | Yes |
templateUrl | Yes |
transclude | Yes |
控制器
控制器应该只和组件一起使用,如果你感觉需要一个控制器,可能你需要的是一个管理这个特定行为的组件。
这是一些使用 Class
定义controllers的建议:
- 使用
controller: class TodoComponent {...}
这种写法以应对未来向Angular迁移 - 始终使用
constructor
来进行依赖注入 - 使用 babel-plugin-angularjs-annotate 的
'ngInject';
语法 - 如果需要访问词法作用域,请使用箭头函数
- 除了箭头函数
let ctrl = this;
也是可以接受的 - 将所有的公共函数绑定到
class{}
上 - 适当的使用
$onInit
,$onChanges
,$postLink
and$onDestroy
等生命周期函数 - Note:
$onChanges
is called before$onInit
,see resources section for articles detailing this in more depth - Use
require
alongside$onInit
to reference any inherited logic - Do not override the default
$ctrl
alias for thecontrollerAs
Syntax,therefore do not usecontrollerAs
anywhere
单向数据流和事件
AngularJS 1.5中引入了单向数据流,这重新定义了组件之间的通信.
这里有一些使用单向数据流的建议:
- 在组件中接受数据时,总是使用单向数据绑定语法
'<'
- 任何时候都不要在使用双向绑定语法
'='
- 使用
bindings
绑定传入数据时,在$onChanges
生命周期函数中深复制传入的对象以解除父组件的引用 - 在父组件的方法中使用
$event
作为函数的参数(查看下面有状态组件的例子$ctrl.addTodo($event)
) - 从无状态组件的方法中传递回
$event: {}
对象(查看下面的无状态组件例子this.onAddTodo
)
<!-- * Bonus: Use an EventEmitter
wrapper with .value()
to mirror Angular,avoids manual $event
Object creation
- Why? This mirrors Angular and keeps consistency inside every component. It also makes state predictable. -->
有状态组件
让我们定义一个有状态组件
<!-- * Renders child components that mutate state
- Also referred to as smart/container components -->
一个包括low-level module定义的有状态组件的例子(为了简洁省略了一些代码)
/* ----- 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>
组件
无状态组件
让我们第一我们所谓的无状态组件:
<!-- * Mutates state,passes data back up on-demand (such as a click or submit event) -->
- 不要关心数据来自哪里 - 它是无状态的
- 是高度可重用的组件
- 也被称为展示型组件
一个无状态组件的例子( <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
文件 - 使用路由组件来定义他们自己的路由逻辑
- 组件数据的输入是通过路由的
resolve
块
在这个例子中,我们将使用路由定义和 bindings
重构 <todo>
组件.我们将其视为路由组件,因为它本质上是一个"视图"
/* ----- todo/todo.component.js ----- */ import templateUrl from './todo.html'; export const TodoComponent = { bindings: { todoData: '<' },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
和很多其他特性。在基于组件的应用中应该注意他们的使用。指令不应再声明模板和控制器,或者通过绑定接收数据。指令应该只是用来和DOM互交。简单来说,如果你需要操作DOM,那就写一个指令,然后在组件的模板中使用它。如果您需要大量的DOM操作,还可以考虑$ postLink
生命周期钩子,但是,这不是让你将所有DOM操作迁移到这个函数中。
这有一些使用指令的建议:
- 不要在使用 templates,scope,bindToController or controllers
- 指令总是使用
restrict: 'A'
- 必要时使用编译和链接函数
- 记住在
$scope.$on('$destroy',fn);
中销毁和解除绑定事件处理程序
推荐的属性
由于指令支持.component()
所做的大多数事情(指令是原始组件),我建议将指令对象定义限制为仅限于这些属性,以避免错误地使用指令:
Property | Use it? | Why |
---|---|---|
bindToController | No | Use bindings in components |
compile | Yes | For pre-compile DOM manipulation/events |
controller | No | Use a component |
controllerAs | No | Use a component |
link functions | Yes | For pre/post DOM manipulation/events |
multiElement | Yes | See docs |
priority | Yes | See docs |
require | No | Use a component |
restrict | Yes | Defines directive usage,always use 'A' |
scope | No | Use a component |
template | No | Use a component |
templateNamespace | Yes (if you must) | See docs |
templateUrl | No | Use a component |
transclude | No | Use a component |
Constants or Classes
在ES2015中有几种方式使用指令,要么通过箭头函数,要么使用 Class
,选择你认为合适的。
下面是一个使用箭头函数的例子:
/* ----- 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',TodoComponent) .directive('todoAutofocus',TodoAutoFocus) .name;
或者使用 ES2015 Class
创建一个对象(注意在注册指令时手动调用 "new TodoAutoFocus")
/* ----- todo/todo-autofocus.directive.js ----- */ import angular from 'angular'; export class TodoAutoFocus { constructor($timeout) { 'ngInject'; this.restrict = 'A'; this.$timeout = $timeout; } link($scope,$attrs) { $scope.$watch($attrs.todoAutofocus,oldValue) => { if (!newValue) { return; } this.$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',($timeout) => new TodoAutoFocus($timeout)) .name;
服务
服务概述
服务本质上是业务逻辑的容器。服务包含其他内置或外部服务例如$http
,我们可以在应用的其他位置注入到控制器中。我们有两种使用服务的方式,.service()
和 .factory()
。如果要使用ES2015的 Class
,我们应该使用.service()
,并且使用$inject
自动完成依赖注入。
Classes for Service
这是一个使用 ES2015 Class
的例子:
/* ----- 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 { TodoComponent } from './todo.component'; import { TodoService } from './todo.service'; import './todo.scss'; export const TodoModule = angular .module('todo',TodoService) .name;
样式
使用 Webpack 我们可以在*.module.js
中用 import
导入我们的 .scss
文件,这样做可以让我们的组件在功能和样式上都是隔离的。
如果你有一些变量或全局使用的样式,比如表单输入元素,那么这些文件仍然应该放在根目录scss
文件夹中。 例如 SCSS / _forms.scss
。这些全局样式可以像通常那样被 @importe
.
ES2015 and Tooling
ES2015
- 使用Babel编译你写的ES2015+代码
- 考虑使用TypeScript
Tooling
- 如果想支持组件路由,那么使用
ui-router
latest alpha - 使用 Webpack 编译你的ES2015+代码和样式
- 使用webpack的
ngtemplate-loader
- 使用babel的
babel-plugin-angularjs-annotate
状态管理
考虑使用redux管理你应用的状态.