AngularJS 风格指南 (ES2015)

# AngularJS 风格指南 (ES2015)css

AngularJs1.6.x的最佳实践.涵盖体系结构,文件结构,组件,单向数据流和生命周期。


目录

  1. 模块化体系结构html

    1. 概述
    2. Root module
    3. Component module
    4. Common module
    5. Low-level modules
    6. 文件命名约定
    7. 可伸缩文件结构
  2. 组件webpack

    1. 概述
    2. 支持的属性
    3. 控制器
    4. 单项数据流和事件
    5. 有状态组件/容器型组件
    6. 无状态组件/展现型组件
    7. 路由组件
  3. 指令git

    1. 概述
    2. 推荐的属性
    3. Constants or Classes
  4. 服务angularjs

    1. 概述
    2. Classes for Service
  5. 样式
  6. ES2015 and Tooling
  7. 状态管理

模块化体系结构

Angular应用程序中的每一个模块都应该是组件模块。组件模块用来封装逻辑、模板、路由和子组件。github

模块概述

模块的设计直接反应了咱们的文件结构, 从而保持了可维护性和可预测性。咱们最好有三个高级模块: root、component和common。root模块是用来启动咱们应用和相应模板的基础模块,root模块中导入component和common模块做为依赖模块。component和common模块则负责引入Low-level modules,Low-level modules一般包含可重用的组件,控制器,服务,指令,过滤器和测试.web

Back to toptypescript

Root 模块

root模块中定义整个应用的根组件,根组件中一般包含路由组件,好比ui-router 中的 ui-viewredux

// 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模块中注册根组件,你可能注意到样式也被引入到了根组件,咱们将在后面的章节介绍这一点.api

Back to top

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;

Back to top

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;

Back to top

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;

Back to top

文件命名约定

保持文件名简单,而且使用小写字母,文件名使用 ' - '分割,好比 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

Back to top

可伸缩文件结构

文件结构是很是重要的,这描述了一个可伸缩和可预测的结构,下面是一个例子。

├── 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/

Back to top

组件

组件概述

组件是带有控制器的模板,组件能够经过 bindings 定义数据或是事件的输入和输出,你能够将组件视为完整的代码段,而不只仅是 .component() 定义的对象,让咱们探讨一些关于组件的最佳实践和建议, 而后深刻了解如何经过有状态的、无状态的和路由组件的概念来构造它们。

Back to top

支持的属性

这些是 .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

Back to top

控制器

控制器应该只和组件一块儿使用,若是你感受须要一个控制器,可能你须要的是一个管理这个特定行为的组件。

这是一些使用 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 the controllerAs syntax, therefore do not use controllerAs anywhere

Back to top

单向数据流和事件

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. -->

Back to top

有状态组件

让咱们定义一个有状态组件

  • 获取状态,实质上是经过服务与后台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>组件

Back to top

无状态组件

让咱们第一咱们所谓的无状态组件:

  • 使用 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 ,这意味着在提交回父组件前,父组件的数据是不受影响的,

Back to top

路由组件

让咱们定义一个路由组件。

  • 一个带有路由定义的有状态组件
  • 没有 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;

Back to top

指令

指令概述

指令有 template, scope 绑定, bindToController, link 和不少其余特性。在基于组件的应用中应该注意他们的使用。指令不该再声明模板和控制器,或者经过绑定接收数据。指令应该只是用来和DOM互交。简单来讲,若是你须要操做DOM,那就写一个指令,而后在组件的模板中使用它。若是您须要大量的DOM操做,还能够考虑$ postLink生命周期钩子,可是,这不是让你将全部DOM操做迁移到这个函数中。

这有一些使用指令的建议:

  • 不要在使用 templates, scope, bindToController or controllers
  • 指令老是使用 restrict: 'A'
  • 必要时使用编译和连接函数
  • 记住在 $scope.$on('$destroy', fn); 中销毁和解除绑定事件处理程序

Back to top

推荐的属性

因为指令支持.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

Back to top

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;

Back to top

服务

服务概述

服务本质上是业务逻辑的容器。服务包含其余内置或外部服务例如$http,咱们能够在应用的其余位置注入到控制器中。咱们有两种使用服务的方式,.service().factory()。若是要使用ES2015的 Class,咱们应该使用.service(),而且使用$inject自动完成依赖注入。

Back to top

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;

Back to top

样式

使用 Webpack 咱们能够在*.module.js 中用 import 导入咱们的 .scss 文件,这样作可让咱们的组件在功能和样式上都是隔离的。

若是你有一些变量或全局使用的样式,好比表单输入元素,那么这些文件仍然应该放在根目录scss文件夹中。 例如 SCSS / _forms.scss。这些全局样式能够像一般那样被 @importe.

Back to top

ES2015 and Tooling

ES2015
Tooling
  • 若是想支持组件路由,那么使用 ui-router latest alpha
  • 使用 Webpack 编译你的ES2015+代码和样式
  • 使用webpack的ngtemplate-loader
  • 使用babel的babel-plugin-angularjs-annotate

Back to top

状态管理

考虑使用redux管理你应用的状态.

Back to top

资源

Back to top

相关文章
相关标签/搜索