使用 AngularJS 实现 WebBinding 已翻译 100%

oschina 投递于 2015/02/27 08:05 (共 23 段, 翻译完成于 03-17)
阅读 1192
收藏 2
0
加载中

下载 demo - 2.8 MB

下载源代码 - 39.8 KB

AngularJS WebBinding

介绍

在之前的文章 (WebBinding - 如何将客户端JavaScript对象绑定到服务器端的 .NET 对象)中, 我们实现了一个将.Net服务器端对象绑定到JavaScript客户端对象的通用解决方案. 对于那个方案,我们提供了一个针对 Knockout 库的实现. 鉴于 AngularJS 已经变成了一个非常流行的库, 我就觉得有必要针对这个库也提供一个实现.

咋一看上去非常容易,只需实现很少的带有针对那个库特定实现的几个函数就行了. 听起来相当简单.。在实现这个 WebBinding 方案的时候我也是这么想的。但是,在我们顺着这篇文章往下看的时候就会明白,在我们这儿 (使用 AngularJS 库), 不会像听起来的这么简单。

LeoXu
LeoXu
翻译于 2015/02/28 13:37
1

背景

为了能够可以对特定库做针对性的实现,WebBinding 客户端脚本提供一堆可以被重写的函数:

  • createObjectHolder: 创建一个属性的对象处理器 (一个可以用来处理属性值访问的对象).

  • createArrayHolder: 创建一个管理数组的属性的对象处理器.

  • getObjectValue: 从一个属性的对象处理器哪里获取一个属性的值.

  • getArrayValue: 从一个数组属性的对象处理器哪里获取一个数组的属性值.

  • setObjectValue: 使用一个属性的对象处理器设置一个属性的值.

  • setArrayValue: 使用一个数组属性的对象处理器设置一个数组属性的值.

  • registerForPropertyChanges: 设置一个在属性值改变时会被调用的一个函数.

  • registerForArrayChanges: 设置一个在数组属性值被改变时会被调用的一个函数.

    在一些情况下 (就像本文中第一个情况), 我们可能也想要去改变其它的公共函数:

  • addBindingMapping: 为一个给定的绑定映射构建一个绑定的客户端模型.

  • applyBindings: 将构建好的绑定的客户端模型注册变更通知.

  • beginServerChangesRequests: 开始绑定通信.

  • createBoundObjectForPropertyPath: 创建一个同 WebBinding 机制绑定的客户端对象.

本文展示了我们如何为AngularJS库, 通过实现了相应的特定代码的那些函数 (没有去改变原生 WebBinding 带),去实现一个WebBinding方案.

本文假定读者对JavaScript和AngularJS库基本熟悉. 我们实现的某些部分需要对Angular的内部原理有一个更加深入的理解. 我们会在合适的地方提到每一个这些问题.

LeoXu
LeoXu
翻译于 2015/03/01 22:31
1

它是如何运作的

用于AngularJS对象的对象处理器

用一个对象封装 AngularJS 作用域的属性

当我在开发 WebBinding 方案的时候, 我使用过 Knockout 库. 在 Knockout 库中, 每一个 (我们想要绑定的) 属性都用一个观察者对象进行了封装. 使用这个对象,我们可以获得这个属性的值,设置这个属性的值(还有就是通知属性发生了变化) 以及,订阅对某个属性变化的观察. 根据那个设计,我设计 WebBinding 的 通用实现, 基于处理对属性值的访问的对象处理器 (封装了相关属性的观察者对象) .

利用 Knockout, 用为属性是可被观察值的封装, 我们使用属性自身来作为对象处理器. 使用 AngularJS, 就不一样了. 在 AngularJS 库中,我们有一个带有可以使用 Angular 表达式 访问到常规属性 的 作用域 对象. 为了能实现针对AngularJS的 WebBinding, 我们也需要针对AngularJS的对象处理器 (可被观察值的封装).

LeoXu
LeoXu
翻译于 2015/03/03 21:14
1

为了达成目标,我们创建了:

  1. 一个用于处理绑定了Angular作用域(绑定功能的根对象)的对象以及,一个用于容纳相应的(用于作用域属性的)对象处理器的对象:

    function RootObjectWrapper(rootObj) {
        this.orgRootObj = rootObj;
        this.wrapperRootObj = {};
    }
  2. 一个用于为一个作用域属性实现一个对象处理器的对象:

    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 + &apos;[&apos; + elemInx + &apos;]&apos;;
                    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 + &apos;.&apos; + prop;
                    objHolder.validateProperties(rootObj, subPropExp);
                }
            }
        }
    }
};
LeoXu
LeoXu
翻译于 2015/03/04 21:52
1

获取属性的值

   解析 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 依赖注入

LeoXu
LeoXu
翻译于 2015/03/05 21:52
1

在之前的环节中,我们聊到了 $parse 服务. 但是,那个 $parse 是什么呢? 我们怎么获取到它呢? 一般,我们可以通过向我们的Angular 组件的 (控制器指令, 等等...) 构造函数中加入一个名叫 $parse 的参数 来获得它, 它会想对应的参数注入想要的服务. 你可以访问下面的链接,来获取更多有关Angular依赖注入的详细信息:

