我想在页面中使用chrome.tabs.executeScript
执行一个函数,从浏览器动作弹出窗口运行。权限设置正确,它可以与同步回调一起正常工作:
chrome.tabs.executeScript(
tab.id,
{ code: `(function() {
// Do lots of things
return true;
})()` },
r => console.log(r[0])); // Logs true
问题是我要调用的函数经历了几次回调,因此我想使用async
和await
:
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()
)也不起作用。
如果我在页面中执行完全相同的功能,它会按预期返回一个承诺,并且可以等待。
知道我做错了什么,有什么方法吗?
答案 0 :(得分:5)
问题是页面和扩展名之间不能直接使用事件和本机对象。基本上你会得到一个序列化副本,就像你JSON.parse(JSON.stringify(obj))
一样。
这意味着一些本机对象(例如new Error
或new Promise
)将被清空(变为{}
),事件将丢失,并且任何承诺的实现都无法跨越边界。< / p>
解决方案是使用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.executeScript
和chrome.runtime.onMessage.addListener
,并在调用try
来解析承诺之前将脚本包装在finally
- chrome.runtime.sendMessage
中
答案 1 :(得分:0)
将承诺从页面传递到内容脚本不起作用,解决方案是使用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);
}
});