在之前的文章 (WebBinding - 如何将客户端JavaScript对象绑定到服务器端的 .NET 对象)中, 我们实现了一个将.Net服务器端对象绑定到JavaScript客户端对象的通用解决方案. 对于那个方案,我们提供了一个针对 Knockout 库的实现. 鉴于 AngularJS 已经变成了一个非常流行的库, 我就觉得有必要针对这个库也提供一个实现.
咋一看上去非常容易,只需实现很少的带有针对那个库特定实现的几个函数就行了. 听起来相当简单.。在实现这个 WebBinding 方案的时候我也是这么想的。但是,在我们顺着这篇文章往下看的时候就会明白,在我们这儿 (使用 AngularJS 库), 不会像听起来的这么简单。
为了能够可以对特定库做针对性的实现,WebBinding 客户端脚本提供一堆可以被重写的函数:
createObjectHolder: 创建一个属性的对象处理器 (一个可以用来处理属性值访问的对象).
createArrayHolder: 创建一个管理数组的属性的对象处理器.
getObjectValue: 从一个属性的对象处理器哪里获取一个属性的值.
getArrayValue: 从一个数组属性的对象处理器哪里获取一个数组的属性值.
setObjectValue: 使用一个属性的对象处理器设置一个属性的值.
setArrayValue: 使用一个数组属性的对象处理器设置一个数组属性的值.
registerForPropertyChanges: 设置一个在属性值改变时会被调用的一个函数.
registerForArrayChanges: 设置一个在数组属性值被改变时会被调用的一个函数.
在一些情况下 (就像本文中第一个情况), 我们可能也想要去改变其它的公共函数:
addBindingMapping: 为一个给定的绑定映射构建一个绑定的客户端模型.
applyBindings: 将构建好的绑定的客户端模型注册变更通知.
beginServerChangesRequests: 开始绑定通信.
createBoundObjectForPropertyPath: 创建一个同 WebBinding 机制绑定的客户端对象.
本文展示了我们如何为AngularJS库, 通过实现了相应的特定代码的那些函数 (没有去改变原生 WebBinding 带),去实现一个WebBinding方案.
本文假定读者对JavaScript和AngularJS库基本熟悉. 我们实现的某些部分需要对Angular的内部原理有一个更加深入的理解. 我们会在合适的地方提到每一个这些问题.
当我在开发 WebBinding 方案的时候, 我使用过 Knockout 库. 在 Knockout 库中, 每一个 (我们想要绑定的) 属性都用一个观察者对象进行了封装. 使用这个对象,我们可以获得这个属性的值,设置这个属性的值(还有就是通知属性发生了变化) 以及,订阅对某个属性变化的观察. 根据那个设计,我设计 WebBinding 的 通用实现, 基于处理对属性值的访问的对象处理器 (封装了相关属性的观察者对象) .
利用 Knockout, 用为属性是可被观察值的封装, 我们使用属性自身来作为对象处理器. 使用 AngularJS, 就不一样了. 在 AngularJS 库中,我们有一个带有可以使用 Angular 表达式 访问到常规属性 的 作用域 对象. 为了能实现针对AngularJS的 WebBinding, 我们也需要针对AngularJS的对象处理器 (可被观察值的封装).
为了达成目标,我们创建了:
一个用于处理绑定了Angular作用域(绑定功能的根对象)的对象以及,一个用于容纳相应的(用于作用域属性的)对象处理器的对象:
function RootObjectWrapper(rootObj) { this.orgRootObj = rootObj; this.wrapperRootObj = {}; }
一个用于为一个作用域属性实现一个对象处理器的对象:
function PropertyObjectHolder() { this.pathExp = ""; this.rootObj = {}; this.isArray = false; this.innerValue = null; }
在 PropertyObjectHolder 对象中,我们放了根对象,以及相关的属性的表达式. 我们可以通过遍历整个 PropertyObjectHolder 的属性,为每一个对象处理器,设置适当的属性表达式, 从 RootObjectWrapper 对象开始,用如下代码依照属性的树形结构来构建表达式:
function validateRootObjectWrappers() { for (var objWrapInx = 0; objWrapInx < rootObjectWrappers.length; objWrapInx++) { var objWrapper = rootObjectWrappers[objWrapInx]; if (objWrapper instanceof RootObjectWrapper) { objWrapper.validateProperties(); } } } RootObjectWrapper.prototype.validateProperties = function () { for (var prop in this.wrapperRootObj) { var objHolder = this.wrapperRootObj[prop]; if (objHolder instanceof PropertyObjectHolder) { objHolder.validateProperties(this.orgRootObj, prop); } } }; PropertyObjectHolder.prototype.validateProperties = function (rootObj, pathExpression) { this.rootObj = rootObj; this.pathExp = pathExpression; if (this.isArray) { if (this.innerValue instanceof Array) { for (var elemInx = 0; elemInx < this.innerValue.length; elemInx++) { var objHolder = this.innerValue[elemInx]; if (objHolder instanceof PropertyObjectHolder) { var subPropExp = pathExpression + '[' + elemInx + ']'; objHolder.validateProperties(rootObj, subPropExp); } } } } else { if (this.innerValue) { for (var prop in this.innerValue) { var objHolder = this.innerValue[prop]; if (objHolder instanceof PropertyObjectHolder) { var subPropExp = pathExpression + '.' + prop; objHolder.validateProperties(rootObj, subPropExp); } } } } };
解析 Angular 表达式
在为我们的作用域构建了对象处理器树结构之后,由于每一个对象处理器都容纳了作用域对象以及一个对应的(针对制定属性的)Angular表达式, 为了要获取到属性的值,我们所有需要的就是为一个给定的作用域解析一个Angular表达式的方法. 幸运的是,我们在AngularJS库里面拥有了这种机制. 利用 Angular 的 $parse 服务, 我么可以获取到获取和设置属性值的函数.
使用 $parse 服务, 我们能够使用一个Angular表达式获得一个获取器函数, 并且像下面这样利用带有相应对象的获取器函数来获得属性的值:
var getter = $parse(expression); var propertyValue = getter(scopeObject);
为了设置属性的值,我们可以有一个设置器函数,有了设置器函数,就可以像下面这样设置属性的值了:
var getter = $parse(expression); var setter = getter.assign; setter(scopeObject, propertyValue);
AngularJS 依赖注入
在之前的环节中,我们聊到了 $parse 服务. 但是,那个 $parse 是什么呢? 我们怎么获取到它呢? 一般,我们可以通过向我们的Angular 组件的 (控制器, 指令, 等等...) 构造函数中加入一个名叫 $parse 的参数 来获得它, 它会想对应的参数注入想要的服务. 你可以访问下面的链接,来获取更多有关Angular依赖注入的详细信息:
由于我们的 WebBinding 实现并不是有AngularJS构建的,我们就只有手动去讲 $parse 服务注入了. 那样做了之后,我们就可以使用ng模块来获得 angular 注入器, 继而利用注入器它来像下面这样获取到 $parse 服务:
var angInjector = angular.injector(["ng"]); var angParser = angInjector.get("$parse");
实现获取器函数
在我们有了 $parse 服务之后,我们就可以利用它来获取所需要的属性值. 我们可以像下面这样做:
为每一个属性设置获取器函数:
PropertyObjectHolder.prototype.getGetterFunction = function() { var ret = angParser(this.pathExp); return ret; }; PropertyObjectHolder.prototype.validateProperties = function (rootObj, pathExpression) { // ... this.getterFn = this.getGetterFunction(); // ... };
针对每一个对象处理器,为获取到属性值实现一个函数:
PropertyObjectHolder.prototype.getValue = function () { var res = ""; if (this.isOfSimpleType()) { if (this.validate()) { res = this.getterFn(this.rootObj); } } else { res = this.innerValue; } return res; }; PropertyObjectHolder.prototype.validate = function () { if (!this.isValid()) { validateRootObjectWrappers(); if (!this.isValid()) { /*--- The object is still invalid, after the validation... ---*/ return false; } } return true; }; PropertyObjectHolder.prototype.isValid = function () { if (!this.rootObj || !this.pathExp || this.pathExp.length == 0) { return false; } return true; }; PropertyObjectHolder.prototype.isOfSimpleType = function () { if (this.isArray || this.hasPropertyObjectHolderProperties()) { return false; } if (this.innerValue) { return isSimpleType(this.innerValue); } return true; }; PropertyObjectHolder.prototype.hasPropertyObjectHolderProperties = function () { if (!this.innerValue) { return false; } for (var prop in this.innerValue) { if (this.innerValue[prop] instanceof PropertyObjectHolder) { return true; } } return false; }; function isSimpleType(val) { return typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean'; }
在 isOfSimpleType 函数中,我们会检查被对象处理器封装的属性类型是否是简单类型。在我们这儿,每一个没有一个数组,以及没有在内部包含一个对象处理器的对象处理器,以及其被封装了是本地类型(字符串、数字或者布尔值)的属性, 都会被看做是一个简单类型。
在 getValue 函数中,我们会根据类型获取到属性的值. 如果值的类型是一个简单类型,我们会返回(借助于$parse获取到的)获取器函数的执行结果. 其它 的(如果值的类型不是一个简单类型), 我们会返回对象处理器的内部对象. 内部对象包含了(针对子属性,或者是针对数组元素的)针对被封装属性的内部对象处理器.
将值的变化应用到AngularJS
为了获取到属性的值,我们可以简单的调用借助于$parse服务获取到的获取器函数. 为了设置属性的值,我们可以调用相应的(借助于从分配给获取器函数的属性那里获得的)设置器函数. 但是,当为我们的作用域属性设置值的时候,我们常常想要变化应该也要被反映到被绑定的DOM元素上.
通常,使用AngularJS组件时,可以透明地做到. 但它是如何做到的呢? 所有的奥妙都在作用域中的 $apply 和 $digest 函数里. 当使用AngularJS运行我们的代码时(例如,通过一个 ng-click 指令),我们的代码使用了作用域的 $apply 函数被封装了起来. 这个函数会运行我们的代码并调用作用域$digest函数,以将我们的变更反映到相关的绑定上. 当我们的代码不是由AngularJS运行的时候(想我们这里的这种情况),我们就应该手动去调用$apply函数了.
实现设置器函数
利用 $parse 服务和 $apply 函数, 我们可以设置需要的属性值. 可以像下面这样做:
为每一个属性设置设置器函数:
PropertyObjectHolder.prototype.getSetterFunction = function () { var getter = angParser(this.pathExp); return getter.assign; }; PropertyObjectHolder.prototype.validateProperties = function (rootObj, pathExpression) { // ... this.setterFn = this.getSetterFunction(); // ... };
为每一个对象处理器实现一个用于设置属性值的函数:
PropertyObjectHolder.prototype.setValue = function (val) { this.innerValue = val; if (this.isOfSimpleType()) { if (this.validate()) { var self = this; if (isScope(self.rootObj)) { /*--- Sometimes we should check the AngularJS scope's $$phase to ensure that we aren't already in middle of an $apply or a $digest process. But, since our script runs outside of AngularJS, we don't have to be bothered on that issue. ---*/ self.rootObj.$apply(function () { self.setterFn(self.rootObj, val); }); } else { self.setterFn(self.rootObj, val); } } } }; function isScope(obj) { if (obj && obj.$apply && obj.$watch && obj.$watchCollection) { return true; } return false; }
在 setValue 函数中,我们将给定的值设置为对象处理器的内部值,而如果值的类型是一个简单类型,我们就可以使用给定的值来调用(通过$parse获取的)设置器函数.
监视 AngularJS 变化
在前面一节中,我们提到了作用域的 $digest 函数. 使用那个函数,我们就可以通知被绑定的组件有关 作用域 的变化了. 但是,那个 $digest 函数是怎么运作的呢 ? AngularJS 是如何知道应该通知哪个组件的呢? 答案就是,组件自身会告诉作用域,那些变化它们需要被通知到. 那是利用了作用域的 $watch, $watchGroup 以及 $watchCollection 函数做到的.
在开发 Angular 组件(例如 指令, 等.)的时候, 我们应该利用那些函数以为相关的变化进行注册. 在 $digest 阶段, AngularJS 处理了作用域所有被注册的监视器.
对属性变化进行注册
对我们的情况而言,我们想要就需要的属性变化被通知到. 那样的话就可以像下面这样做:
实现一个用于注册一个属性值变化的监视器的函数:
PropertyObjectHolder.prototype.subscribeForPropertyChange = function (propNotificationFunc) { if (isScope(this.rootObj) && this.isValid()) { this.rootObj.$watch(this.pathExp, function (newValue, oldValue) { propNotificationFunc(); }); return true; } else { this.pendingNotificationFunc = propNotificationFunc; } return false; };
实现一个用于注册数组变化的监视器的函数:
PropertyObjectHolder.prototype.subscribeForArrayChange = function (arrNotificationFunc) { if (isScope(this.rootObj) && this.isValid()) { this.rootObj.$watchCollection(this.pathExp, function (newValue, oldValue) { arrNotificationFunc(); }); return true; } else { this.pendingNotificationFunc = arrNotificationFunc; } return false; };
这些函数的算法相当简单. 如果对象处理器可用 (也就是所作用域和表达式已经被设置好了), 就用给定的函数注册一个监视. 否则,把给定的函数存储起来,知道对象可用.
在 validateProperties 函数中,我们用存储起来的函数(如果它存在的话)注册了一个监视:
PropertyObjectHolder.prototype.validateProperties = function (rootObj, pathExpression) { // ... if (this.isArray) { // ... if (this.pendingNotificationFunc) { /*--- There is a notifications function that is pending for registration. ---*/ if (this.subscribeForArrayChange(this.pendingNotificationFunc)) { this.pendingNotificationFunc = null; } } } else { // ... if (this.pendingNotificationFunc) { /*--- There is a notifications function that is pending for registration. ---*/ if (this.subscribeForPropertyChange(this.pendingNotificationFunc)) { this.pendingNotificationFunc = null; } } } };
因为我们为每一个作用域的属性都维护了一个相应的对象处理器, 我们不得不保持对象都是同步的. 对于简单的属性(不包括数组类型), 我们并不要为了同步做任何特别的处理. 但是,对于数组属性而言,我们需要让对象们都有相同的元素总数.
评论删除后,数据将无法恢复
评论(0)