Mensaje de extensión de Chrome pasando: respuesta no enviada

151

Estoy intentando pasar mensajes entre el script de contenido y la extensión

Esto es lo que tengo en content-script

chrome.runtime.sendMessage({type: "getUrls"}, function(response) {
  console.log(response)
});

Y en el guión de fondo tengo

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    if (request.type == "getUrls"){
      getUrls(request, sender, sendResponse)
    }
});

function getUrls(request, sender, sendResponse){
  var resp = sendResponse;
  $.ajax({
    url: "http://localhost:3000/urls",
    method: 'GET',
    success: function(d){
      resp({urls: d})
    }
  });

}

Ahora, si envío la respuesta antes de la llamada ajax en la getUrlsfunción, la respuesta se envía con éxito, pero en el método de éxito de la llamada ajax cuando envío la respuesta no la envía, cuando entro en la depuración puedo ver que el puerto es nulo dentro del código para la sendResponsefunción.

Una oferta
fuente
Almacenar una referencia al parámetro sendResponse es crítico. Sin él, el objeto de respuesta queda fuera de alcance y no se puede llamar. ¡Gracias por el código que me insinuó para solucionar mi problema!
TrickiDicki
¿Quizás otra solución es envolver todo dentro de una función asincrónica con Promise y call wait para los métodos asincrónicos?
Enrique

Respuestas:

348

De la documentación parachrome.runtime.onMessage.addListener :

Esta función se vuelve inválida cuando el detector de eventos regresa, a menos que devuelva verdadero del detector de eventos para indicar que desea enviar una respuesta de forma asincrónica (esto mantendrá el canal de mensajes abierto al otro extremo hasta que se llame a sendResponse).

Por lo tanto, solo necesita agregar return true;después de la llamada a getUrlspara indicar que llamará a la función de respuesta de forma asincrónica.

rsanchez
fuente
esto es correcto, agregué una forma de automatizar esto en mi respuesta
Zig Mandel
62
+1 por esto. Me ha salvado después de perder 2 días tratando de depurar este problema. No puedo creer que esto no se mencione en absoluto en la guía de paso de mensajes en: developer.chrome.com/extensions/messaging
funforums
66
Aparentemente he tenido este problema antes; volví a darme cuenta de que ya había votado por esto. Esto debe estar en negrita en grande <blink>y <marquee>etiquetas en algún lugar de la página.
Qix - MONICA FUE MALTRATADA el
2
@funforums FYI, este comportamiento ahora está documentado en la documentación de mensajería (la diferencia está aquí: codereview.chromium.org/1874133002/patch/80001/90002 ).
Rob W
10
Juro que esta es la API más poco intuitiva que he usado.
michaelsnowden
8

La respuesta aceptada es correcta, solo quería agregar un código de muestra que simplifique esto. El problema es que la API (en mi opinión) no está bien diseñada porque nos obliga a los desarrolladores a saber si un mensaje en particular se manejará de forma asíncrona o no. Si maneja muchos mensajes diferentes, esto se convierte en una tarea imposible porque nunca se sabe si en el fondo de alguna función un sendResponse pasado se llamará asíncrono o no. Considera esto:

chrome.extension.onMessage.addListener(function (request, sender, sendResponseParam) {
if (request.method == "method1") {
    handleMethod1(sendResponse);
}

¿Cómo puedo saber si en handleMethod1el fondo la llamada será asíncrona o no? ¿Cómo puede alguien que modifica handleMethod1saber que interrumpirá una llamada introduciendo algo asíncrono?

Mi solución es esta:

chrome.extension.onMessage.addListener(function (request, sender, sendResponseParam) {

    var responseStatus = { bCalled: false };

    function sendResponse(obj) {  //dummy wrapper to deal with exceptions and detect async
        try {
            sendResponseParam(obj);
        } catch (e) {
            //error handling
        }
        responseStatus.bCalled= true;
    }

    if (request.method == "method1") {
        handleMethod1(sendResponse);
    }
    else if (request.method == "method2") {
        handleMethod2(sendResponse);
    }
    ...

    if (!responseStatus.bCalled) { //if its set, the call wasn't async, else it is.
        return true;
    }

});

Esto maneja automáticamente el valor de retorno, independientemente de cómo elija manejar el mensaje. Tenga en cuenta que esto supone que nunca olvida llamar a la función de respuesta. También tenga en cuenta que el cromo podría haber automatizado esto para nosotros, no veo por qué no lo hicieron.

Zig Mandel
fuente
Un problema es que a veces no querrá llamar a la función de respuesta, y en esos casos debe devolver falso . Si no lo hace, está evitando que Chrome libere recursos asociados al mensaje.
rsanchez
Sí, por eso dije que no olvide llamar a la devolución de llamada. El caso especial que usted menciona puede manejarse teniendo una convención de que el manejador (handleMethod1, etc.) devuelve falso para indicar el caso de "no respuesta" (aunque Id siempre responde, incluso uno vacío). De esta forma, el problema de mantenibilidad solo se localiza en esos casos especiales de "no retorno".
Zig Mandel
8
No reinventes la rueda. Los métodos chrome.extension.onRequest/ obsoletos se chrome.exension.sendRequestcomportan exactamente como usted describe. Estos métodos están en desuso porque resulta que muchos desarrolladores de extensiones NO cerraron el puerto de mensajes. La API actual (que requiere return true) es un mejor diseño, porque fallar mucho es mejor que tener una fuga silenciosa.
Rob W
@RobW pero ¿cuál es el problema entonces? mi respuesta evita que el desarrollador se olvide de volver verdadero.
Zig Mandel
@ ZigMandel Si desea enviar una respuesta, simplemente use return true;. No evita que se limpie el puerto si la llamada está sincronizada, mientras que las llamadas asíncronas todavía se procesan correctamente. El código en esta respuesta introduce una complejidad innecesaria sin beneficio aparente.
Rob W
2

Puede usar mi biblioteca https://github.com/lawlietmester/webextension para hacer que esto funcione tanto en Chrome como en FF con Firefox sin devoluciones de llamada.

Su código se verá así:

Browser.runtime.onMessage.addListener( request => new Promise( resolve => {
    if( !request || typeof request !== 'object' || request.type !== "getUrls" ) return;

    $.ajax({
        'url': "http://localhost:3000/urls",
        'method': 'GET'
    }).then( urls => { resolve({ urls }); });
}) );
Rustam
fuente