【问题标题】:Using chrome.tabs.executeScript to execute an async function使用 chrome.tabs.executeScript 执行异步函数
【发布时间】:2017-08-25 22:48:01
【问题描述】:

我想在页面中使用chrome.tabs.executeScript 执行一个函数,该函数从浏览器操作弹出窗口运行。权限设置正确,并且可以与同步回调一起正常工作:

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

问题是我要调用的函数要经过几次回调,所以我想用asyncawait

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

问题是回调结果r。它应该是一个脚本结果数组,所以我希望r[0] 是一个在脚本完成时解析的承诺。

Promise 语法(使用.then())也不起作用。

如果我在页面中执行完全相同的函数,它会按预期返回一个承诺并且可以等待。

知道我做错了什么吗?有什么办法可以解决吗?

【问题讨论】:

  • 如果这可行的话会很有趣,但我没想到会这样。内容脚本中的代码和后台上下文(弹出)中的代码在完全独立的进程中运行。如果它对await 来自async 内容脚本的响应起作用,我会感到惊讶。您需要使用Message Passing
  • async/await 只是一个语法糖,它不会修改基于事件循环的 js 引擎行为,因此它不会工作。
  • 在 Firefox Web Extensions 中,chrome.tabs.executeScript 返回一个 Promise ,并且,根据 MDN 文档,这也与 chrome 的工作方式兼容——不过,我还没有找到任何有用的 Chrome 文档,但这可能需要考虑
  • @JaromandaX 我正在使用chrome-extension-async 将所有 Chrome API 的回调函数转换为 Promise。使用这个chrome.tabs.executeScript 已经返回了一个promise,它在API 回调触发时解析(如果chrome.runtime.lastError 被填充,则拒绝)。这无助于我得到这个脚本的结果。
  • @wOxxOm 是的,我希望得到PromiseSymbol.iterator 的承诺。

标签: javascript google-chrome-extension async-await es6-promise


【解决方案1】:

问题在于页面和扩展之间不能直接使用事件和原生对象。基本上你会得到一个序列化的副本,就像你做JSON.parse(JSON.stringify(obj))一样。

这意味着一些本机对象(例如 new Errornew Promise)将被清空(变为 {}),事件丢失并且承诺的任何实现都无法跨边界工作。

解决方法是在脚本中使用chrome.runtime.sendMessage返回消息,在popup.js中使用chrome.runtime.onMessage.addListener监听:

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

我有 extended this into a function chrome.tabs.executeAsyncFunction(作为 chrome-extension-async 的一部分,它“承诺”了整个 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;
}

这个executeAsyncFunction 然后可以这样调用:

const result = await chrome.tabs.executeAsyncFunction(
    tab.id, 
    // Async function to execute in the page
    async function() { 
        // Do lots of things with await
        return true; 
    });

This 包装 chrome.tabs.executeScriptchrome.runtime.onMessage.addListener,并将脚本包装在 try-finally 中,然后调用 chrome.runtime.sendMessage 来解决承诺。

【讨论】:

    【解决方案2】:

    将承诺从页面传递到内容脚本不起作用,解决方案是使用chrome.runtime.sendMessage 并仅在两个世界之间发送简单数据,例如:

    function doSomethingOnPage(data) {
      fetch(data.url).then(...).then(result => chrome.runtime.sendMessage(result));
    }
    
    let data = JSON.stringify(someHash);
    chrome.tabs.executeScript(tab.id, { code: `(${doSomethingOnPage})(${data})` }, () => {
      new Promise(resolve => {
        chrome.runtime.onMessage.addListener(function listener(result) {
          chrome.runtime.onMessage.removeListener(listener);
          resolve(result);
        });
      }).then(result => {
        // we have received result here.
        // note: async/await are possible but not mandatory for this to work
        logger.error(result);
      }
    });
    

    【讨论】:

    • 干杯,但我不确定这个答案补充了什么,已经接受的答案还没有。
    • 您通常是对的@Keith,我只是想添加一个更简短的答案,其中包含我可以在 cmets 和其他地方找到的更好的部分——更好的页面逻辑调用、更好的侦听器处理,并展示异步/await 不是完成这项任务的强制性(当我第一次阅读时,这对我来说并不明显)。
    • async 从来都不是强制性的,您始终可以使用 Promise,并且 Chrome API 是基于回调的(因此在接受的答案中链接到库)。如果您的答案更好,您需要在代码中解释 - 一件事是 async 挂钩链接库传递的异常,否则您需要在 chrome.runtime.lastError 上添加自己的检查。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-18
    • 1970-01-01
    相关资源
    最近更新 更多