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

Lin*_*per 9 javascript asynchronous generator promise firefox-addon-sdk

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

背景

我有一个使用Firefox Add-on SDK编写的Firefox扩展.与SDK一样,代码分为附加脚本内容脚本.这两个脚本具有不同类型的权限:附加脚本可以执行奇特的操作,例如,通过js-ctypes接口调用本机代码,而内容脚本可以与网页交互.然而,附加的脚本和脚本的内容可以只用相互作用彼此通过异步消息传递接口.

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

(编辑:它原来是不是是你只能使用情况exportFunction的内容脚本,请参见下面的评论.)

为了解决这个问题,我在内容脚本中编写了一个"包装器"函数; 这个包装器是我实际导出的函数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});
Run Code Online (Sandbox Code Playgroud)

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

// 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();
}
Run Code Online (Sandbox Code Playgroud)

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

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

我想做的事

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

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

但是,即使在观看了上面链接的视频并阅读有关承诺和发电机的信息之后,我仍然难以理解如何解决这个问题.lengthInBytes返回的版本Promise看起来像:

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

    return new Promise(
        // do something with `lengthInBytesReturned` event???  idk.
    );
}
Run Code Online (Sandbox Code Playgroud)

用户脚本会涉及到类似的东西

var result = yield lengthInBytesPromise("hello");
console.log(result);
Run Code Online (Sandbox Code Playgroud)

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

到目前为止,我已经完成了一个完整的工作示例.

谢谢你的帮助!

Ben*_*Ben 7

这个问题的一个非常优雅的解决方案是以函数的形式出现在下一个版本的JavaScript ECMAScript 7中,它们是s和生成器的结合,可以解释两者的瑕疵.更多关于这个答案的最底层.asyncPromise

我是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);
    });
  });
}
Run Code Online (Sandbox Code Playgroud)

function(resolve, reject) { ... }时的承诺被实例化回调立即调用,并且resolvereject参数是可以用于提供承诺的最终值回调函数.

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

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

lengthInBytesPromise("hello").then(function(length) {
  console.log(result);
});
Run Code Online (Sandbox Code Playgroud)

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

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

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

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);
});
Run Code Online (Sandbox Code Playgroud)

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

spawn函数接受生成器函数并立即调用它以获取生成器对象,然后调用生成器对象的gen.next()方法以获取第一个yielded promise(结果lengthInBytesPromise("hello")),然后等待该promise被实现,然后调用gen.next(result)结果,它为第一个yield表达式(指定的表达式)提供一个值length,并使生成器函数运行到下一个yield表达式(即,yield someOtherPromise(length)),产生下一个承诺,依此类推,直到没有更多的承诺留待等待,因为生成器函数终于返回了.

为了让您了解ES7中的内容,您可以使用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);
});
Run Code Online (Sandbox Code Playgroud)

这里真正发生的一切是async关键字已经取代了spawn函数(和*生成器语法),并且await已经被替换了yield.这不是一个巨大的飞跃,但将这种语法内置到语言中而不是依赖于task.js这样的外部库将会非常好.

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