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

相關文章
相關標籤/搜索