3

Ich habe eine Funktion, die ich auf der Seite ausführen möchte, indem ich chrome.tabs.executeScript benutze, das von einem Browser-Aktions-Popup läuft. Die Berechtigungen korrekt eingerichtet sind und es funktioniert gut mit einem synchronen Rückruf:Verwenden von chrome.tabs.executeScript zum Ausführen einer asynchronen Funktion

chrome.tabs.executeScript(
    tab.id, 
    { code: `(function() { 
     // Do lots of things 
     return true; 
    })()` }, 
    r => console.log(r[0])); // Logs true 

Das Problem ist, dass die Funktion, die ich durchläuft mehrere Rückrufe nennen wollen, so will ich async und await verwenden:

chrome.tabs.executeScript(
    tab.id, 
    { code: `(async function() { 
     // Do lots of things with await 
     return true; 
    })()` }, 
    async r => { 
     console.log(r); // Logs array with single value [Object] 
     console.log(await r[0]); // Logs empty Object {} 
    }); 

Das Problem ist, dass das Rückrufergebnis r. Es sollte ein Array von Skript-Ergebnissen sein, also erwarte ich, dass r[0] ein Versprechen ist, das verrechnet wird, wenn das Skript beendet ist.

Die Promise-Syntax (mit) funktioniert auch nicht.

Wenn ich die exakt gleiche Funktion auf der Seite ausführen, gibt es ein Versprechen wie erwartet zurück und kann erwartet werden.

Irgendeine Idee, was ich falsch mache und gibt es einen Weg um es herum?

+1

