AngularJS 风格指南 (ES2015)

前端之家收集整理的这篇文章主要介绍了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. @L_403_19@
  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() 支持属性

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: '<'
  },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',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;

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',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

猜你在找的Angularjs相关文章