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 single InjectDetails object (representing a script file or code snippet), as well as a callback function to be executed after the script. Current answers propose nesting executeScript 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?
-
One potential solution, that may not fit your situation: concatenate all of your files using a tool like grunt or gulp into one single file, concat.js, then simply execute the concatenated file.SethWhite– SethWhite2015年10月13日 19:02:16 +00:00Commented Oct 13, 2015 at 19:02
7 Answers 7
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();" }
])
});
5 Comments
addListener function doesn't take arrays, but my helper executeScripts function can take an array and convert it into a nested callback.InjectDetails objects to my helper executeScripts function.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
2 Comments
browser.* APIs with their Chrome polyfill 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
});
Comments
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.
1 Comment
code, it appears that you can specify {func:transformPage}.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.
2 Comments
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.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"]);This is mostly an updated answer (on the other answer) :P
const executeScripts = (tabId, scripts, finalCallback) => {
try {
if (scripts.length && scripts.length > 0) {
const execute = (index = 0) => {
chrome.tabs.executeScript(tabId, scripts[index], () => {
const newIndex = index + 1;
if (scripts[newIndex]) {
execute(newIndex);
} else {
finalCallback();
}
});
}
execute();
} else {
throw new Error('scripts(array) undefined or empty');
}
} catch (err) {
console.log(err);
}
}
executeScripts(
null,
[
{ file: "jquery.js" },
{ file: "master.js" },
{ file: "helper.js" },
{ code: "transformPage();" }
],
() => {
// Do whatever you want to do, after the last script is executed.
}
)
Or return a promise.
const executeScripts = (tabId, scripts) => {
return new Promise((resolve, reject) => {
try {
if (scripts.length && scripts.length > 0) {
const execute = (index = 0) => {
chrome.tabs.executeScript(tabId, scripts[index], () => {
const newIndex = index + 1;
if (scripts[newIndex]) {
execute(newIndex);
} else {
resolve();
}
});
}
execute();
} else {
throw new Error('scripts(array) undefined or empty');
}
} catch (err) {
reject(err);
}
});
};
executeScripts(
null,
[
{ file: "jquery.js" },
{ file: "master.js" },
{ file: "helper.js" },
{ code: "transformPage();" }
]
).then(() => {
// Do whatever you want to do, after the last script is executed.
})
Comments
with v3:
chrome.action.onClicked.addListener((tab) => {
console.log("entering");
chrome.scripting
.executeScript({
target: { tabId: tab.id },
files: [
"scripts/jquery.min.js",
"scripts/bootstrap.min.js",
"scripts/script.js",
],
})
.then(() => {
// All scripts loaded
console.log("done");
});
});
Comments
Explore related questions
See similar questions with these tags.