Przekazanie wiadomości rozszerzenia Chrome: odpowiedź nie została wysłana

151

Próbuję przekazywać komunikaty między skryptem zawartości a rozszerzeniem

Oto, co mam w skrypcie zawartości

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

A w tle mam skrypt

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

}

Teraz, jeśli wyślę odpowiedź przed wywołaniem ajax w getUrlsfunkcji, odpowiedź zostanie wysłana pomyślnie, ale w metodzie sukcesu wywołania ajax, kiedy wysyłam odpowiedź, nie wysyła jej, kiedy przechodzę do debugowania, widzę to port ma wartość null w kodzie sendResponsefunkcji.

Oferta
źródło
Przechowywanie odniesienia do parametru sendResponse jest krytyczne. Bez tego obiekt odpowiedzi wychodzi poza zakres i nie można go wywołać. Dzięki za kod, który podpowiedział mi, jak naprawić mój problem!
TrickiDicki
może innym rozwiązaniem jest zawinięcie wszystkiego wewnątrz funkcji asynchronicznej za pomocą Promise i wywołanie funkcji await dla metod asynchronicznych?
Enrique

Odpowiedzi:

348

Z dokumentacji dlachrome.runtime.onMessage.addListener :

Ta funkcja staje się nieprawidłowa, gdy detektor zdarzeń zwraca, chyba że zwrócisz wartość true z detektora zdarzeń, aby wskazać, że chcesz wysłać odpowiedź asynchronicznie (spowoduje to, że kanał wiadomości będzie otwarty na drugi koniec do momentu wywołania sendResponse).

Musisz więc tylko dodać return true;po wywołaniu to, getUrlsaby wskazać, że będziesz wywoływać funkcję odpowiedzi asynchronicznie.

rsanchez
źródło
to prawda, dodałem sposób, aby zautomatyzować to w mojej odpowiedzi
Zig Mandel
62
+1 za to. Uratowało mnie to po zmarnowaniu 2 dni na debugowanie tego problemu. Nie mogę uwierzyć, że w ogóle nie ma o tym wzmianki w przewodniku po przekazywaniu wiadomości pod adresem: developer.chrome.com/extensions/messaging
funforums
6
Najwyraźniej miałem ten problem wcześniej; wróciłem, aby zdać sobie sprawę, że już za tym głosowałem. To musi być pogrubione, duże <blink>i <marquee>tagi gdzieś na stronie.
Qix - MONICA ZOSTAŁA POŁAMANIANA
2
@funforums FYI, to zachowanie jest teraz udokumentowane w dokumentacji obsługi wiadomości (różnica jest tutaj: codereview.chromium.org/1874133002/patch/80001/90002 ).
Rob W
10
Przysięgam, że to najbardziej nieintuicyjny interfejs API, jakiego kiedykolwiek używałem.
michaelsnowden
8

Przyjęta odpowiedź jest prawidłowa, chciałem tylko dodać przykładowy kod, który to uprości. Problem polega na tym, że API (moim zdaniem) nie jest dobrze zaprojektowane, ponieważ zmusza nas programistów do tego, aby wiedzieć, czy dana wiadomość będzie obsługiwana asynchronicznie, czy nie. Jeśli obsługujesz wiele różnych wiadomości, staje się to niemożliwe, ponieważ nigdy nie wiesz, czy w głębi duszy jakaś funkcja przekazana sendResponse zostanie nazwana asynchroniczną, czy nie. Rozważ to:

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

Skąd mam wiedzieć, czy w głębi duszy handleMethod1połączenie będzie asynchroniczne, czy nie? Jak ktoś, kto modyfikuje, może handleMethod1wiedzieć, że zepsuje dzwoniącego, wprowadzając coś asynchronicznego?

Moje rozwiązanie jest takie:

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

});

Spowoduje to automatyczną obsługę zwracanej wartości, niezależnie od wybranego sposobu obsługi wiadomości. Zauważ, że zakłada to, że nigdy nie zapomnisz wywołać funkcji odpowiedzi. Zauważ też, że chrom mógł to dla nas zautomatyzować, nie rozumiem, dlaczego tego nie zrobił.

Zig Mandel
źródło
Jednym z problemów jest to, że czasami nie będziesz chciał wywoływać funkcji odpowiedzi iw takich przypadkach powinieneś zwrócić false . Jeśli tego nie zrobisz, uniemożliwiasz Chrome zwalnianie zasobów powiązanych z wiadomością.
rsanchez
tak, dlatego powiedziałem, żeby nie zapomnieć o oddzwonieniu. Ten specjalny przypadek, o którym myślisz, można obsłużyć, stosując konwencję, że procedura obsługi (handleMethod1 itp.) Zwraca fałsz, aby wskazać przypadek „brak odpowiedzi” (chociaż Id raczej zawsze udziela odpowiedzi, nawet pustej). W ten sposób problem z konserwowalnością jest zlokalizowany tylko w tych specjalnych przypadkach „bez powrotu”.
Zig Mandel
8
Nie wymyślaj ponownie koła. Przestarzałe chrome.extension.onRequest/ chrome.exension.sendRequestmetody zachowują się dokładnie tak, jak je opisujesz. Te metody są przestarzałe, ponieważ okazuje się, że wielu programistów rozszerzeń NIE zamknęło portu komunikatów. Obecne API (wymagające return true) jest lepszym projektem, ponieważ twarda awaria jest lepsza niż cichy wyciek.
Rob W
@RobW, ale jaki jest problem? moja odpowiedź nie pozwala programistom zapomnieć o zwróceniu prawdy.
Zig Mandel
@ZigMandel Jeśli chcesz wysłać odpowiedź, po prostu użyj return true;. Nie zapobiega to czyszczeniu portu, jeśli wywołanie jest synchronizowane, podczas gdy wywołania asynchroniczne są nadal poprawnie przetwarzane. Kod w tej odpowiedzi wprowadza niepotrzebną złożoność bez widocznych korzyści.
Rob W
2

Możesz użyć mojej biblioteki https://github.com/lawlietmester/webextension, aby ta działała zarówno w Chrome, jak i FF z Firefoksem bez wywołań zwrotnych.

Twój kod będzie wyglądał następująco:

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
źródło