使用 ES2015 開發 Angular1.x 應用指南

圖片描述

關鍵詞 架構, 文件結構, 組件, 單向數據流以及最佳實踐html

來自 @toddmotto 團隊的編碼指南webpack

Angular 的編碼風格以及架構已經使用ES2015進行重寫,這些在Angular 1.5+的變化能夠更好幫助您的更好的升級到Angular2.。
這份指南包括了新的單向數據流,事件委託,組件架構和組件路由。git

老版本的指南你能夠在這裏找到, 在這裏你能看到最新的.angularjs

模塊架構

Angular 中的每個模塊都是一個模塊組件。一個模塊組件囊括了邏輯,模版,路由和子組件。es6

Module 基本概念

在模塊的設計直接反映到咱們的文件夾結構,從而保證咱們項目的可維護性和可預測性。
咱們最好應該有三個高層次的模塊:根,組件和經常使用模塊。根模塊定義了用於啓動App和相應的模板的基本架子。
而後,咱們導入須要依賴的組件和通用模塊。組件和通用模塊而後須要低級別的組件模塊,其中包含咱們的組件,控制器,服務,指令,過濾器和給可重複使用的功能進行測試。github

根模塊

根模塊會啓動一個根組件,整個組件主要定義了整個應用的基本的元素和路由出口,例如使用ui-viewui-routerweb

// app.component.js
const AppComponent = {
  template: `
    <header>
        Hello world
    </header>
    <div>
        <div ui-view></div>
    </div>
    <footer>
        Copyright MyApp 2016.
    </footer>
  `
};

export default AppComponent;

咱們導入AppComponent而且使用.component("app",AppComponent)完成註冊即表示一個根模塊建立完成。
更進一步咱們會導入一些子模塊(組件和通用模塊)用於引入相關的組件。typescript

// app.js
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import AppComponent from './app.component';
import Components from './components/components';
import Common from './common/common';

const root = angular
  .module('app', [
    Components,
    Common,
    uiRouter
  ])
  .component('app', AppComponent);

export default root;

組件模塊

一個組件模塊就是引用全部課重複使用的組件容器。上面咱們能夠了解咱們如何導入組件和將它們注入到根模塊,
這樣我麼能夠有一個地方導入全部應用程序須要的組件。
咱們要求這些模塊從全部其它模塊分離出來,這樣這些模塊能夠應用到其它的應用程序中。redux

import angular from 'angular';
import Calendar from './calendar';
import Events from './events';

const components = angular
  .module('app.components', [
    Calendar,
    Events
  ])
  .name;

export default components;

公共模塊

公共模塊爲全部的應用提供一些特殊組件的引用,咱們不但願它可以在另外一個應用程序中使用。好比佈局,導航和頁腳。
前面咱們已經知道如何導入Common並將其注入到根模塊,而這裏就是咱們導入全部通用組件的地方。後端

import angular from 'angular';
import Nav from './nav';
import Footer from './footer';

const common = angular
  .module('app.common', [
    Nav,
    Footer
  ])
  .name;

export default common;

低級別的模塊

低層次的模塊是一些獨立的組件,它們包含邏輯和功能。這些將分別定義成模塊,被引入到較高層次模塊中,
好比一個組件或通用模塊。必定要記住每次建立一個新的模塊時(並不是引用),記得在export中添加後綴。你會注意到路由定義也是在這裏,咱們將在隨後的部分講到它。

import angular from 'angular';
import uiRouter from 'angular-ui-router';
import CalendarComponent from './calendar.component';

const calendar = angular
  .module('calendar', [
    uiRouter
  ])
  .component('calendar', CalendarComponent)
  .config(($stateProvider, $urlRouterProvider) => {
    $stateProvider
      .state('calendar', {
        url: '/calendar',
        component: 'calendar'
      });
    $urlRouterProvider.otherwise('/');
  })
  .name;

export default calendar;

文件命名規範

使用小寫並保持命名的簡介, 好比使用組件名稱時, e.g. calendar.*.js*, calendar-grid.*.js - 將名稱放到中間. 使用 index.js 做爲模塊的定義文件 ,這樣你就能夠直接經過目錄引入了。

