Passagem de mensagem de extensão do Chrome: resposta não enviada

151

Estou tentando passar mensagens entre o script de conteúdo e a extensão

Aqui está o que eu tenho no script de conteúdo

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

E no script de fundo eu tenho

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})
    }
  });

}

Agora, se eu enviar a resposta antes da chamada de ajax na getUrlsfunção, a resposta será enviada com êxito, mas no método de sucesso da chamada de ajax, quando envio a resposta, ela não a envia, quando entra na depuração, posso ver que a porta é nula dentro do código da sendResponsefunção.

Uma aposta
fonte
Armazenar uma referência ao parâmetro sendResponse é crítico. Sem ele, o objeto de resposta fica fora do escopo e não pode ser chamado. Obrigado pelo código que me indicou a solução do meu problema!
TrickiDicki
talvez outra solução seja agrupar tudo dentro de uma função assíncrona com Promise e aguardar pelos métodos assíncronos?
Enrique

Respostas:

348

A partir da documentação parachrome.runtime.onMessage.addListener :

Essa função se torna inválida quando o ouvinte de evento retorna, a menos que você retorne true do ouvinte de evento para indicar que deseja enviar uma resposta de forma assíncrona (isso manterá o canal de mensagens aberto para o outro lado até que sendResponse seja chamado).

Então, você só precisa adicionar return true;após a chamada getUrlspara indicar que você chamará a função de resposta de forma assíncrona.

rsanchez
fonte
isso é correto, eu adicionei uma maneira de automatizar isso na minha resposta
Zig Mandel
62
+1 para isso. Ele me salvou depois de perder 2 dias tentando depurar esse problema. Eu não posso acreditar que isso não é mencionado em tudo no guia de passagem de mensagens em: developer.chrome.com/extensions/messaging
funforums
6
Aparentemente, eu já tive esse problema antes; voltou para perceber que eu já tinha votado nisso. Ele precisa estar em negrito em tamanho grande <blink>e <marquee>em algum lugar da página.
Qix - MONICA FOI ERRADA
2
@funforums FYI, esse comportamento agora está documentado na documentação das mensagens (a diferença está aqui: codereview.chromium.org/1874133002/patch/80001/90002 ).
Rob W
10
Juro que essa é a API mais intuitiva que já usei.
21816 michaelsnowden
8

A resposta aceita está correta, eu só queria adicionar um código de exemplo que simplifique isso. O problema é que a API (na minha opinião) não é bem projetada porque nos obriga os desenvolvedores a saber se uma mensagem específica será tratada de forma assíncrona ou não. Se você lida com muitas mensagens diferentes, isso se torna uma tarefa impossível, porque você nunca sabe se, no fundo de alguma função, um sendResponse passado será chamado de assíncrono ou não. Considere isto:

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

Como posso saber se, no fundo, handleMethod1a chamada será assíncrona ou não? Como alguém que modifica handleMethod1sabe que interromperá uma chamada introduzindo algo assíncrono?

Minha solução é 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;
    }

});

Isso lida automaticamente com o valor de retorno, independentemente de como você escolhe lidar com a mensagem. Observe que isso pressupõe que você nunca se esqueça de chamar a função de resposta. Observe também que o cromo poderia ter automatizado isso para nós, não vejo por que eles não o fizeram.

Zig Mandel
fonte
Um problema é que, às vezes, você não deseja chamar a função de resposta e, nesses casos, deve retornar false . Caso contrário, você está impedindo o Chrome de liberar recursos associados à mensagem.
Rsanchez 02/05/19
Sim, foi por isso que eu disse para não esquecer de ligar para o retorno de chamada. Esse caso especial da sua menção pode ser tratado com a convenção de que o manipulador (handleMethod1 etc) retorna false para indicar o caso "sem resposta" (embora eu prefira sempre responder, mesmo que vazio). Dessa forma, o problema de manutenção é localizado apenas nos casos especiais "sem retorno".
Zig Mandel
8
Não reinvente a roda. Os métodos obsoletos chrome.extension.onRequest/ chrome.exension.sendRequestse comportam exatamente como você descreve. Esses métodos foram descontinuados porque muitos desenvolvedores de extensões NÃO fecharam a porta da mensagem. A API atual (exigindo return true) é um design melhor, porque falhar muito é melhor do que vazar silenciosamente.
Rob W
@ RobW, mas qual é o problema, então? minha resposta impede que o desenvolvedor se esqueça de retornar verdadeiro.
Zig Mandel
@ZigMandel Se você quiser enviar uma resposta, basta usar return true;. Isso não impede que a porta seja limpa se a chamada for sincronizada, enquanto as chamadas assíncronas ainda estão sendo processadas corretamente. O código nesta resposta apresenta complexidade desnecessária sem benefício aparente.
22415 Robert W
2

Você pode usar minha biblioteca https://github.com/lawlietmester/webextension para fazer isso funcionar tanto no Chrome quanto no FF com o Firefox, sem retornos de chamada.

Seu código será parecido com:

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
fonte