Message d'extension Chrome transmis: réponse non envoyée

151

J'essaye de passer des messages entre le script de contenu et l'extension

Voici ce que j'ai dans content-script

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

Et dans le script d'arrière-plan que j'ai

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

}

Maintenant, si j'envoie la réponse avant l'appel ajax dans la getUrlsfonction, la réponse est envoyée avec succès, mais dans la méthode de succès de l'appel ajax lorsque j'envoie la réponse, elle ne l'envoie pas, quand je vais au débogage, je peux voir que le port est nul dans le code de la sendResponsefonction.

Abid
la source
Le stockage d'une référence au paramètre sendResponse est essentiel. Sans lui, l'objet de réponse est hors de portée et ne peut pas être appelé. Merci pour le code qui m'a suggéré de résoudre mon problème!
TrickiDicki
peut-être qu'une autre solution consiste à tout envelopper dans une fonction asynchrone avec Promise et à appeler await pour les méthodes asynchrones?
Enrique

Réponses:

348

De la documentation pourchrome.runtime.onMessage.addListener :

Cette fonction devient invalide lorsque l'écouteur d'événement revient, à moins que vous ne renvoyiez true à partir de l'écouteur d'événement pour indiquer que vous souhaitez envoyer une réponse de manière asynchrone (cela gardera le canal de message ouvert à l'autre extrémité jusqu'à ce que sendResponse soit appelé).

Il vous suffit donc d'ajouter return true;après l'appel à getUrlspour indiquer que vous appellerez la fonction de réponse de manière asynchrone.

rsanchez
la source
c'est correct, j'ai ajouté un moyen d'automatiser cela dans ma réponse
Zig Mandel
62
+1 pour cela. Cela m'a sauvé après avoir perdu 2 jours à essayer de déboguer ce problème. Je ne peux pas croire que cela ne soit pas du tout mentionné dans le guide de passage des messages à: developer.chrome.com/extensions/messaging
funforums
6
J'ai apparemment déjà eu ce problème; est revenu pour réaliser que j'avais déjà voté pour cela. Cela doit être en gras en grand <blink>et <marquee>balises quelque part sur la page.
Qix - MONICA A ÉTÉ BRUYÉ
2
@funforums FYI, ce comportement est maintenant documenté dans la documentation de la messagerie (la différence est ici: codereview.chromium.org/1874133002/patch/80001/90002 ).
Rob W
10
Je jure que c'est l'API la plus peu intuitive que j'aie jamais utilisée.
michaelsnowden
8

La réponse acceptée est correcte, je voulais juste ajouter un exemple de code qui simplifie cela. Le problème est que l'API (à mon avis) n'est pas bien conçue car elle nous oblige les développeurs à savoir si un message particulier sera traité de manière asynchrone ou non. Si vous gérez de nombreux messages différents, cela devient une tâche impossible car vous ne savez jamais si au fond d'une fonction une sendResponse transmise sera appelée async ou non. Considère ceci:

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

Comment puis-je savoir si au fond, handleMethod1l'appel sera asynchrone ou non? Comment quelqu'un qui modifie peut-il handleMethod1savoir qu'il brisera un appelant en introduisant quelque chose d'async?

Ma solution est la suivante:

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

});

Cela gère automatiquement la valeur de retour, quelle que soit la façon dont vous choisissez de gérer le message. Notez que cela suppose que vous n'oublierez jamais d'appeler la fonction de réponse. Notez également que le chrome aurait pu automatiser cela pour nous, je ne vois pas pourquoi ils ne l'ont pas fait.

Zig Mandel
la source
Un problème est que parfois vous ne voudrez pas appeler la fonction de réponse, et dans ces cas, vous devez retourner false . Si vous ne le faites pas, vous empêchez Chrome de libérer les ressources associées au message.
rsanchez
oui, c'est pourquoi j'ai dit de ne pas oublier d'appeler le rappel. Ce cas spécial que vous mentionnez peut être traité en ayant une convention selon laquelle le gestionnaire (handleMethod1 etc) retourne false pour indiquer le cas "pas de réponse" (bien que Id fasse plutôt toujours une réponse, même vide). De cette façon, le problème de maintenabilité n'est localisé que dans les cas spéciaux «sans retour».
Zig Mandel
8
Ne réinventez pas la roue. Les méthodes chrome.extension.onRequest/ obsolètes chrome.exension.sendRequestse comportent exactement comme vous le décrivez. Ces méthodes sont obsolètes car il s'avère que de nombreux développeurs d'extensions n'ont PAS fermé le port de message. L'API actuelle (exigeant return true) est une meilleure conception, car échouer dur est mieux que de fuir silencieusement.
Rob W
@RobW mais quel est le problème alors? ma réponse empêche le développeur d'oublier de retourner vrai.
Zig Mandel
@ZigMandel Si vous souhaitez envoyer une réponse, utilisez simplement return true;. Cela n'empêche pas le nettoyage du port si l'appel est synchronisé, alors que les appels asynchrones sont toujours traités correctement. Le code de cette réponse introduit une complexité inutile sans avantage apparent.
Rob W
2

Vous pouvez utiliser ma bibliothèque https://github.com/lawlietmester/webextension pour que cela fonctionne à la fois dans Chrome et FF avec Firefox, sans rappel.

Votre code ressemblera à:

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
la source