AngularJS 风格指南 (ES2015)

# AngularJS 风格指南 (ES2015)

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


目录

  1. 模块化体系结构

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

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

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

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

模块化体系结构

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

模块概述

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

Back to top

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

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() 支持的属性

PropertySupport
bindingsYes, use '@', '<', '&' only
controllerYes
controllerAsYes, default is $ctrl
requireYes (new Object syntax)
templateYes
templateUrlYes
transcludeYes

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()所做的大多数事情(指令是原始组件),我建议将指令对象定义限制为仅限于这些属性,以避免错误地使用指令:

PropertyUse it?Why
bindToControllerNoUse bindings in components
compileYesFor pre-compile DOM manipulation/events
controllerNoUse a component
controllerAsNoUse a component
link functionsYesFor pre/post DOM manipulation/events
multiElementYesSee docs
priorityYesSee docs
requireNoUse a component
restrictYesDefines directive usage, always use 'A'
scopeNoUse a component
templateNoUse a component
templateNamespaceYes (if you must)See docs
templateUrlNoUse a component
transcludeNoUse 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值