index.js
calendar.controller.js
calendar.component.js
calendar.service.js
calendar.directive.js
calendar.filter.js
calendar.spec.js

返回目錄

易於擴展的文件結構

文件目錄結構實際上十分重要,它有利於咱們更好的擴展和預測。下面的例子展現了模塊組件的基本架構。

├── app/
│   ├── components/
│   │  ├── calendar/
│   │  │  ├── index.js
│   │  │  ├── calendar.controller.js
│   │  │  ├── calendar.component.js
│   │  │  ├── calendar.service.js
│   │  │  ├── calendar.spec.js
│   │  │  └── calendar-grid/
│   │  │     ├── index.js
│   │  │     ├── calendar-grid.controller.js
│   │  │     ├── calendar-grid.component.js
│   │  │     ├── calendar-grid.directive.js
│   │  │     ├── calendar-grid.filter.js
│   │  │     └── calendar-grid.spec.js
│   │  └── events/
│   │     ├── index.js
│   │     ├── events.controller.js
│   │     ├── events.component.js
│   │     ├── events.directive.js
│   │     ├── events.service.js
│   │     ├── events.spec.js
│   │     └── events-signup/
│   │        ├── index.js
│   │        ├── events-signup.controller.js
│   │        ├── events-signup.component.js
│   │        ├── events-signup.service.js
│   │        └── events-signup.spec.js
│   ├── common/
│   │  ├── nav/
│   │  │     ├── index.js
│   │  │     ├── nav.controller.js
│   │  │     ├── nav.component.js
│   │  │     ├── nav.service.js
│   │  │     └── nav.spec.js
│   │  └── footer/
│   │        ├── index.js
│   │        ├── footer.controller.js
│   │        ├── footer.component.js
│   │        ├── footer.service.js
│   │        └── footer.spec.js
│   ├── app.js
│   └── app.component.js
└── index.html

頂級目錄 僅僅包含了 index.html 以及 app/, 而在app/目錄中則包含了咱們要用到的組件,公共模塊,以及低級別的模塊。

組件

組件的基本概念

組件實際上就是帶有控制器的模板。他們即不是指令,也不該該使用組件代替指令,除非你正在用控制器升級「模板指令」,
組件還包含數據事件的輸入與輸出,生命週期鉤子和使用單向數據流以及從父組件上獲取數據的事件對象。
從父組件獲取數據備份。這些都是在Angular 1.5及以上推出的新標準。
咱們建立的一切模板,控制器均可能是一個組件,它們多是是有狀態的,無狀態或路由組件。
你能夠把一個「部件」做爲一個完整的一段代碼,而不只僅是.component()定義的對象。
讓咱們來探討一些組件最佳實踐和建議,而後你應該能夠明白如何組織他們。

支持的屬性

下面是一些.component()你可能會使用到的屬性 :

Property Support
bindings Yes, 僅僅使用 '@', '<', '&'
controller Yes
controllerAs Yes, 默認 $ctrl
require Yes (new Object syntax)
template Yes
templateUrl Yes
transclude Yes

控制器

控制器應該只與組件一塊兒使用。若是你以爲你須要一個控制器,你真正須要的多是一個無狀態的組件來管理特定的行爲。

這裏有一些使用Class構建controller的建議:

  • 始終使用 constructor 用於依賴注入;

  • 不要直接導出 Class,導出它的名稱,並容許$inject;

  • 若是你需訪問到 scope 裏的語法,使用箭頭函數;

  • 另外關於箭頭 函數, let ctrl = this; 也是能夠接受的,固然這更取決於使用場景;

  • 綁定到全部公共函數到Class上;

  • 適當的利用生命週期的一些鉤子, $onInit, $onChanges, $postLink 以及$onDestroy

    • 注意: $onChanges$onInit以後調用的, 這裏 擴展閱讀 有更深一步的講解。

  • $onInit使用require 以便引用繼承的邏輯;

  • 不要覆蓋默認 $ctrl 使用controllerAs 起的別名, 固然也不要在別的地方使用 controllerAs

One-way dataflow and Events

