Расширение Chrome Передача сообщений: ответ не отправлен

151

Я пытаюсь передать сообщения между содержимым скрипта и расширением

Вот что у меня есть в контент-скрипте

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

И в фоновом режиме сценарий у меня есть

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

}

Теперь, если я отправляю ответ до вызова ajax в getUrlsфункции, ответ отправляется успешно, но в методе успеха вызова ajax, когда я отправляю ответ, он не отправляет его, когда я иду в отладку, я вижу, что Порт является нулевым внутри кода для sendResponseфункции.

Ставка
источник
Хранение ссылки на параметр sendResponse имеет решающее значение. Без этого объект ответа выходит из области видимости и не может быть вызван. Спасибо за код, который намекал мне на решение моей проблемы!
TrickiDicki
может быть, другое решение - обернуть все внутри асинхронной функции с помощью Promise и вызвать await для асинхронных методов?
Энрике

Ответы:

348

Из документации дляchrome.runtime.onMessage.addListener :

Эта функция становится недействительной при возврате прослушивателя событий, если только вы не вернете истину из прослушивателя событий, чтобы указать, что вы хотите отправить ответ асинхронно (это будет держать канал сообщений открытым для другого конца, пока не будет вызван sendResponse).

Так что вам просто нужно добавить return true;после вызова, getUrlsчтобы указать, что вы будете вызывать функцию ответа асинхронно.

rsanchez
источник
это правильно, я добавил способ автоматизировать это в своем ответе
Зиг Мандель
62
+1 за это. Это спасло меня после того, как я потратил 2 дня, пытаясь решить эту проблему. Я не могу поверить, что об этом вообще не упоминается в руководстве по передаче сообщений по адресу: developer.chrome.com/extensions/messaging
funforums
6
У меня, видимо, была эта проблема раньше; вернулся, чтобы понять, что я уже проголосовал за это. Это должно быть выделено жирным шрифтом <blink>и <marquee>тегами где-то на странице.
Qix - МОНИКА БЫЛА ПОВТОРЕНА
2
@funforums FYI, это поведение теперь задокументировано в документации обмена сообщениями (разница здесь: codereview.chromium.org/1874133002/patch/80001/90002 ).
Роб W
10
Клянусь, это самый неинтуитивный API, который я когда-либо использовал.
michaelsnowden
8

Принятый ответ правильный, я просто хотел добавить пример кода, который упрощает это. Проблема в том, что API (на мой взгляд) не очень хорошо разработан, потому что заставляет нас, разработчиков, знать, будет ли конкретное сообщение обрабатываться асинхронно или нет. Если вы обрабатываете много разных сообщений, это становится невыполнимой задачей, потому что вы никогда не знаете, будет ли в глубине какой-то функции переданный sendResponse вызываться как асинхронный или нет. Учти это:

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

Как я могу узнать, будет handleMethod1ли вызов в глубине асинхронным или нет? Как может кто-то, кто изменяет, handleMethod1знает, что он сломает вызывающую сторону, введя что-то асинхронное?

Мое решение таково:

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

});

Это автоматически обрабатывает возвращаемое значение, независимо от того, как вы решите обработать сообщение. Обратите внимание, что это предполагает, что вы никогда не забудете вызвать функцию ответа. Также обратите внимание, что хром мог автоматизировать это для нас, я не понимаю, почему они этого не сделали.

Зиг Мандель
источник
Одна проблема заключается в том, что иногда вы не захотите вызывать функцию ответа, и в этих случаях вы должны возвращать false . Если вы этого не сделаете, вы не позволите Chrome освободить ресурсы, связанные с сообщением.
rsanchez
да, вот почему я сказал, чтобы не забыть позвонить. Этот особый случай, о котором вы упомянули, может быть обработан при наличии соглашения, согласно которому обработчик (handleMethod1 и т. Д.) Возвращает false, чтобы указать случай «без ответа» (хотя Id, скорее всего, просто всегда дает ответ, даже пустой). Таким образом, проблема ремонтопригодности локализуется только в тех особых случаях «без возврата».
Зиг Мандель
8
Не изобретай велосипед. Устаревшие chrome.extension.onRequest/ chrome.exension.sendRequestметоды ведут себя именно так, как вы описываете. Эти методы устарели, потому что оказывается, что многие разработчики расширений НЕ закрывали порт сообщения. Текущий API (требующий return true) - лучший дизайн, потому что терпеть неудачу лучше, чем молча.
Роб W
@RobW, но в чем проблема? мой ответ мешает разработчику забыть вернуть истину.
Зиг Мандель
@ZigMandel Если вы хотите отправить ответ, просто используйте return true;. Это не препятствует очистке порта, если вызов синхронизирован, в то время как асинхронные вызовы все еще обрабатываются правильно. Код в этом ответе вносит ненужную сложность без видимой выгоды.
Роб W
2

Вы можете использовать мою библиотеку https://github.com/lawlietmester/webextension, чтобы сделать эту работу как в Chrome, так и в FF с помощью Firefox без обратных вызовов.

Ваш код будет выглядеть так:

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 }); });
}) );
Рустам
источник