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 met
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.
If you want to catch the errors, I'd suggest to use the Firefox’ browser.*
APIs with their Chrome polyfill
browser.browserAction.onClicked.addListener(tab => {
Promise.all([
browser.tabs.executeScript(tab.id, { file: "jquery.js" }),
browser.tabs.executeScript(tab.id, { file: "master.js" }),
browser.tabs.executeScript(tab.id, { file: "helper.js" }),
browser.tabs.executeScript(tab.id, { code: "transformPage();" })
]).then(() => {
console.log('All scripts definitely loaded')
}, error => {
console.error(error);
});
});
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.
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();" }
])
});
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.
})