單向數據流已經在Angular1.5中引入了,而且從新定義了組件之間的通訊。

關於單向數據流的一些小建議:

  • 在組件接受數據,始終使用 單向數據綁定符號'<'

  • 不要再使用 '='雙向的數據綁定的語法

  • 擁有綁定的組件應該使用$ onChanges克隆單向綁定的數據阻止對象經過引用傳遞和更新原數據

  • 使用 $event 做爲一個父級方法中的的一個函數參數(參見有狀態的例子 $ctrl.addTodo($event))

  • 傳遞一個$event: {} 從無狀態的組件中進行備份(參見無狀態的例子 this.onAddTodo).

    • Bonus: 使用 包裹 .value()EventEmitter 以便遷到Angular2 , 避免手動建立一個 $event

  • 爲何? 這和Angular2相似而且保持組件的一致性.而且可讓狀態可預測。

有狀態的組件

什麼是「有狀態的組件」

  • 獲取狀態,經過服務與後端API通訊

  • 不直接發生狀態變化

  • 渲染髮生狀態變化的子組件

  • 做爲一個組件容器的引用

下面的是一個狀態組件案例,它和一個低級別的模塊組件共同完成(這只是演示,爲了精簡省略的一些代碼)

/* ----- todo/todo.component.js ----- */
import controller from './todo.controller';

const TodoComponent = {
  controller,
  template: `
    <div class="todo">
      <todo-form 
        todo="$ctrl.newTodo"
        on-add-todo="$ctrl.addTodo($event);">
      <todo-list 
        todos="$ctrl.todos"></todo-list>
    </div>
  `
};

export default TodoComponent;

/* ----- todo/todo.controller.js ----- */
class TodoController {
  constructor(TodoService) {
    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
    };
  }
}

TodoController.$inject = ['TodoService'];

export default TodoController;

/* ----- todo/index.js ----- */
import angular from 'angular';
import TodoComponent from './todo.component';

const todo = angular
  .module('todo', [])
  .component('todo', TodoComponent)
  .name;

export default todo;

這個例子顯示了一個有狀態的組件,在控制器哪經過服務獲取狀態,而後再將它傳遞給無狀態的子組件。注意這裏並無在模版使用指令好比ng-repeat以及其餘指令,相反,數據和功能委託到<todo-form> <todo-list>這兩個無狀態的組件。

無狀態的組件

什麼是無狀態的組件

  • 使用bindings: {} 定義了輸入和輸出;

  • 數據經過屬性綁定進入到組件內

  • 數據經過事件離開組件

  • 狀態變化,會將數據進行備份 (好比觸發點擊和提交事件)

  • 並不須要關心的數據來自哪裏

  • 可高度重複利用的組件

  • 也被稱做無聲活着表面組件

下面是一個無狀態組件的例子 (咱們使用<todo-form> 做爲例子) (僅僅用於演示,省略了部分代碼):

/* ----- todo/todo-form/todo-form.component.js ----- */
import controller from './todo-form.controller';

const TodoFormComponent = {
  bindings: {
    todo: '<',
    onAddTodo: '&'
  },
  controller,
  template: `
    <form name="todoForm" ng-submit="$ctrl.onSubmit();">
      <input type="text" ng-model="$ctrl.todo.title">
      <button type="submit">Submit</button>
    </form>
  `
};

export default TodoFormComponent;

/* ----- todo/todo-form/todo-form.controller.js ----- */
class TodoFormController {
  constructor(EventEmitter) {}
  $onChanges(changes) {
    if (changes.todo) {
      this.todo = Object.assign({}, this.todo);
    }
  }
  onSubmit() {
    if (!this.todo.title) return;
    // with EventEmitter wrapper
    this.onAddTodo(
      EventEmitter({
        todo: this.todo
      });
    );
    // without EventEmitter wrapper
    this.onAddTodo({
      $event: {
        todo: this.todo
      }
    });
  }
}

TodoFormController.$inject = ['EventEmitter'];

export default TodoFormController;

/* ----- todo/todo-form/index.js ----- */
import angular from 'angular';
import TodoFormComponent from './todo-form.component';

