說明:參照了Angular1.x+es2015的中文翻譯,並將我的以爲不合適、不正確的地方進行了修改,歡迎批評指正。
來自@toddmotto團隊的實用編碼指南javascript
Angular 的編碼風格以及架構已經使用ES2015進行重寫,這些在AngularJS 1.5+的變化能夠更好幫助您的更好的升級到Angular2.。 這份指南包括了新的單向數據流,事件委託,組件架構和組件路由。css
加入終極的 AngularJS 學習經驗,徹底掌握初級和高級的 AngularJS 特性,構建更快,易於擴展的真實應用程序。
Angular中的每一個模塊都是一個模塊組件。模塊組件是包括了組件邏輯,模板,路由和子組件的根。es6
模塊的設計直接反映到咱們的文件夾結構,從而保證咱們項目的可維護性和可預測性。 咱們最好應該有三個高層次的模塊:根模塊,組件模塊和經常使用模塊。根模塊定義用於啓動 app 和相應的模板的基礎模塊。 而後導入咱們須要依賴的組件和通用模塊。組件和通用模塊而後須要低級別的組件模塊,包含咱們的組件,控制器,服務,指令,過濾器和給可重複使用的功能進行測試。github
回到頂部web
根模塊以一個根組件開始,它定義了整個應用程序的基本元素和路由出口,例如使用ui-router
展現ui-view
。
// app.component.ts export const AppComponent: angular.IComponentOptions = { template: ` <header> Hello world </header> <div> <div ui-view></div> </div> <footer> Copyright MyApp 2016. </footer> ` };
隨着AppComponent
導入和使用.component('app', AppComponent)
註冊,一個根模塊就建立了。進一步導入子模塊(組件和公共模塊)包括與應用程序相關的全部組件。你可能會注意到在這裏也導入了樣式,咱們將在本直男的後面章節介紹這個。
// app.ts 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'; const root = angular .module('app', [ ComponentsModule, CommonModule, uiRouter ]) .component('app', AppComponent) .name; export default root;
一個組件模塊是引用全部可複用組件的容器。在上面咱們看到如何導入Components
而且將他們注入到根模塊,這裏給了咱們一個導入全部應用程序須要的組件的地方。咱們要求這些模塊與其餘模塊都是解耦的,所以能夠很容易的移動到其餘任何應用程序中。
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;
公共模塊是引用全部爲應用程序提供的特殊組件的容器,咱們不但願它在其餘應用程序中使用。這能夠是佈局,導航和頁腳之類的東西。在上面咱們看到如何導入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;
Always remember to add the .name
suffix to each export
when creating a new module, not when referencing one. You'll noticed routing definitions also exist here, we'll come onto this in later chapters in this guide.
低級別的模塊是包含每一個功能塊邏輯的獨立組件。每一個模塊都將定義成一個能夠被導入較高級別的單獨模塊,例如一個組件或者公共模塊,以下所示。 。必定要記住每次建立一個新的模塊,而非引用的時候,記得給每一個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: angular.ui.IStateProvider, $urlRouterProvider: angular.ui.IUrlRouterProvider) => { $stateProvider .state('calendar', { url: '/calendar', component: 'calendar' }); $urlRouterProvider.otherwise('/'); }) .name;
使用小寫並保持命名的簡潔, 使用組件名稱舉例, calendar.*.ts*
, calendar-grid.*.ts
- 將文件類型的名稱放到中間。使用 index.ts
做爲模塊的定義文件,這樣就能夠經過目錄名導入模塊了。
index.ts calendar.component.ts calendar.service.ts calendar.directive.ts calendar.filter.ts calendar.spec.ts calendar.html calendar.scss
文件目錄結構很是重要,它有利於咱們更好的擴展和預測。下面的例子展現了模塊組件的基本架構。
├── app/ │ ├── components/ │ │ ├── calendar/ │ │ │ ├── index.ts │ │ │ ├── calendar.component.ts │ │ │ ├── calendar.service.ts │ │ │ ├── calendar.spec.ts │ │ │ ├── calendar.html │ │ │ ├── calendar.scss │ │ │ └── calendar-grid/ │ │ │ ├── index.ts │ │ │ ├── calendar-grid.component.ts │ │ │ ├── calendar-grid.directive.ts │ │ │ ├── calendar-grid.filter.ts │ │ │ ├── calendar-grid.spec.ts │ │ │ ├── calendar-grid.html │ │ │ └── calendar-grid.scss │ │ ├── events/ │ │ │ ├── index.ts │ │ │ ├── events.component.ts │ │ │ ├── events.directive.ts │ │ │ ├── events.service.ts │ │ │ ├── events.spec.ts │ │ │ ├── events.html │ │ │ ├── events.scss │ │ │ └── events-signup/ │ │ │ ├── index.ts │ │ │ ├── events-signup.controller.ts │ │ │ ├── events-signup.component.ts │ │ │ ├── events-signup.service.ts │ │ │ ├── events-signup.spec.ts │ │ │ ├── events-signup.html │ │ │ └── events-signup.scss │ │ └── components.module.ts │ ├── common/ │ │ ├── nav/ │ │ │ ├── index.ts │ │ │ ├── nav.component.ts │ │ │ ├── nav.service.ts │ │ │ ├── nav.spec.ts │ │ │ ├── nav.html │ │ │ └── nav.scss │ │ ├── footer/ │ │ │ ├── index.ts │ │ │ ├── footer.component.ts │ │ │ ├── footer.service.ts │ │ │ ├── footer.spec.ts │ │ │ ├── footer.html │ │ │ └── footer.scss │ │ └── index.ts │ ├── index.ts │ ├── app.component.ts │ └── app.scss └── index.html
頂級目錄僅僅包含了index.html
和app/
, app/
目錄中則包含了咱們要用到的根模塊,組件,公共模塊,以及低級別的模塊。
組件實際上就是帶有控制器的模板。他們即不是指令,也不該該使用組件代替指令,除非你正在用控制器升級「模板指令」,它是最適合做爲組件的。 組件還包含數據事件的輸入與輸出,生命週期鉤子和使用單向數據流以及從父組件上獲取數據的事件對象備份。這些都是在AngularJS 1.5及以上推出的新標準。咱們建立的全部模板和控制器均可能是一個組件,它多是是有狀態的,無狀態或者路由組件。你能夠將「組件」看做一段完整的代碼,而不只僅是.component()
定義的對象。讓咱們來探討一些組件最佳實踐和建議,而後你應該能夠明白如何經過有狀態,無狀態和路由組件的概念來組織結構。
下面是一些你可能會使用到的.component()
屬性 :
Property | Support |
---|---|
bindings | Yes, 僅僅使用 '@' , '<' , '&' |
controller | Yes |
controllerAs | Yes, 默認是$ctrl |
require | Yes (新對象語法) |
template | Yes |
templateUrl | Yes |
transclude | Yes |
控制器應該僅僅與組件一塊兒使用,而不該該是任何地方。若是你以爲你須要一個控制器,你真正須要的多是一個來管理特定行的無狀態組件。
這裏有一些使用Class
構建控制器的建議:
constructor
來依賴注入Class
,導出它的名字去容許使用$inject
註解let ctrl = this;
也是能夠接受的,固然這更取決於使用場景Class
適當的使用生命週期鉤子,$onInit
, $onChanges
, $postLink
和 $onDestroy
$onChanges
在$onInit
以前被調用,查看這裏的擴展閱讀對生命週期有進一步的理解$onInit
使用require
去引用其餘繼承的邏輯controllerAs
語法去覆蓋默認的$ctrl
別名,固然也不要再其餘地方使用controllerAs
單向數據流已經在Angular1.5中引入了,而且從新定義了組件之間的通訊。
這裏有一些使用單向數據流的建議:
<
來接收數據'='
bindings
的組件應該使用 $onChanges
克隆單向綁定數據而阻止經過引用傳遞對象,而且更新父級數據$event
做爲一個函數參數(查看有狀態組件的例子$ctrl.addTodo($event)
)從無狀態組件傳回一個 $event: {}
對象(查看無狀態組件的例子this.onAddTodo
)
.value()
包裝 EventEmitter
以便遷移到 Anuglar2,避免手動創一個 $event
對象咱們來定義下什麼叫做「有狀態組件」:
下面的是一個狀態組件案例,它和一個低級別的模塊組件共同完成(這只是演示,爲了精簡省略了一些代碼)
/* ----- todo/todo.component.ts ----- */ import { TodoController } from './todo.controller'; import { TodoService } from './todo.service'; import { TodoItem } from '../common/model/todo'; export const TodoComponent: angular.IComponentOptions = { controller: TodoController, template: ` <div class="todo"> <todo-form todo="$ctrl.newTodo" on-add-todo="$ctrl.addTodo($event);"> <todo-list todos="$ctrl.todos"></todo-list> </div> ` }; /* ----- todo/todo.controller.ts ----- */ export class TodoController { static $inject: string[] = ['TodoService']; todos: TodoItem[]; constructor(private todoService: TodoService) { } $onInit() { this.newTodo = new TodoItem('', false); this.todos = []; this.todoService.getTodos().then(response => this.todos = response); } addTodo({ todo }) { if (!todo) return; this.todos.unshift(todo); this.newTodo = new TodoItem('', false); } } /* ----- todo/index.ts ----- */ import angular from 'angular'; import { TodoComponent } from './todo.component'; export const TodoModule = angular .module('todo', []) .component('todo', TodoComponent) .name; /* ----- todo/todo.service.ts ----- */ export class TodoService { static $inject: string[] = ['$http']; constructor(private $http: angular.IHttpService) { } getTodos() { return this.$http.get('/api/todos').then(response => response.data); } } /* ----- common/model/todo.ts ----- */ export class TodoItem { constructor( public title: string, public completed: boolean) { } ) }
這個例子展現了一個有狀態的組件,在控制器經過服務獲取狀態,而後再將它傳遞給無狀態的子組件。注意這裏並無在模版中使例如如ng-repeat
和其餘指令。相反,將數據和函數代理到 <todo-form>
和 <todo-list>
這兩個無狀態的組件。
咱們來定義下什麼叫做「無狀態組件」:
bindings: {}
定義輸入輸出下面是一個無狀態組件的例子 (咱們使用 <todo-form>
做爲例子) , 使用低級別模塊定義來完成(僅僅用於演示,省略了部分代碼):
/* ----- todo/todo-form/todo-form.component.ts ----- */ import { TodoFormController } from './todo-form.controller'; export const TodoFormComponent: angular.IComponentOptions = { bindings: { todo: '<', onAddTodo: '&' }, controller: TodoFormController, template: ` <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.controller.ts ----- */ import { EventEmitter } from '../common/event-emitter'; import { Event } from '../common/event'; export class TodoFormController { static $inject = ['EventEmitter']; constructor(private eventEmitter: 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(new Event({ todo: this.todo })); } } /* ----- common/event.ts ----- */ export class Event { constructor(public $event: any){ } } /* ----- common/event-emitter.ts ----- */ import { Event } from './event'; export function EventEmitter(payload: any): Event { return new Event(payload); } /* ----- todo/todo-form/index.ts ----- */ import angular from 'angular'; import { EventEmitter } from './common/event-emitter'; import { TodoFormComponent } from './todo-form.component'; export const TodoFormModule = angular .module('todo.form', []) .component('todoForm', TodoFormComponent) .value('EventEmitter', EventEmitter) .name;
請注意 <todo-form>
組件不獲取狀態,它只是簡單的接收,它經過控制器的邏輯去改變一個對象,而後經過綁定的屬性將改變後的值傳回給父組件。 在這個例子中 $onChanges
生命週期鉤子克隆了初始的 this.todo
對象並從新賦值,這意味着父組件的數據在咱們提交表單以前不受影響,同時還要新的單向數據綁定語法'<' 。
咱們來定義下什麼叫做「路由組件」:
router.ts
文件在這個例子中,咱們將利用已經存在的 <todo>
組件,咱們使用路由定義和組件上的 bindings
接收數據(這裏使用ui-router
的祕訣是咱們建立的reslove
屬性,這個例子中todoData
直接映射到了bindings
)。咱們把它看做一個路由組件,由於它本質上是一個「視圖」:
/* ----- todo/todo.component.ts ----- */ import { TodoController } from './todo.controller'; export const TodoComponent: angular.IComponentOptions = { bindings: { todoData: '<' }, controller: TodoController, template: ` <div class="todo"> <todo-form todo="$ctrl.newTodo" on-add-todo="$ctrl.addTodo($event);"> <todo-list todos="$ctrl.todos"></todo-list> </div> ` }; /* ----- todo/todo.controller.ts ----- */ import { TodoItem } from '../common/model/todo'; export class TodoController { todos: TodoItem[] = []; $onInit() { this.newTodo = new TodoItem(); } $onChanges(changes) { if (changes.todoData) { this.todos = Object.assign({}, this.todoData); } } addTodo({ todo }) { if (!todo) return; this.todos.unshift(todo); this.newTodo = new TodoItem(); } } /* ----- common/model/todo.ts ----- */ export class TodoItem { constructor( public title: string = '', public completed: boolean = false) { } } /* ----- todo/todo.service.ts ----- */ export class TodoService { static $inject: string[] = ['$http']; constructor(private $http: angular.IHttpService) { } getTodos() { return this.$http.get('/api/todos').then(response => response.data); } } /* ----- todo/index.ts ----- */ import angular from 'angular'; import { TodoComponent } from './todo.component'; import { TodoService } from './todo.service'; export const TodoModule = angular .module('todo', []) .component('todo', TodoComponent) .service('TodoService', TodoService) .config(($stateProvider: angular.ui.IStateProvider, $urlRouterProvider: angular.ui.IUrlRouterProvider) => { $stateProvider .state('todos', { url: '/todos', component: 'todo', resolve: { todoData: TodoService => TodoService.getTodos(); } }); $urlRouterProvider.otherwise('/'); }) .name;
指令給了咱們 template
,scope
綁定 ,bindToController
,link
和許多其餘的事情。使用這些咱們應該慎重考慮如今的 .component()
。指令不該該再聲明模板和控制器了,或者經過綁定接收數據。指令應該僅僅是爲了裝飾DOM使用。這樣,使用 .component()
建立就意味着擴展示有的HTML。簡而言之,若是你須要自定義DOM
事件/ APIs和邏輯,在組件裏使用一個指令將其綁定到模板。若是你須要的足夠的數量的 DOM
操做,$postLink
生命週期鉤子值得考慮,可是這並非遷移全部的的DOM操做,若是能夠的話,你可使用指令來處理非Angular的事情。
下面是一些使用指令的建議:
restrict: 'A'
compile
和 link
$scope.$on('$destroy', fn);
中銷燬或者解綁事件處理Due to the fact directives support most of what .component()
does (template directives were the original component), I'm recommending limiting your directive Object definitions to only these properties, to avoid using directives incorrectly:
因爲指令實際上支持了大多數 .component()
的語法 (模板指令就是最原始的組件), 我建議將指令對象定義限制在這些屬性上,去避免錯誤的使用指令:
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) | 文檔 |
templateUrl | No | 使用一個組件 |
transclude | No | 使用一個組件 |
這裏有使用 TypeScript 和 directives 實現的幾種方式,不論是使用箭頭函數仍是更簡單的複製,或者使用 TypeScript 的 Class
。選擇最適合你或者你團隊的,Angular2中使用的是Class
。
下面是使用箭頭函數表達式使用常量的例子() => ({})
,返回一個對象字面量(注意與使用.directive()
的不一樣):
/* ----- todo/todo-autofocus.directive.ts ----- */ import angular from 'angular'; export const TodoAutoFocus = ($timeout: angular.ITimeoutService) => (<angular.IDirective> { restrict: 'A', link($scope, $element, $attrs) { $scope.$watch($attrs.todoAutofocus, (newValue, oldValue) => { if (!newValue) { return; } $timeout(() => $element[0].focus()); }); } }); TodoAutoFocus.$inject = ['$timeout']; /* ----- todo/index.ts ----- */ import angular from 'angular'; import { TodoComponent } from './todo.component'; import { TodoAutofocus } from './todo-autofocus.directive'; export const TodoModule = angular .module('todo', []) .component('todo', TodoComponent) .directive('todoAutofocus', TodoAutoFocus) .name;
或者使用 TypeScript Class
(注意在註冊指令的時候手動調用new TodoAutoFocus
)去建立一個新對象:
/* ----- todo/todo-autofocus.directive.ts ----- */ import angular from 'angular'; export class TodoAutoFocus implements angular.IDirective { static $inject: string[] = ['$timeout']; restrict: string; constructor(private $timeout: angular.ITimeoutService) { this.restrict = 'A'; } link($scope, $element: HTMLElement, $attrs) { $scope.$watch($attrs.todoAutofocus, (newValue, oldValue) => { if (!newValue) { return; } $timeout(() => $element[0].focus()); }); } } /* ----- todo/index.ts ----- */ import angular from 'angular'; import { TodoComponent } from './todo.component'; import { TodoAutofocus } from './todo-autofocus.directive'; export const TodoModule = angular .module('todo', []) .component('todo', TodoComponent) .directive('todoAutofocus', ($timeout: angular.ITimeoutService) => new TodoAutoFocus($timeout)) .name;
服務本質上是包含業務邏輯的容器,而咱們的組件不該該直接進行請求。服務包含其它內置或外部服務,例如$http
,咱們能夠隨時隨地的在應用程序注入到組件控制器。咱們在開發服務有兩種方式,使用.service()
或者 .factory()
。使用TypeScript Class
,咱們應該只使用.service()
,經過$inject
完成依賴注入。
下面是使用 TypeScript Class
實現<todo>
應用的一個例子:
/* ----- todo/todo.service.ts ----- */ export class TodoService { static $inject: string[] = ['$http']; constructor(private $http: angular.IHttpService) { } getTodos() { return this.$http.get('/api/todos').then(response => response.data); } } /* ----- todo/index.ts ----- */ import angular from 'angular'; import { TodoComponent } from './todo.component'; import { TodoService } from './todo.service'; export const todo = angular .module('todo', []) .component('todo', TodoComponent) .service('TodoService', TodoService) .name;
利用Webpack 咱們如今能夠在 *.module.js
中的 .scss
文件上使用import
語句,讓 Webpack 知道在咱們的樣式中包含這樣的文件。 這樣作可使咱們的組件在功能和樣式上保持分離,它還與Angular2中使用樣式的方式更加貼近。這樣作不會讓樣式像Angular2同樣隔離在某個組件上,樣式還能夠普遍應用到咱們的應用程序上,可是它更加易於管理,而且使得咱們的應用結構更加易於推理。
If you have some variables or globally used styles like form input elements then these files should still be placed into the root scss
folder. e.g. scss/_forms.scss
. These global styles can the be @imported
into your root module (app.module.js
) stylesheet like you would normally do.
若是你有一些變量或者全局使用的樣式,像表單的input元素,那麼這些文件應該放在根scss
文件夾。例如scss/_forms.scss
。這些全局的樣式能夠像一般意義被@imported
到根模塊(app.module.ts
)。
若是你想支持組件路由,使用ui-router
latest alpha(查看Readme)
template: '<component>'
和 沒有 bindings
困住$inject
屬性考慮在Angular1.5中使用Redux用於數據管理。
For anything else, including API reference, check the Angular documentation.
Open an issue first to discuss potential changes/additions. Please don't open issues for questions.
Copyright (c) 2016 Todd Motto
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
本文github倉庫 準備持續翻譯一些文章,方便的話給個star!謝謝~