# 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()
定义的对象,让我们探讨一些关于组件的最佳实践和建议, 然后深入了解如何通过有状态的、无状态的和路由组件的概念来构造它们。
支持的属性
这些是 .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. -->
有状态组件
让我们定义一个有状态组件
- 获取状态,实质上是通过服务与后台API通信
- 不要直接变化状态
<!-- * 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>
组件
无状态组件
让我们第一我们所谓的无状态组件:
- 使用
bindings: {}
明确的定义输入和输出 - 通过属性绑定传递数据
- 数据通过事件离开组件
<!-- * 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: '<'
},
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
和很多其他特性。在基于组件的应用中应该注意他们的使用。指令不应再声明模板和控制器,或者通过绑定接收数据。指令应该只是用来和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', [])
.component('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, $element, $attrs) {
$scope.$watch($attrs.todoAutofocus, (newValue, 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', [])
.component('todo', TodoComponent)
.directive('todoAutofocus', ($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', [])
.component('todo', TodoComponent)
.service('TodoService', 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管理你应用的状态.