const todoForm = angular
  .module('todo')
  .component('todo', TodoFormComponent)
  .value('EventEmitter', payload => ({ $event: payload});

export default todoForm;

請注意<todo-form>組件不獲取狀態,它只是接收,它經過控制器的邏輯去改變一個對象而後經過綁定的屬性將改變後的值傳回給父組件。
在這個例子中,$onChanges週期鉤子 產生一個this.todo的對象克隆並從新分配它,這意味着原數據不受影響,直到咱們提交表單,沿着單向數據流的新的綁定語法'<' 。

路由組件

什麼是路由組件

  • 它本質上是個有狀態的組件,具有路由定義

  • 沒有router.js 文件
    *咱們使用路由組件去定義它本身的路由邏輯

*數據流入到組件是經過路由分解得到 (固然在控制器中咱們經過服務得到)

在這個例子中,咱們將利用現有<TODO>組件,咱們會重構它,使用路由定義和以及組件上的數據綁定接收數據(在這裏咱們咱們是經過ui-router產生的reslove,這個例子todoData直接映射了數據綁定)。咱們把它看做一個路由組件,由於它本質上是一個"view":

/* ----- todo/todo.component.js ----- */
import controller from './todo.controller';

const TodoComponent = {
  bindings: {
    todoData: '<'
  },
  controller,
  template: `
    <div class="todo">
      <todo-form 
        todo="$ctrl.newTodo"
        on-add-todo="$ctrl.addTodo($event);">
      <todo-list 
        todos="$ctrl.todos"></todo-list>
    </div>
  `
};

export default TodoComponent;

/* ----- todo/todo.controller.js ----- */
class TodoController {
  constructor() {}
  $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
    };
  }
}

export default TodoController;

/* ----- todo/todo.service.js ----- */
class TodoService {
  constructor($http) {
    this.$http = $http;
  }
  getTodos() {
    return this.$http.get('/api/todos').then(response => response.data);
  }
}

TodoService.$inject = ['$http'];

export default TodoService;

/* ----- todo/index.js ----- */
import angular from 'angular';
import TodoComponent from './todo.component';
import TodoService from './todo.service';

const todo = angular
  .module('todo', [])
  .component('todo', TodoComponent)
  .service('TodoService', TodoService)
  .config(($stateProvider, $urlRouterProvider) => {
    $stateProvider
      .state('todos', {
        url: '/todos',
        component: 'todo',
        resolve: {
          todoData: TodoService => TodoService.getTodos();
        }
      });
    $urlRouterProvider.otherwise('/');
  })
  .name;

export default todo;

指令

基本概念

指令給予了咱們的模板,scope ,與控制器綁定,連接和許多其餘的事情。這些的使用使咱們慎重考慮 .component()的存在。指令不該在聲明模板和控制器了,或者經過綁定接收數據。指令應該僅僅是爲了裝飾DOM使用。這樣,就意味着擴展示有的HTML - 若是用.component()建立。簡而言之,若是你須要自定義DOM事件/ API和邏輯,使用一個指令並將其綁定到一個組件內的模板。若是你須要的足夠的數量的 DOM變化,postLink生命週期鉤子值得考慮,可是這並非遷移全部的的DOM操做。你能夠給一個無需Angular的地方使用directive

使用指令的小建議:

  • 不要使用模板 ,scope,控制器

  • 一直設置 restrict: 'A'

  • 在須要的地方使用 compile and link

  • 記得 $scope.$on('$destroy', fn) 進行銷燬和事件解除;

返回目錄

推薦的屬性

因爲指令支持了大多數 .component() 的語法 (模板指令就是最原始的組件), 建議限制指令中的的 Object,以及避免使用錯誤的指令方法。

Property Use it? Why
bindToController No 在組件中使用 bindings
compile Yes 預編譯 DOM 操做/事件
controller No 使用一個組件
controllerAs No 使用一個組件
link functions Yes 對於 DOM操做/事件 的先後
multiElement Yes 文檔
priority Yes 文檔
require No 使用一個組件
restrict Yes 定義一個組件並使用 A
scope No 使用一個組件
template No 使用一個組件
templateNamespace Yes (if you must) See docs
templateUrl No 使用一個組件
transclude No 使用一個組件