由于我们的 WebBinding 实现并不是有AngularJS构建的,我们就只有手动去讲 $parse 服务注入了. 那样做了之后,我们就可以使用ng模块来获得 angular 注入器, 继而利用注入器它来像下面这样获取到 $parse 服务:

var angInjector = angular.injector(["ng"]);
var angParser = angInjector.get("$parse");

   实现获取器函数

在我们有了 $parse 服务之后,我们就可以利用它来获取所需要的属性值. 我们可以像下面这样做:

  1. 为每一个属性设置获取器函数:

    PropertyObjectHolder.prototype.getGetterFunction = function() {
        var ret = angParser(this.pathExp);
        return ret;
    };
    
    PropertyObjectHolder.prototype.validateProperties = function (rootObj, pathExpression) {
        // ...
    
        this.getterFn = this.getGetterFunction();
    
        // ...
    };
  2. 针对每一个对象处理器,为获取到属性值实现一个函数:

    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 === &apos;string&apos; || typeof val === &apos;number&apos; || typeof val === &apos;boolean&apos;;
    }
LeoXu
LeoXu
翻译于 2015/03/06 21:33
1

在 isOfSimpleType 函数中,我们会检查被对象处理器封装的属性类型是否是简单类型。在我们这儿,每一个没有一个数组,以及没有在内部包含一个对象处理器的对象处理器,以及其被封装了是本地类型(字符串、数字或者布尔值)的属性, 都会被看做是一个简单类型。

在 getValue 函数中,我们会根据类型获取到属性的值. 如果值的类型是一个简单类型,我们会返回(借助于$parse获取到的)获取器函数的执行结果. 其它 的(如果值的类型不是一个简单类型), 我们会返回对象处理器的内部对象. 内部对象包含了(针对子属性,或者是针对数组元素的)针对被封装属性的内部对象处理器.

LeoXu
LeoXu
翻译于 2015/03/07 16:57
1

设置属性的值

   将值的变化应用到AngularJS

为了获取到属性的值,我们可以简单的调用借助于$parse服务获取到的获取器函数. 为了设置属性的值,我们可以调用相应的(借助于从分配给获取器函数的属性那里获得的)设置器函数. 但是,当为我们的作用域属性设置值的时候,我们常常想要变化应该也要被反映到被绑定的DOM元素上.

通常,使用AngularJS组件时,可以透明地做到. 但它是如何做到的呢? 所有的奥妙都在作用域中的 $apply 和 $digest 函数里. 当使用AngularJS运行我们的代码时(例如,通过一个 ng-click 指令),我们的代码使用了作用域的 $apply 函数被封装了起来. 这个函数会运行我们的代码并调用作用域$digest函数,以将我们的变更反映到相关的绑定上. 当我们的代码不是由AngularJS运行的时候(想我们这里的这种情况),我们就应该手动去调用$apply函数了.

LeoXu
LeoXu
翻译于 2015/03/07 17:05
1

  实现设置器函数

利用 $parse 服务和 $apply 函数, 我们可以设置需要的属性值. 可以像下面这样做:

  1. 为每一个属性设置设置器函数:

    PropertyObjectHolder.prototype.getSetterFunction = function () {
        var getter = angParser(this.pathExp);
        return getter.assign;
    };
    
    PropertyObjectHolder.prototype.validateProperties = function (rootObj, pathExpression) {
        // ...
    
        this.setterFn = this.getSetterFunction();
    
        // ...
    };
  2. 为每一个对象处理器实现一个用于设置属性值的函数:

    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&apos;s $$phase to
                          ensure that we aren&apos;t already in middle of an $apply or a $digest process.
                          But, since our script runs outside of AngularJS,
                          we don&apos;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 作用域变化的注册器

   监视 AngularJS 变化

在前面一节中,我们提到了作用域的 $digest 函数. 使用那个函数,我们就可以通知被绑定的组件有关 作用域 的变化了. 但是,那个 $digest 函数是怎么运作的呢 ?  AngularJS 是如何知道应该通知哪个组件的呢? 答案就是,组件自身会告诉作用域,那些变化它们需要被通知到. 那是利用了作用域的 $watch$watchGroup 以及 $watchCollection 函数做到的.

LeoXu
LeoXu
翻译于 2015/03/08 21:18
1

在开发 Angular 组件(例如 指令, 等.)的时候, 我们应该利用那些函数以为相关的变化进行注册. 在 $digest 阶段, AngularJS 处理了作用域所有被注册的监视器.

   对属性变化进行注册

对我们的情况而言,我们想要就需要的属性变化被通知到. 那样的话就可以像下面这样做:

  1. 实现一个用于注册一个属性值变化的监视器的函数:

    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;
    };
  2. 实现一个用于注册数组变化的监视器的函数:

    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;
            }
        }
    }
};

处理数组的变化

因为我们为每一个作用域的属性都维护了一个相应的对象处理器, 我们不得不保持对象都是同步的. 对于简单的属性(不包括数组类型), 我们并不要为了同步做任何特别的处理. 但是,对于数组属性而言,我们需要让对象们都有相同的元素总数.

LeoXu
LeoXu
翻译于 2015/03/08 21:31
1
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(0)

返回顶部
顶部