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?
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();" }
])
});
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
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.