Es wäre interessant gewesen wenn das funktioniert hätte, aber das hätte ich nicht erwartet. Der Code im Inhaltsskript und der Code im Hintergrundkontext (Popup) laufen in völlig getrennten Prozessen ab. Ich wäre überrascht gewesen, wenn es gelänge, eine Antwort von einem asynchronen Inhaltsskript zu erwarten. Sie müssen [Message Passing] (https://developer.chrome.com/extensions/messaging) verwenden. – Makyen

+1

async/await ist nur ein syntaktischer Zucker, der das Event-Loop-basierte Verhalten der js-Engine nicht verändert, so dass es nicht funktioniert. – wOxxOm

+0

In Firefox Web Extensions gibt 'chrome.tabs.executeScript' ein Versprechen zurück, und gemäß der MDN-Dokumentation ist dies kompatibel mit der Funktionsweise von Chrome. Ich habe diesbezüglich jedoch keine brauchbare Chrome-Dokumentation gefunden, aber möglicherweise etwas zu beachten –

Antwort

3

Das Problem ist, dass Ereignisse und systemeigene Objekte nicht direkt zwischen der Seite und der Erweiterung verfügbar sind. Im Wesentlichen erhalten Sie eine serialisierte Kopie, etwas wie Sie, wenn Sie tun.

Das bedeutet, einige native Objekte (zum Beispiel new Error oder new Promise) wird (werden {}) geleert werden, werden die Ereignisse verloren und keine Umsetzung der Versprechen kann über die Grenze arbeiten.

Die Lösung ist chrome.runtime.sendMessage zu verwenden, um die Nachricht in dem Skript zurückzukehren, und chrome.runtime.onMessage.addListener in popup.js für sie zu hören:

chrome.tabs.executeScript(
    tab.id, 
    { code: `(async function() { 
     // Do lots of things with await 
     let result = true; 
     chrome.runtime.sendMessage(result, function (response) { 
      console.log(response); // Logs 'true' 
     }); 
    })()` }, 
    async emptyPromise => { 

     // Create a promise that resolves when chrome.runtime.onMessage fires 
     const message = new Promise(resolve => { 
      const listener = request => { 
       chrome.runtime.onMessage.removeListener(listener); 
       resolve(request); 
      }; 
      chrome.runtime.onMessage.addListener(listener); 
     }); 

     const result = await message; 
     console.log(result); // Logs true 
    }); 

Ich habe extended this into a function chrome.tabs.executeAsyncFunction (als Teil der chrome-extension-async, die 'promisifies' die ganze API):

:

function setupDetails(action, id) { 
    // Wrap the async function in an await and a runtime.sendMessage with the result 
    // This should always call runtime.sendMessage, even if an error is thrown 
    const wrapAsyncSendMessage = action => 
     `(async function() { 
    const result = { asyncFuncID: '${id}' }; 
    try { 
     result.content = await (${action})(); 
    } 
    catch(x) { 
     // Make an explicit copy of the Error properties 
     result.error = { 
      message: x.message, 
      arguments: x.arguments, 
      type: x.type, 
      name: x.name, 
      stack: x.stack 
     }; 
    } 
    finally { 
     // Always call sendMessage, as without it this might loop forever 
     chrome.runtime.sendMessage(result); 
    } 
})()`; 

    // Apply this wrapper to the code passed 
    let execArgs = {}; 
    if (typeof action === 'function' || typeof action === 'string') 
     // Passed a function or string, wrap it directly 
     execArgs.code = wrapAsyncSendMessage(action); 
    else if (action.code) { 
     // Passed details object https://developer.chrome.com/extensions/tabs#method-executeScript 
     execArgs = action; 
     execArgs.code = wrapAsyncSendMessage(action.code); 
    } 
    else if (action.file) 
     throw new Error(`Cannot execute ${action.file}. File based execute scripts are not supported.`); 
    else 
     throw new Error(`Cannot execute ${JSON.stringify(action)}, it must be a function, string, or have a code property.`); 

    return execArgs; 
} 

function promisifyRuntimeMessage(id) { 
    // We don't have a reject because the finally in the script wrapper should ensure this always gets called. 
    return new Promise(resolve => { 
     const listener = request => { 
      // Check that the message sent is intended for this listener 
      if (request && request.asyncFuncID === id) { 

       // Remove this listener 
       chrome.runtime.onMessage.removeListener(listener); 
       resolve(request); 
      } 

      // Return false as we don't want to keep this channel open https://developer.chrome.com/extensions/runtime#event-onMessage 
      return false; 
     }; 

     chrome.runtime.onMessage.addListener(listener); 
    }); 
} 

chrome.tabs.executeAsyncFunction = async function (tab, action) { 

    // Generate a random 4-char key to avoid clashes if called multiple times 
    const id = Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); 

    const details = setupDetails(action, id); 
    const message = promisifyRuntimeMessage(id); 

    // This will return a serialised promise, which will be broken 
    await chrome.tabs.executeScript(tab, details); 

    // Wait until we have the result message 
    const { content, error } = await message; 

    if (error) 
     throw new Error(`Error thrown in execution script: ${error.message}. 
Stack: ${error.stack}`) 

    return content; 
} 

diese executeAsyncFunction kann dann wie folgt aufgerufen werden

This wickelt die chrome.tabs.executeScript und chrome.runtime.onMessage.addListener, und wickelt das Skript in einem try - finally vor chrome.runtime.sendMessage Aufruf der Versprechen zu lösen.

+0

Ich würde die onMessage Listener innerhalb der Versprechen hinzufügen und es" auflösen "es richtig (und trennen Sie es) anstelle eines Timers . – wOxxOm

+0

@wOxxOm Ich verwende derzeit kein Versprechen und ich konnte nicht finden, wie man den Ereignis-Listener entfernt, um aufzuräumen. Irgendwelche Hinweise, wie man das macht? PR zum verknüpften GitHub-Projekt sind ebenfalls willkommen :-) – Keith

+0

Alle Chrome-Ereignisse unterstützen removeListener mit der gleichen Funktionsreferenz, z. 'foo.addListener (Funktionsleiste (msg) {foo.removeListener (bar); auflösen (msg)})' (kann natürlich mit const und Pfeilen umgeschrieben werden). – wOxxOm

Verwandte Themen