常量 和 類

下面有幾個使用es2015和指令的方法,不管是帶有箭頭函數,更容易的操做,或使用ES2015Class。記住選擇最適合本身或者團隊的方法,而且記住 Angular 2中使用 Class.

下面是一個恆在箭頭函數的表達式()=>({})使用常量的例子,它返回一個對象面(注意裏面與.directive的使用差別()):

/* ----- todo/todo-autofocus.directive.js ----- */
import angular from 'angular';

const TodoAutoFocus = ($timeout) => ({
  restrict: 'A',
  link($scope, $element, $attrs) {
    $scope.$watch($attrs.todoAutofocus, (newValue, oldValue) => {
      if (!newValue) {
        return;
      }
      $timeout(() => $element[0].focus());
    });
  }
});

TodoAutoFocus.$inject = ['$timeout'];

export default TodoAutoFocus;

/* ----- todo/index.js ----- */
import angular from 'angular';
import TodoComponent from './todo.component';
import TodoAutofocus from './todo-autofocus.directive';

const todo = angular
  .module('todo', [])
  .component('todo', TodoComponent)
  .directive('todoAutofocus', TodoAutoFocus)
  .name;

export default todo;

或者用ES2015 Class(注意在註冊指令時手動調用 new TodoAutoFocus)來建立對象:

/* ----- todo/todo-autofocus.directive.js ----- */
import angular from 'angular';

class TodoAutoFocus {
  constructor() {
    this.restrict = 'A';
  }
  link($scope, $element, $attrs) {
    $scope.$watch($attrs.todoAutofocus, (newValue, oldValue) => {
      if (!newValue) {
        return;
      }
      $timeout(() => $element[0].focus());
    });
  }
}

TodoAutoFocus.$inject = ['$timeout'];

export default TodoAutoFocus;

/* ----- todo/index.js ----- */
import angular from 'angular';
import TodoComponent from './todo.component';
import TodoAutofocus from './todo-autofocus.directive';

const todo = angular
  .module('todo', [])
  .component('todo', TodoComponent)
  .directive('todoAutofocus', () => new TodoAutoFocus)
  .name;

export default todo;

服務

基本理論

服務本質上是包含業務邏輯的容器,而咱們的組件不該該直接進行請求。服務包含其它內置或外部服務,如$http,咱們能夠隨時隨地的在應用程序注入到組件控制器。咱們在開發服務有兩種方式,.service() 以及 .factory()。使用ES2015Class,咱們應該只使用.service(),經過$inject完成依賴注入。

構建服務Class

下面的 <todo> 就是使用 ES2015 Class:

/* ----- todo/todo.service.js ----- */
class TodoService {
  constructor($http) {
    this.$http = $http;
  }
  getTodos() {
    return this.$http.get('/api/todos').then(response => response.data);
  }
}

TodoService.$inject = ['$http'];

export default TodoService;

/* ----- todo/index.js ----- */
import angular from 'angular';
import TodoComponent from './todo.component';
import TodoService from './todo.service';

const todo = angular
  .module('todo', [])
  .component('todo', TodoComponent)
  .service('TodoService', TodoService)
  .name;

export default todo;

返回目錄

ES2015 以及相關工具

ES2015
  • 使用 Babel 將ES2015進行轉換爲當前瀏覽器所支持的代碼

  • 考慮使用 TypeScript 讓你更好的遷移到Angular2

工具
  • 使用 ui-router latest alpha (查看 Readme) 若是你但願支持路由鑽

    • 你可能會在 template: '<component>' 以及 不須要 bindings中遇到一些挫折

  • 考慮使用 Webpack 來編譯es2016的代碼

  • 使用 ngAnnotate 自動完成 $inject 屬性注入

  • 如何使用ngAnnotate with ES6

狀態管理

考慮使用 Redux 用於 數據管理.

資源

文檔

關於Angular API Angular documentation.

Github: https://github.com/JackPu/angular-styleguide/blob/master/i18n/zh-cn.md
感謝 @toddmotto 許可

相關文章
相關標籤/搜索