使用generator + promises在Firefox SDK插件中进行“模拟同步”通信

时间:2014-12-03 01:55:21

标签: javascript asynchronous promise generator firefox-addon-sdk

TL; DR:有没有办法重写这个基于回调的JavaScript代码来代替使用promises和生成器?

背景

我使用Firefox Add-on SDK编写了Firefox扩展程序。与SDK一样,代码分为加载项脚本content script。这两个脚本具有不同类型的权限:附加脚本可以执行奇特的操作,例如,通过js-ctypes接口调用本机代码,而内容脚本可以与网页交互。但是,附加脚本和内容脚本只能通过异步message-passing interface相互交互

我希望能够在普通的,无特权的网页上调用用户脚本中的扩展代码。这可以使用一种名为exportFunction的机制来完成,该机制允许将一个函数从扩展代码导出到用户代码。到现在为止还挺好。 但是,只能在内容脚本中使用exportFunction,而不能使用附加脚本。这没关系,除了我需要导出的函数需要使用前面提到的js- ctypes接口,只能在附加脚本中完成。

(编辑:事实证明只能在内容脚本中使用exportFunction。请参阅下面的评论。) < / p>

为了解决这个问题,我在内容脚本中编写了一个“包装器”函数;这个包装器是我实际通过exportFunction导出的函数。然后,我通过将消息传递给附加脚本,使包装函数在附加脚本中调用“真实”函数。这是内容脚本的样子;它正在导出函数lengthInBytes

// content script

function lengthInBytes(arg, callback) {
    self.port.emit("lengthInBytesCalled", arg);

    self.port.on("lengthInBytesReturned", function(result) {
        callback(result);
    });
}

exportFunction(lengthInBytes, unsafeWindow, {defineAs: "lengthInBytes",
                                             allowCallbacks: true});

这是附加脚本,其中定义了lengthInBytes的“真实”版本。此处的代码侦听内容脚本以向其发送lengthInBytesCalled消息,然后调用lengthInBytes的实际版本,并将结果发回lengthInBytesReturned消息。 (在现实生活中,当然,我可能不需要使用js-ctypes来获取字符串的长度;这只是一个更有趣的C库调用的替身。使用你的想象力。:))< / p>

// add-on script

// Get "chrome privileges" to access the Components object.
var {Cu, Cc, Ci} = require("chrome");

Cu.import("resource://gre/modules/ctypes.jsm");
Cu.import("resource://gre/modules/Services.jsm");

var pageMod = require("sdk/page-mod");
var data = require("sdk/self").data;
pageMod.PageMod({
    include: ["*", "file://*"],
    attachTo: ["existing", "top"],
    contentScriptFile: data.url("content.js"),
    contentScriptWhen: "start", // Attach the content script before any page script loads.

    onAttach: function(worker) {
        worker.port.on("lengthInBytesCalled", function(arg) {
            let result = lengthInBytes(arg);
            worker.port.emit("lengthInBytesReturned", result);
        });
    }
});

function lengthInBytes(str) {
    // str is a JS string; convert it to a ctypes string.
    let cString = ctypes.char.array()(str);

    libc.init();
    let length = libc.strlen(cString); // defined elsewhere
    libc.shutdown();

    // `length` is a ctypes.UInt64; turn it into a JSON-serializable
    // string before returning it.
    return length.toString();
}

最后,用户脚本(仅在安装了扩展程序时才有效)如下所示:

// user script, on an ordinary web page
lengthInBytes("hello", function(result) {
    console.log("Length in bytes: " + result);
});

我想做什么

现在,用户脚本中对lengthInBytes的调用是异步调用;而不是返回结果,它在其回调参数中“返回”其结果。但是,在看到this video关于使用promises和生成器来使异步代码更容易理解之后,我想知道如何以这种方式重写这段代码。

具体来说,我想要的是lengthInBytes返回Promise,它以某种方式表示lengthInBytesReturned消息的最终有效负载。然后,在用户脚本中,我有一个评估yield lengthInBytes("hello")的生成器以获得结果。

但是,即使在观看上面链接的视频并阅读有关承诺和发电机之后,我仍然难以理解如何解决这个问题。返回lengthInBytes的{​​{1}}版本如下所示:

Promise

并且用户脚本将涉及类似

的内容
function lengthInBytesPromise(arg) {
    self.port.emit("lengthInBytesCalled", arg);

    return new Promise(
        // do something with `lengthInBytesReturned` event???  idk.
    );
}

但这和我能够弄清楚的一样多。我将如何编写此代码,以及调用它的用户脚本是什么样的?我想做什么甚至可能?

A complete working example of what I have so far is here.

