# 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治理你运用的状况.