jQuery Promises对PostMessage的异步性的回答

66 阅读4分钟

Visual Website Optimizer的编辑组件使用代理隧道加载一个网站进行编辑。它对什么样的网站可以被载入其中有很大的限制。防火墙后面的网站、本地网络中的网站或HTTP认证后面的网站都不能使用隧道加载。除此以外,即使网站在编辑器中加载,由于JavaScript或AJAX通信的问题,它也有可能在前台中断。

你会问:为什么首先要有一个代理?因为,如果一个页面包含另一个域的iframe,它就不能访问其属性或功能。这是浏览器供应商为用户提供的安全功能,以保护他们的隐私。

问题

因此,我们最近在前端的任务是消除这个麻烦的中间人,为跨域iframe通信找到一个解决方案。我们知道答案是什么:PostMessage API。 只要客户在其网站上集成了VWO跟踪代码,我们就可以直接加载iframe,无需代理,并使用该API与之通信。然而,更大的问题是如何做到这一点。编辑器在用户执行的每项任务中都有大量的父子通信。 当试图使用PostMessage进行通信时,我们面临着一些问题:

  1. 我们的传统代码在所有地方都有父框架和子框架之间的直接通信,也就是说,子框架中的对象和函数是同步访问的。另一方面,PostMessage API是完全异步的,在现有代码基础上实现这样的API几乎意味着重新思考整个逻辑和程序流程。 我们可以预见,这种异步过渡会成为编辑器中很多竞赛条件的原因。
  2. 通常情况下,在向另一个框架发送消息后,我们希望听到回复,为此我们需要一个体面的双向通信。一种可以跟踪发送者和接收者,并且可以使用唯一的标识符(将请求和响应捆绑在一起)在不同的iframes中进行识别。
  3. 由于PostMessage使用字符串信息进行通信(或在最近的浏览器中使用结构上可克隆的对象),它对我们在这种通信中可以发送什么样的数据提出了很大的限制。直接访问DOM节点和发送某些循环对象已经不可能了。

例如,当你在子框架中选择一个元素时,它会在父框架中创建一个新的VWO.Element 实例并要求它打开一个上下文菜单。这段代码看起来是这样的:

  $(elementSelectorPath).click(function() {
    var element = parent.VWO.Element.create(elementSelectorPath);
    parent.VWO.ContextMenu.showForElement(element);
  });

虽然,这在表面上看起来是一个微不足道的问题,但在内心深处,我们面临着一个竞赛条件。Element.create 方法要求子框架为该元素添加一个类,而ContextMenu.showForElement 希望在它被执行时,该类已经被应用。

解决方案

我们得出结论,重构代码以适应异步性将是一项艰巨的任务,我们必须找到另一种方法。我们决定在PostMessage API周围写一个包装器来解决上述三个问题。我们把它叫做please.js 。我们目前正在对它进行最后的润色,然后把它推送给社区。我们的做法是这样的:

  • 我们决定在jQuery Deferred API的基础上建立这个库。虽然延迟对象和承诺并没有完全消除异步,但它们以某种方式弥合了两者之间的差距,使异步代码感觉更加线性和扁平化。因此,利用这个基础,任何一段期望前面的代码已经完全执行的代码,现在都可以不费吹灰之力实现。在上面的例子中,过渡到please.js,看起来是这样的。
  $(elementSelectorPath).click(function() {
    please(parent)
      .call('VWO.Element.create', elementSelectorPath)
      .then(function (element) {
          please(parent)
              .call('VWO.ContextMenu.showForElement', element);
    });
  });

虽然这乍一看是黑客行为,但这是一种快速迭代同步代码并将其转换为使用承诺和回调的方法,而无需对逻辑进行过多考虑。

  • 为了建立一个良好的双向通信,我们把每次通信看作一对消息:一个请求和一个响应。在引擎盖下,我们使用一个时间戳来识别每条消息,并使用该标识符创建一个请求对象。然后我们将请求发送给另一个框架,同时将其存储在当前框架的哈希姆中。然后,另一个框架将接收该请求,执行适当的操作并发回一个响应。在收到响应后,该请求将从哈希姆表中删除。为了使事情变得更容易,我们创建了一组函数来使某些频繁的任务变得更容易。例如,获取/设置一个属性和调用一个函数是我们最常见的任务。这些任务的代码现在看起来像这样。
  please(parent).get('window.location').then(function(location) {
    // use location here
  });

  please(parent).set('foo', 'bar').then(function () {
    // do something here
  });

  // reload the child window.
  var childWindow = $('iframe#child').get(0).contentWindow;
  please(childWindow).call('window.location.reload');

一个范式的转变,但逻辑仍然不受影响。这正是我们想要的:

  • 最后一项任务是个大任务。我们在父框架中有大量的代码直接访问子框架的DOM。虽然这不是一个好的做法,但在建立和改进遗留代码时,经常会遇到这样的问题。有了PostMessage,你就不能再以任何方式访问子框架的DOM了。 但我们想出了一个聪明的解决方案。我们知道jQuery是一个围绕传统DOM的包装器。我们创建了一个围绕jQuery本身的PostMessage包装器!这使得不可能的转变成为可能。
  // set #bar's height in child = foo's height in child
  var pls = please($('iframe#child').get(0).contentWindow);
  pls.$('div#foo').height().then(function (fooHeight) {
    pls.$('div#bar').height(fooHeight);
  });

  // DOM elements are returned back as please.UnserializableObject
  // which can then be passed back to please.$ to do more stuff
  pls.$('<div>hello world</div>').then(function (newDiv) {
    pls.$(newDiv).appendTo('body');
  });

这是我在Wingify举办的一次黑客活动中想到的东西。结果是非常有成效的!

总结

在我个人看来,我相信在如此大的转变中使用承诺,极大地影响了我对前端Web开发的思考方式。它是快速异步开发的一种方式,同时又有扁平化的类似同步的代码结构。

please.js 承诺 "将很快被开源,所以请留意博客上的更新。