Injecting multiple scripts through executeScript in Google Chrome
Solution 1
This is my proposed solution:
function executeScripts(tabId, injectDetailsArray)
{
function createCallback(tabId, injectDetails, innerCallback) {
return function () {
chrome.tabs.executeScript(tabId, injectDetails, innerCallback);
};
}
var callback = null;
for (var i = injectDetailsArray.length - 1; i >= 0; --i)
callback = createCallback(tabId, injectDetailsArray[i], callback);
if (callback !== null)
callback(); // execute outermost function
}
Subsequently, the sequence of InjectDetails
scripts can be specified as an array:
chrome.browserAction.onClicked.addListener(function (tab) {
executeScripts(null, [
{ file: "jquery.js" },
{ file: "master.js" },
{ file: "helper.js" },
{ code: "transformPage();" }
])
});
Solution 2
From Chrome v32, it supports Promise. We should use it for making code clean.
Here is an example:
new ScriptExecution(tab.id)
.executeScripts("js/jquery.js", "js/script.js")
.then(s => s.executeCodes('console.log("executes code...")'))
.then(s => s.injectCss("css/style.css"))
.then(s => console.log('done'));
ScriptExecution
source:
(function() {
function ScriptExecution(tabId) {
this.tabId = tabId;
}
ScriptExecution.prototype.executeScripts = function(fileArray) {
fileArray = Array.prototype.slice.call(arguments); // ES6: Array.from(arguments)
return Promise.all(fileArray.map(file => exeScript(this.tabId, file))).then(() => this); // 'this' will be use at next chain
};
ScriptExecution.prototype.executeCodes = function(fileArray) {
fileArray = Array.prototype.slice.call(arguments);
return Promise.all(fileArray.map(code => exeCodes(this.tabId, code))).then(() => this);
};
ScriptExecution.prototype.injectCss = function(fileArray) {
fileArray = Array.prototype.slice.call(arguments);
return Promise.all(fileArray.map(file => exeCss(this.tabId, file))).then(() => this);
};
function promiseTo(fn, tabId, info) {
return new Promise(resolve => {
fn.call(chrome.tabs, tabId, info, x => resolve());
});
}
function exeScript(tabId, path) {
let info = { file : path, runAt: 'document_end' };
return promiseTo(chrome.tabs.executeScript, tabId, info);
}
function exeCodes(tabId, code) {
let info = { code : code, runAt: 'document_end' };
return promiseTo(chrome.tabs.executeScript, tabId, info);
}
function exeCss(tabId, path) {
let info = { file : path, runAt: 'document_end' };
return promiseTo(chrome.tabs.insertCSS, tabId, info);
}
window.ScriptExecution = ScriptExecution;
})()
If you would like to use ES5, you can use online compiler to compile above codes to ES5.
Fork me on GitHub: chrome-script-execution
Solution 3
Fun fact, the scripts are injected in order and you don't need to wait for each one to be injected.
chrome.browserAction.onClicked.addListener(tab => {
chrome.tabs.executeScript(tab.id, { file: "jquery.js" });
chrome.tabs.executeScript(tab.id, { file: "master.js" });
chrome.tabs.executeScript(tab.id, { file: "helper.js" });
chrome.tabs.executeScript(tab.id, { code: "transformPage();" }, () => {
// All scripts loaded
});
});
This is considerably faster than manually waiting for each one. You can verify that they are loaded in order by loading a huge library first (like d3.js
) and then loading a small file after. The order will still be preserved.
Note: errors aren't caught, but this should never happen if all files exist.
I wrote a little module to simplify this even further, including proper error handling, Promise support and scripting
API in Manifest v3:
executeScript({
tabId: tab.id,
files: ["jquery.js", "master.js", "helper.js"]
}).then(() => {
// All scripts loaded
});
Solution 4
Given your answer, I expected synchronously injecting the scripts to cause problems (namely, I thought that the scripts might be loaded in the wrong order), but it works well for me.
var scripts = [
'first.js',
'middle.js',
'last.js'
];
scripts.forEach(function(script) {
chrome.tabs.executeScript(null, { file: script }, function(resp) {
if (script!=='last.js') return;
// Your callback code here
});
});
This assumes you only want one callback at the end and don't need the results of each executed script.
Solution 5
Since Manifest v3, you can use promise chains and async/await:
Promises
MV3 provides first-class support for promises: many popular APIs support promises now, and we will eventually support promises on all appropriate methods.
You can use promise chains, as well as async/await. [...]
The following should work.
chrome.browserAction.onClicked.addListener(async (tab) => {
await chrome.scripting.executeScript({ files: ["jquery.js"] });
await chrome.scripting.executeScript({ files: ["master.js"] });
await chrome.scripting.executeScript({ files: ["helper.js"] });
// await chrome.tabs.executeScript({ code: "transformPage();" });
});
Note that, despite the argument name, files
must specify exactly one file.
Note that you can't execute arbitrary code anymore, so best move that transformPage();
into a file and execute it.
Douglas
Updated on June 13, 2022Comments
-
Douglas almost 2 years
I need to programmatically inject multiple script files (followed by a code snippet) into the current page from my Google Chrome extension. The
chrome.tabs.executeScript
method allows for a singleInjectDetails
object (representing a script file or code snippet), as well as a callback function to be executed after the script. Current answers propose nestingexecuteScript
calls:chrome.browserAction.onClicked.addListener(function(tab) { chrome.tabs.executeScript(null, { file: "jquery.js" }, function() { chrome.tabs.executeScript(null, { file: "master.js" }, function() { chrome.tabs.executeScript(null, { file: "helper.js" }, function() { chrome.tabs.executeScript(null, { code: "transformPage();" }) }) }) }) });
However, the callback nesting gets unwieldy. Is there a way of abstracting this?
-
Douglas almost 10 years@Lior: What do you mean? The standard
addListener
function doesn't take arrays, but my helperexecuteScripts
function can take an array and convert it into a nested callback. -
Lior almost 10 yearsOh ok, you wrote "the sequence of InjectDetails scripts can be specified as an array", you meant injectDetailsArray? I got confused with the chrome.tabs.executeScript's argument InjectDetails.
-
Douglas almost 10 yearsYes. I meant that you can specify an array of
InjectDetails
objects to my helperexecuteScripts
function. -
ljs.dev over 9 yearsWorks perfectly. @Piyey, please mark Douglas' answer as the best, as it seems to have solved your problem :)
-
Douglas almost 9 yearsThanks for the observation! However, even though your code might work correctly under the current version of Chrome, I would not rely on this behaviour. Given that
executeScript
is defined as asynchronous (taking a callback function), one should not assume that the operation is complete before the callback is called. This concern would not apply if Chrome guaranteed that the injected scripts will always execute in order, but I didn't find that assurance documented. -
baskint over 8 yearsi was having the same issue, one of the scripts were relying on the previous script being available. it worked most of the time, but this seems like a better solution. Voted up!
-
Admin almost 8 yearsYes! Promise is the best! No more manual synchronizing and nested callbacks!
-
fregante almost 5 yearsIf you want to use Promises, nowadays you can use Firefox’
browser.*
APIs with their Chrome polyfill -
danivicario over 4 yearsIt works to me! I have put it in this way for ease of use of others too:
function loadScripts(scripts) { scripts.forEach(function(script) { chrome.tabs.executeScript(null, { file: script }, function(resp) {}); }); }
then I use it like:loadScripts(["generics.js", "content-o.js"]);
-
Teepeemm about 2 yearsInstead of
code
, it appears that you can specify{func:transformPage}
.