2009年7月1日星期三

写个 JavaScript 异步调用框架 (Part 6 - 实例 & 模式)

我们用了5篇文章来讨论如何编写一个JavaScript异步调用框架(问题 & 场景用例设计代码实现链式调用链式实现),现在是时候让我们看一下在各种常见开发情景中如何使用它了。

封装Ajax

设计Async.Operation的最初目的就是解决Ajax调用需要传递callback参数的问题,为此我们先把Ajax请求封装为Async.Operation。我在这里使用的是jQuery,当然无论你用什么基础库,在使用Async.Operation时都可以做这种简单的封装。

var Ajax = {};

Ajax.get = function(url, data) {
  var operation = new Async.Operation();
  $.get(url, data, function(result) {
    operation.yield(result);
  }, "json");
  return operation;
};

Ajax.post = function(url, data) {
  var operation = new Async.Operation();
  $.post(url, data, function(result) {
    operation.yield(result);
  }, "json");
  return operation;
};


在我所调用的服务器端API中,只需要GET和POST,且数据都为JSON,所以我就直接把jQuery提供的其它Ajax选项屏蔽掉了,并设置数据类型为JSON。在你的项目当中,也可以用类似的方式将Ajax封装为若干仅仅返回Async.Operation的方法,将jQuery提供的选项都封装在Ajax这一层内,不再向上层暴露这些选项。

调用Ajax

把Ajax封装好后,我们就可以开始专心写业务逻辑了。

假设我们有一个Friend对象,它的get方法用于返回单个好友对象,而getAll方法用于返回所有好友对象。于此对应的是两个服务器端API,friend接口会返回单个好友JSON,而friendlist接口会返回所有好友名称组成的JSON。

首先我们看看较为基础的get方法怎么写:

function get(name) {
  return Ajax.get("/friend",
    "name=" + encodeURIComponent(name));
}


就这么简单?对的,假如服务器端API返回的JSON结构正好就是你要的好友对象结构的话。如果JSON结构和好友对象结构是异构的,或许你还要加点代码来把JSON映射为对象:

function get(name) {
  var operation = new Async.Operation()
  Ajax.get("/friend", "name=" + encodeURIComponent(name))
    .addCallback(function(json) {
      operation.yield(createFriendFromJson(json));
    });
  return operation;
}

Ajax队列

接下来我们要编写的是getAll方法。因为friendlist接口只返回好友名称列表,因此在取得这份列表后我们还要逐一调用get方法获取具体的好友对象。考虑到在同时进行多个friend接口调用可能触发服务器的防攻击策略,导致被关小黑屋一段时间,所以对friend接口的调用必须排队。

function getAll(){
  var operation = new Async.Operation();
  var friends = [];
  var chain = Async.chain();
  Ajax.get("/friendlist", "")
    .addCallback(function(json) {
      for (var i = 0; i < json.length; i++) {
        chain.next(function() {
          return get(json.shift())
            .addCallback(function(friend) {
              friends.push(friend);
            });
        });
      }
      chain
        .next(function() { operation.yield(friends); })
        .go();
    })
  return operation;
}


在这里,我们假设friendlist接口返回的JSON就是一个Array,在获取到这个Array后构造一个等长的异步调用队列,其中每一个调用的逻辑都是一样的——取出Array中首个好友的名称,用get方法获取对应的好友对象,再将好友对象放入另一个Array中。在调用队列的末端,我们再追加了一个调用,用于返回保存好友对象的Array。

在这个例子当中,我们没有利用调用队列会把上一个函数的结果传递给下一个函数的特性,不过也足够展示调用队列的用途了——让多个底层为Ajax请求的异步操作按照固定的顺序阻塞式执行。

由于底层异步函数返回的就是Async.Operation,你可以直接把它传递给next方法,也可以用匿名函数包装后传递给next方法,而匿名函数内部只需要一个return。

延时函数

在上面的例子中,使用队列是为了避免触发服务器的防攻击策略,但有时候这还是不够的。例如说,服务器要求两个请求之间至少间隔500毫秒,否则就认为是攻击,那么我们就要在队列里面插入这个间隔了。

在原本next方法调用的匿名函数中手动加入setTimeout是一个办法,但为什么我们不写一个辅助函数来解决这类问题呢?让我们来写一个辅助方法并让它和Async.Operation无缝结合起来。

Async.wait = function(delay, context) {
  var operation = new Async.Operation();
  setTimeout(function() {
    operation.yield(context);
  }, delay);
  return operation;
};

Async.Operation.prototype.wait = function(delay, context) {
  return this.next(function(context) {
    return Async.wait(delay, context);
  });
}


在有了这个辅助方法后,我们就可以在上述getAll方法中轻松实现在每个Ajax请求之间间隔500毫秒。在for循环内的加上对wait的调用就可以了。

for (var i = 0; i < json.length; i++) {
  chain
    .wait(500)
    .next(function() {
      return get(json.shift())
        .addCallback(function(friend) {
          friends.push(friend);
        });
  });
}

小结

通过一些简单的例子,我们了解到了Async.Operation常见的使用方式,以及在有需要的时候如何扩展它的功能。希望Async.Operation能够有效帮助大家提高Ajax应用的代码可读性。

最后,如果大家希望将来继续读到类似的JavaScript开发模式相关的文章,不妨考虑订阅我的博客:

4 条评论:

  1. 有了例子代码更方便理解这个框架了,Ajax队列的例子正好解决我当前项目中遇到的问题。
    我想将你的代码修改后用在我的项目中,不会有什么版权问题吧;-)

    回复删除
  2. 没问题。你可以根据你项目的实际情况调整Ajax的异步队列封装方式,甚至做更高层次的封装,这在大型项目中往往十分有用。

    回复删除
  3. 谢谢,在我的项目中有个特殊的需求:一个表格拥有一个上下文菜单而菜单事件要执行的逻辑是由客户自己定义的,在这个菜单的事件中客户会根据实际的业务需要调用一系列的已有函数,为了方便用户理解我想定义两个对象:Action和ActionQueue,用户设置的代码类似于这种写法:
    var q = new ActionQueue();
    q.Add(function() { voidBlockUI(); });
    for (i = 0; i < selectedRows.length; i++) {
    // execute stored procedure AJAXifly
    q.Add(function() { voidExecuteSP('xxxx', selectedRows[i].intRecordID); });
    q.Add(function() { voidHideRow(selectedRows[i].intRecordID); });
    }
    q.Add(function() { voidShowMessage(); });
    q.Add(function() { voidUnBlockUI(); });

    但我不知道该如何修改你的代码,能给些建议吗?
    谢谢!

    回复删除
  4. 我在框架的代码中加入了很多调试代码总算搞明白它的运行原理了,下一步就是按照自己的要求进行修改并做一些封装。

    回复删除