使用chrome.tabs.executeScript执行异步函数

12

我有一个函数,想要使用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())也不起作用。

如果我在页面中执行完全相同的函数,则会返回预期的 promise,并且可以被等待。

您有什么想法我做错了什么,是否有任何解决办法?


1
如果这个代码能够工作的话,那将是很有趣的,但我并没有期望它会成功。内容脚本中的代码和后台上下文(弹出窗口)中的代码在完全不同的进程中运行。如果从“async”内容脚本中“await”响应结果,我会感到惊讶。你需要使用消息传递技术(Message Passing)。 - Makyen
1
async/await 只是一种语法糖,不会修改基于事件循环的 JavaScript 引擎行为,因此它无法工作。 - wOxxOm
在Firefox Web扩展中,chrome.tabs.executeScript返回一个Promise,并且根据MDN文档,这也与Chrome的工作方式兼容 - 尽管我还没有找到任何有用的Chrome文档,但这可能是需要考虑的事情。 - Jaromanda X
@wOxxOm 是的,我希望得到一个 Promise 或 Promise 的 Symbol.iterator。 - Keith
我希望得到一个 Promise,而 chrome.tabs.executeScript 已经返回了一个 Promise - 所以,你已经拥有了你所期望的 Promise。 - Jaromanda X
显示剩余4条评论
3个回答

11
问题在于页面和扩展程序之间无法直接访问事件和本地对象。实际上,您获得的是一个序列化的副本,类似于执行JSON.parse(JSON.stringify(obj))时的结果。
这意味着某些本地对象(例如new Errornew Promise)将被清空(变成{}),事件将丢失,并且任何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
    }); 

我已经将这个转换成了一个函数{{link1:chrome.tabs.executeAsyncFunction}}(作为{{link2:chrome-extension-async}}的一部分,该函数“promisifies”整个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; 
    });

Thischrome.tabs.executeScriptchrome.runtime.onMessage.addListener 包装起来,并在调用 chrome.runtime.sendMessage 以解析承诺之前,在脚本中使用 try-finally 进行包装。


4

将Promise从页面传递到内容脚本不起作用,解决方法是使用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
你说得一般正确,@Keith。我只是想补充一个更简短的答案,并提供更好的部分,这些部分可以在评论和其他地方找到 - 更好的页面逻辑调用,更好的监听器处理,并展示async/await不是完成此任务所必需的(当我第一次阅读时,这并不明显)。 - Lev Lukomsky
“async”并不是必须的,您始终可以使用promises,而Chrome APIs是基于回调的(因此在接受的答案中提供了链接到库)。如果您的答案更好,您需要在代码中解释清楚 - 其中一件事是“async”钩住了由链接库传递的异常,如果没有它,您需要添加自己的检查chrome.runtime.lastError - Keith

4

如果你正在使用新的清单版本3(MV3),请注意,现在应该支持此功能。

chrome.tabs.executeScript已被chrome.scripting.executeScript替换,并且文档明确说明:“如果[injected]脚本计算为Promise,则浏览器将等待Promise解决并返回结果值。”


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接