javascript - uncaught - El uso de generadores+promesas para hacer una comunicación "sincrónica simulada" en/con un complemento de Firefox SDK
node js await catch (2)
Creo que la promesa se construye envolviendo su devolución de llamada original dentro de la función de resolución / rechazo:
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;
}
Básicamente, crearía la Promesa y la devolvería de inmediato, mientras que el interior de la Promesa se inicia y luego maneja la tarea asincrónica. Creo que al final del día alguien tiene que tomar el código de devolución de llamada y finalizarlo.
Su usuario entonces haría algo como
lengthInBytesPromise(arg).then(function(result) {
// do something with the result
});
TL; DR: ¿Hay alguna forma de reescribir este código JavaScript de devolución de llamada para usar promesas y generadores en su lugar?
Fondo
Tengo una extensión de Firefox escrita usando el SDK Add-on de Firefox. Como es habitual en el SDK, el código se divide en un script de complemento y un script de contenido . Los dos scripts tienen diferentes tipos de privilegios: los scripts complementarios pueden hacer cosas sofisticadas como, por ejemplo, llamar al código nativo a través de la interfaz js-ctypes , mientras que los scripts de contenido pueden interactuar con páginas web. Sin embargo, los scripts complementarios y los scripts de contenido solo pueden interactuar entre sí a través de una interfaz de paso de mensajes asíncrona.
Deseo poder llamar al código de extensión desde un script de usuario en una página web ordinaria y sin privilegios. Esto se puede hacer utilizando un mecanismo llamado exportFunction
que permite exportar una función desde el código de extensión al código de usuario. Hasta aquí todo bien. Sin embargo, solo se puede usar Eso estaría bien, excepto que la función que necesito exportar necesita usar la interfaz js-ctypes antes mencionada, que solo se puede hacer en una secuencia de comandos complementaria. exportFunction
en un script de contenido, no un script complementario.
(Editar: no es el caso de que solo puedas usar exportFunction
en un script de contenido. Mira el comentario a continuación).
Para evitar esto, escribí una función de "envoltura" en el script de contenido; este contenedor es la función que realmente exporto a través de exportFunction
. Luego hago que la función contenedora llame a la función "real", en el script del complemento, pasando un mensaje al script del complemento. Así es como se ve el script de contenido; está exportando la función 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});
Y aquí está el script complementario, donde se define la versión "real" de lengthInBytes
. El código aquí escucha la secuencia de comandos de contenido para enviar un mensaje lengthInBytesCalled
, luego llama a la versión real de lengthInBytes
y devuelve el resultado en un mensaje lengthInBytesReturned
. (En la vida real, por supuesto, probablemente no necesitaría usar js-ctypes para obtener la longitud de una cadena, esto es solo un sustituto para una llamada a la biblioteca C más interesante. Usa tu imaginación :))
// 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();
}
Finalmente, la secuencia de comandos del usuario (que solo funcionará si la extensión está instalada) se ve así:
// user script, on an ordinary web page
lengthInBytes("hello", function(result) {
console.log("Length in bytes: " + result);
});
Lo que quiero hacer
Ahora, la llamada a lengthInBytes
en la secuencia de comandos del usuario es una llamada asincrónica; en lugar de devolver un resultado, "devuelve" su resultado en su argumento de devolución de llamada. Pero, después de ver este video sobre el uso de promesas y generadores para hacer que el código asíncrono sea más fácil de entender, me pregunto cómo volver a escribir este código con ese estilo.
Específicamente, lo que quiero es que lengthInBytes
devuelva una Promise
que de alguna manera represente la carga útil final del mensaje lengthInBytesReturned
. Luego, en la secuencia de comandos del usuario, tendría un generador que evaluaba el yield lengthInBytes("hello")
para obtener el resultado.
Pero, incluso después de ver el video arriba mencionado y leer acerca de promesas y generadores, todavía estoy perplejo acerca de cómo conectar esto. Una versión de lengthInBytes
que devuelve una Promise
sería algo así como:
function lengthInBytesPromise(arg) {
self.port.emit("lengthInBytesCalled", arg);
return new Promise(
// do something with `lengthInBytesReturned` event??? idk.
);
}
y el script del usuario implicaría algo así como
var result = yield lengthInBytesPromise("hello");
console.log(result);
pero eso es todo lo que he podido descifrar. ¿Cómo escribiría este código y cómo se vería el script del usuario que lo llama? ¿Es lo que quiero hacer posible?
Un ejemplo completo de trabajo de lo que tengo hasta ahora está aquí.
¡Gracias por tu ayuda!
Una solución realmente elegante a este problema viene en la siguiente próxima versión de JavaScript, ECMAScript 7, en forma de funciones async
, que son un matrimonio de Promise
y generadores que azuzan las verrugas de ambos. Más sobre eso en la parte inferior de esta respuesta.
Soy el autor de Regenerator , un transpiler que admite funciones async
en los navegadores de hoy, pero me doy cuenta de que podría ser demasiado costoso sugerirle que introduzca un paso de compilación en su proceso de desarrollo de complementos, así que me centraré en las preguntas que En realidad, estoy preguntando: ¿cómo se diseña una API sensible de recuperación de promesas y cuál es la forma más agradable de consumir dicha API?
Antes que nada, así es como implementaría lengthInBytesPromise
:
function lengthInBytesPromise(arg) {
self.port.emit("lengthInBytesCalled", arg);
return new Promise(function(resolve, reject) {
self.port.on("lengthInBytesReturned", function(result) {
resolve(result);
});
});
}
La devolución de llamada de function(resolve, reject) { ... }
se invoca inmediatamente cuando se crea una instancia de la promesa, y los parámetros de resolve
y reject
son funciones de devolución de llamada que se pueden utilizar para proporcionar el valor final de la promesa.
Si hubiera alguna posibilidad de falla en este ejemplo, podría pasar un objeto Error
a la devolución de llamada reject
, pero parece que esta operación es infalible, por lo que podemos ignorar ese caso aquí.
Entonces, así es como una API crea promesas, pero ¿cómo consumen los consumidores esa API? En su script de contenido, lo más simple es llamar a lengthInBytesPromise
e interactuar con la Promise
resultante directamente:
lengthInBytesPromise("hello").then(function(length) {
console.log(result);
});
En este estilo, coloque el código que depende del resultado de lengthInBytesPromise
en una función de devolución de llamada pasada al método .then
de la promesa, que puede no parecer una gran mejora sobre el infierno de devolución de llamada, pero al menos la sangría es más manejable si estás encadenando una serie más larga de operaciones asincrónicas:
lengthInBytesPromise("hello").then(function(length) {
console.log(result);
return someOtherPromise(length);
}).then(function(resultOfThatOtherPromise) {
return yetAnotherPromise(resultOfThatOtherPromise + 1);
}).then(function(finalResult) {
console.log(finalResult);
});
Los generadores pueden ayudar a reducir el texto estándar aquí, pero se necesita soporte de tiempo de ejecución adicional. Probablemente el enfoque más fácil es utilizar la biblioteca task.js de Dave Herman:
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);
});
Este código es mucho más corto y menos de devolución de llamada, eso es seguro. Como se puede adivinar, la mayoría de la magia simplemente se ha movido a la función de spawn
, pero su implementación es bastante sencilla.
La función spawn
toma una función de generador e invoca de inmediato para obtener un objeto generador, luego invoca el método gen.next()
del objeto generador para obtener la primera promesa de yield
(el resultado de lengthInBytesPromise("hello")
), luego espera a que se cumpla esa promesa, luego invoca gen.next(result)
con el resultado, que proporciona un valor para la primera expresión de yield
(el asignado a la length
) y hace que la función del generador se ejecute hasta la siguiente expresión de yield
( es decir, yield someOtherPromise(length)
), produciendo la próxima promesa, y así sucesivamente, hasta que no queden más promesas que esperar, porque la función del generador finalmente regresó.
Para darle una idea de lo que viene en ES7, esta es la forma en que puede usar una función async
para implementar exactamente lo mismo:
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);
});
Todo lo que realmente está sucediendo aquí es que la palabra clave async
ha reemplazado la función spawn
(y la sintaxis del generador), y await
ha reemplazado el yield
. No es un gran salto, pero será realmente agradable tener esta sintaxis integrada en el lenguaje en lugar de tener que depender de una biblioteca externa como task.js.
Si está entusiasmado con el uso de las funciones async
lugar de task.js, ¡entonces sin dudas echa un vistazo a Regenerator !