Angular 1.x + ES6 开发风格指南
阅读本文之前,请确保自己已经读过民工叔的这篇 blog 《Angular 1.x和ES6的结合》。 大概年初开始在我的忽悠下我厂启动了Angular1.x + ES6的切换准备工作,第一个试点项目是公司内部的组件库。目前已经实施了三个多月,期间也包括一些其它新开产品的试点。中间也经历的一些痛苦及反复(组件库代码经历过几次调整,现在还在重构ing),总结了一些经验分享给大家。(实际上民工叔的文章中提到了大部分实践指南,我这里尝试作一定整理及补充,包括一些自己的思考及解决方案) 开始之前务必再次明确一件事情,就是我们使用ES6来开发Angular1.x的目的。总结一下大概三点:
其中第1点是技术投资需要,第2、3点是架构需要。 我们先来看看要达到这些要求,具体要如何一步步实现。 Module在ES6 module的帮助下,ng的模块机制就变成了纯粹的迎合框架的语法了。
example: // moduleA.js import angular from 'angular'; import Controller from './Controller'; export default angular.module('moduleA',[]) .controller('AppController',Controller) .name; // moduleB.js 需要依赖module A import angular from 'angular'; import moduleA from './moduleA'; angular.module('moduleB',[moduleA]); 通过这种方式,无论被依赖的模块的模块名怎么改变都不会对其他模块造成影响。
Controllerng1.2版本开始提供了一个controllerAs语法,自此Controller终于能变成一个纯净的ViewModel(视图模型)了,而不是像之前一样混入过多的$scope痕迹(供angular框架使用)。 example: <div ng-controller="AppCtrl as app"> <div ng-bind="app.name"></div> <button ng-click="app.getName">get app name</button> </div> // Controller AppCtrl.js export default class AppCtrl { constructor() { this.name = 'angular&es6'; } getName() { return this.name; } } // module import AppCtrl from './AppCtrl'; export default angular.module('app',[]) .controller('AppCtrl',AppCtrl) .name; 这种方式写controller等同于ES5中这样去写: function AppCtrl() { this.name = 'angular&es6'; } AppCtrl.prototype.getName = function() { return this.name; }; .... .controller('AppCtrl',AppCtrl) 不过ES6的class语法糖会让整个过程更自然,再加上ES6 Module提供的模块化机制,业务逻辑会变得更清晰独立。
Component (Directive)以datepicker组件为例 // 目录结构 + date-picker - _date-picker.scss - date-picker.tpl.html - DatePickerCtrl.js - index.js // DatePickerCtrl.js export default class DatePickerCtrl { $onInit() { this.date = `${this.year}-${this.month}`; } getMonth() { ... } getYear() { ... } } 注意,这里我们先写的controller而不是指令的link/compile方法,原因在于一个数据驱动的组件体系下,我们应该尽量减少DOM操作,因此理想状态下,组件是不需要link或compile方法的,而且controller在语义上更贴合mvvm架构。 // index.js import template from './date-picker.tpl.html'; import controller from './DatePickerCtrl'; const ddo = { restrict: 'E',template,controller,controllerAs: '$ctrl',bindToContrller: { year: '=',month: '=' } }; export default angular.module('components.datePicker',[]) .directive('datePicker',ddo) .name; 注意,这里跟民工叔的做法有点不一样。叔叔的做法是把指令做成class然后在index.js中import并初始化,like this: // Directive.js export default class Directive { constructor() { } getXXX() { } } // index.js import Directive from './Directive'; export default angular.module('xxx',[]) .directive('directive',() => new Directive()) .name; 但是我的意见是,整个系统设计中index.js作为angular的包装器使得代码变成框架可识别的,换句话说就是只有index.js中是可以出现框架的影子的,其他地方都应该是框架无关的使用原生代码编写的业务模型。 1.5之后提供了一个新的语法 // index.js import template from './date-picker.tpl.html'; import controller from './DatePickerCtrl'; const ddo = { template,bindings: { year: '=',[]) .component('datePicker',ddo) .name; component语义更简洁明了,比如 另外angular1.5版本有一个大招就是,它给组件定义了相对完整的生命周期钩子(虽然之前我们能用其他的一些手段来模拟init到destroy的钩子,但是实现的方式框架痕迹太重,后面会详细讲到)!而且提供了单向数据流实现方式! // DirectiveController.js export class DirectiveController { $onInit() { } $onChanges(changesObj) { } $onDestroy() { } $postLink() { } } // index.js import template from './date-picker.tpl.html'; import controller from './DatePickerCtrl'; const ddo = { template,bindings: { year: '<',month: '<' } }; export default angular.module('components.datePicker',ddo) .name; component相关详细看这里:angular component guide 从angular的这些api变化来看,ng的开发团队正在越来越多的吸取了一些其他社区的思路,这也从侧面上印证了前端框架正在趋于同质化的事实(至少在同类型问题领域,方案趋于同质)。顺带帮vue打个广告,不论是进化速度还是方案落地速度,vue都已经赶超angular了。推荐大家都去关注下vue。
Service、Filter自定义服务 provider、service、factory、constant、valueangular1.x中有五种不同类型的服务定义方式,但是如果我们以功能归类,大概可以归出两种类型:
angular原本设计service的目的是提供一个应用级别的共享单元,单例且私有,也就是只能在框架内部使用(通过依赖注入)。在ES5的无模块化系统下,这是一个很好的设计,但是它的问题也同样明显:
很显然,ES6 Module并不会出现这些问题。举例说明,我们之前使用一个服务是这样的: index.js import angular from 'angular'; import Service from './Service'; import Controller from './Controller'; export default angular.module('services',[]) .service('service',Service) .controller('controller',Controller) .name; Service.js export default class Service { getName() { return 'kuitos'; } } Controller.js 这里使用了工具库angular-es-utils来简化ES6中使用依赖注入的方式。 import {Inject} from 'angular-es-utils/decorators'; @Inject('service') export default class Controller { getUserName() { return this._service.getName(); } } 假如哪天在调用controller.getUserName()时报错了,而且错误出在service.getName方法,那么查错的方式是?我是只能全局搜了不知道你们有没有更好的办法。。。 如果我们使用依赖注入,直接基于ES6 Module来做,改造一下会变成这样: Service.js export default { getName() { return 'kuitos'; } } Controller.js import Service from './Service'; export default class Controller { getUserName() { return Service.getName(); } } 这样定位问题是不是容易很多!! 从这个案例上来看,我们能完美模拟基础的 Service、Factory 了,那么还有Provider、Constant、Value呢? Provider.js let apiPrefix = ''; export function setPrefix(prefix) { apiPrefix = prefix; } export function genResource(url) { return resource(apiPrefix + url); } 应用入口时配置: app.js import {setPrefix} from './Provider'; setPrefix('/rest/1.0'); Contant跟Value呢?其实如果我们忘掉angular,它们倆完全没区别: Constant.js export const VERSION = '1.0.0'; 使用ng内置服务上面我们提到我们所有的服务其实都可以脱离angular来写以消除依赖注入,但是有一种状况比较难搞,就是假如我们自定义的工具方法中需要使用到angular的built-in服务怎么办?要获取ng内置服务我们就绕不开依赖注入。但是好在angular有一个核心服务 import injector from 'angular-es-utils/injector'; export default { getUserName() { return injector.get('$http').get('/users/kuitos'); } }; 这样做确实可以但总觉得不够优雅,不过好在大部分场景下我们需要用到built-in service的场景比较少,而且对于 import injector from 'angular-es-utils/injector'; import {FetchHttp} from 'es6-http-utils'; export const HttpClient { get(url) { return injector.get('$http').get(url); } save(url,payload) { return FetchHttp.post(url,payload); } } // Controller.js import {HttpClient} from './HttpClient'; class Controller { saveUser(user) { HttpClient.save('/users',user); } } 通过这些手段,对于业务代码而言基本上是看不到依赖注入的影子的。 Filterangular中filter做的事情有两类:过滤和格式化。归结起来它做的就是一种数据变换的工作。filter的问题不仅仅在于DI的弊端,还有更多其他的问题。vue2中甚至取消了filter的设计,参见[Suggestion]Vue 2.0 - Bring back filters please。其中有一点我特别认可:过度使用filter会让你的代码在不自知的情况下走向混乱的状态。我们可以自己去写一系列的transformer(或者使用underscore之类的工具)来做数据处理,并在vm中显式的调用它。 import {dateFormatter} from './transformers'; export default class Controller { constructor() { this.data = [1,2,3,4]; this.currency = this.data .filter(v => v < 4) .map(v => '$' + v); this.date = Date.now(); this.today = dateFormatter(this.date); } }
一步步淡化框架概念如果想将业务模型彻底从框架中抽离出来,下面这几件事情是必须解决的。 依赖注入前面提到过,通过一系列手段我们可以最大程度消除依赖注入。但是总有那些edge case,比如我们要用$stateParams或者服务来自路由配置中注入的local service。我写了一个工具可以帮助我们更舒服的应对这类边缘案例 Link to Controller 依赖属性计算对于需要监控属性变化的场景,之前我们都是用 class Controller { get fullName() { return `${this.firstName} ${this.lastName}`; } } template <input type="text" ng-model="$ctrl.firstName"> <input type="text" ng-model="$ctrl.lastName"> <span ng-bind="$ctrl.fullName"></span> 这样当firstName/lastName发生变化时,fullName也会相应的改变。基于的原理是 class Controller { $onChanges(objs) { this.userCount = objs.users.length; } } const ddo = { controller: Controller,template: '<span ng-bind="$ctrl.listTitle"></span><span ng-bind="$ctrl.userCount"></span>' bindings: { title: '<',users: '<' } }; angular.module('component',[]) .component('userList',ddo); template <div ng-controller="ctrl as app"> <user-list title="app.title" users="app.users" ng-click="app.change()"></user-list> </div> class Controller { contructor() { this.title = 'hhhh'; this.users = []; } change() { this.users.push('s'); } } angular.module('app',[]) .controller('ctrl',Controller); 点击user-list组件时,userCount值并不会变化,因为 组件生命周期组件新增的四个生命周期对于我而言可以说是最重大的变化了。虽然之前我们也能通过一些手段来模拟生命周期:比如用compile模拟init,postLink模拟didMounted, 但是它们最大的问题就是身上携带了太多框架的气息,并不能服务文明剥离框架的初衷。具体做法不赘述了,看上面组件部分的介绍Link To Component)。 事件通知以前我们在ng中使用事件模型有 我的建议是,我们只在必要的场景使用事件机制,因为事件滥用和不及时的卸载很容易造成事件爆炸的情况发生。必要的场景就是,当我们需要在兄弟节点、或依赖关系不大的组件间触发式通信时,我们可以使用自制的 事件总线/中介者 来帮我们完成(可以使用我的这个工具库angular-es-utils/EventBus)。在非必要的场景下,我们应该尽量使用 const ddo = { template: '<button type="button" ng-click="$ctrl.change('kuitos')">click me</button>',controller: class { click(userName) { this.onClick({userName}); } },bindings: { onClick: '&' } }; angular.module('app',[]) .component('user',ddp); useage <user on-click="logUserName(userName)"></user> 总结理想状态下,对于一个业务系统而言,会用到angular语法只有 对于web app架构而言,angular/vue/react 等组件框架/库 提供的只是 模板语法&胶水语法(其中胶水语法指的是框架/库 定义组件/控制器 的语法),剥离这两个外壳,我们的业务模型及数据模型应该是可以脱离框架运作的。古者有云,衡量一个完美的MV*架构的标准就是,在V随意变化的情况下,你的M*是可以不改一行代码的情况下就完成迁移的。 在MV*架构中,V层是最薄且最易变的,但是M*理应是 稳定且纯净的。虽然要做到一行代码不改实现框架的迁移是不可能的(视图层&胶水语法的修改不可避免),但是我们可以尽量将最重的 M* 做成框架无关,这样做上层的迁移时剩下的就是一些语法替换的工作了,而且对V层的改变也是代价最小的。 事实上我认为一个真正可伸缩的系统架构都应该是这样一个思路:勿论是 MV* 还是 Flux/Redux or whatever,确保下层 业务模型/数据模型 的纯净都是有必要的,这样才能提供上层随意变化的可能,任何模式下的应用开发,都应该具备这样的一个能力。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
- scala – 无法使用IDEA和SBT运行LWJGL
- angularjs – Angular JS – 如何处理重复的HTML代码,如页眉
- 谈谈Angular指令bindToController的使用(1.4版本后支持)
- 斯卡拉 – JsResult – Monad或Applicative?
- scala – 如何将隐式参数导入匿名函数
- Ubuntu 12.04下3分钟搭建apache+python的运行环境
- 如何使用bash上的csv文件中的特定列解析内容
- WebService CXF学习 1
- LR 测试webservice协议 并发运行报错:Abnormal terminatio
- 如何检测scala executioncontext耗尽?