关键词 架构, 文件结构, 组件, 单向数据流以及最佳实践html
来自 @toddmotto 团队的编码指南webpack
Angular 的编码风格以及架构已经使用ES2015进行重写,这些在Angular 1.5+的变化能够更好帮助您的更好的升级到Angular2.。
这份指南包括了新的单向数据流,事件委托,组件架构和组件路由。git
老版本的指南你能够在这里找到, 在这里你能看到最新的.angularjs
Angular 中的每个模块都是一个模块组件。一个模块组件囊括了逻辑,模版,路由和子组件。es6
在模块的设计直接反映到咱们的文件夹结构,从而保证咱们项目的可维护性和可预测性。
咱们最好应该有三个高层次的模块:根,组件和经常使用模块。根模块定义了用于启动App和相应的模板的基本架子。
而后,咱们导入须要依赖的组件和通用模块。组件和通用模块而后须要低级别的组件模块,其中包含咱们的组件,控制器,服务,指令,过滤器和给可重复使用的功能进行测试。github
根模块会启动一个根组件,整个组件主要定义了整个应用的基本的元素和路由出口,例如使用ui-view
和ui-router
。web
// 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
单向数据流已经在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完成依赖注入。
下面的 <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;
使用 Babel 将ES2015进行转换为当前浏览器所支持的代码
考虑使用 TypeScript 让你更好的迁移到Angular2
使用 ui-router
latest alpha (查看 Readme) 若是你但愿支持路由钻
你可能会在 template: '<component>'
以及 不须要 bindings
中遇到一些挫折
考虑使用 Webpack 来编译es2016的代码
使用 ngAnnotate 自动完成 $inject
属性注入
考虑使用 Redux 用于 数据管理.
关于Angular API Angular documentation.
Github: https://github.com/JackPu/angular-styleguide/blob/master/i18n/zh-cn.md
感谢 @toddmotto 许可