感谢您的帮助!

2 个答案:

答案 0 :(得分:7)

这个问题的一个非常优雅的解决方案是以async functions的形式出现在下一个 next 版本的JavaScript ECMAScript 7中,它是Promise的结合s和发电机,两者的疣糖。更多关于这个答案的最底层。

我是Regenerator的作者,这是一个在今天的浏览器中支持async函数的转换器,但我意识到建议你在附加开发过程中引入编译步骤可能有点过头了。 ,所以我会把注意力集中在你实际问的问题上:如何设计一个合理的Promise - 返回API,以及使用这种API最好的方法是什么?

首先,我将如何实施lengthInBytesPromise

function lengthInBytesPromise(arg) {
  self.port.emit("lengthInBytesCalled", arg);

  return new Promise(function(resolve, reject) {
    self.port.on("lengthInBytesReturned", function(result) {
      resolve(result);
    });
  });
}

在实例化promise时立即调用function(resolve, reject) { ... }回调,resolvereject参数是可用于为promise提供最终值的回调函数。 / p>

如果在这个例子中有可能失败,你可以将Error对象传递给reject回调,但看起来这个操作是绝对可靠的,所以我们可以在这里忽略这种情况

这就是API如何创建承诺,但消费者如何使用这样的API?在您的内容脚本中,最简单的方法是调用lengthInBytesPromise并直接与生成的Promise进行交互:

lengthInBytesPromise("hello").then(function(length) {
  console.log(result);
});

在这种风格中,你将依赖于lengthInBytesPromise结果的代码放在一个回传函数中传递给promise的.then方法,这可能看起来不像是对回调地狱的巨大改进,但如果您链接更长的一系列异步操作,至少缩进更易于管理:

lengthInBytesPromise("hello").then(function(length) {
  console.log(result);
  return someOtherPromise(length);
}).then(function(resultOfThatOtherPromise) {
  return yetAnotherPromise(resultOfThatOtherPromise + 1);
}).then(function(finalResult) {
  console.log(finalResult);
});

生成器可以帮助减少样板,但是需要额外的运行时支持。可能最简单的方法是使用Dave Herman的task.js library

spawn(function*() { // Note the *; this is a generator function!
  var length = yield lengthInBytesPromise("hello");
  var resultOfThatOtherPromise = yield someOtherPromise(length);
  var finalResult = yield yetAnotherPromise(resultOfThatOtherPromise + 1);
  console.log(finalResult);
});

这段代码更短,回调更少,这是肯定的。你可以猜到,大部分魔法都被简单地移到了spawn函数中,但它的实现实际上非常简单。

spawn函数接受生成器函数并立即调用它以获取生成器对象,然后调用生成器对象的gen.next()方法以获得第一个yield版本的承诺( lengthInBytesPromise("hello"))的结果,然后等待该承诺得到满足,然后使用结果调用gen.next(result),该结果为第一个yield表达式(分配给{{1}的表达式提供值并且导致生成器函数运行到下一个length表达式(即yield),产生下一个承诺,依此类推,直到没有更多的承诺等待,因为生成器函数最终返回。

为了让您了解ES7中的内容,以下是使用yield someOtherPromise(length)函数实现完全相同的功能的方法:

async

这里真正发生的是async function process(arg) { var length = await lengthInBytesPromise(arg); var resultOfThatOtherPromise = await someOtherPromise(length); var finalResult = await yetAnotherPromise(resultOfThatOtherPromise + 1); return finalResult; } // An async function always returns a Promise for its own return value. process(arg).then(function(finalResult) { console.log(finalResult); }); 关键字已取代async函数(以及spawn生成器语法),*已替换await }。这不是一个巨大的飞跃,但将语法内置到语言中而不必依赖于task.js之类的外部库将非常好。

如果您对使用yield函数而不是task.js感到兴奋,那么请务必查看Regenerator

答案 1 :(得分:2)

我认为Promise是通过将原始回调包装在resolve / reject函数中构建的:

function lengthInBytesPromise(arg) {
    self.port.emit("lengthInBytesCalled", arg);

    let returnVal = new Promise(function(resolve, reject) {
        self.port.on("lengthInBytesReturned", function(result) {
            if (result) { // maybe some kind of validity check
                resolve(result);
            } else {
                reject("Something went wrong?");
            }
        }
    });

    return returnVal;
}

基本上,它会创建Promise并立即返回它,而Promise的内部启动然后处理异步任务。我认为在一天结束时,有人必须采用回调式代码并将其包装起来。

您的用户将执行类似

的操作
lengthInBytesPromise(arg).then(function(result) {
    // do something with the result
});