Injecting multiple scripts through executeScript in Google Chrome

18,756

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.

Share:
18,756
Douglas
Author by

Douglas

Updated on June 13, 2022

Comments

  • Douglas
    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 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?

  • Douglas
    Douglas almost 10 years
    @Lior: What do you mean? The standard addListener function doesn't take arrays, but my helper executeScripts function can take an array and convert it into a nested callback.
  • Lior
    Lior almost 10 years
    Oh 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
    Douglas almost 10 years
    Yes. I meant that you can specify an array of InjectDetails objects to my helper executeScripts function.
  • ljs.dev
    ljs.dev over 9 years
    Works perfectly. @Piyey, please mark Douglas' answer as the best, as it seems to have solved your problem :)
  • Douglas
    Douglas almost 9 years
    Thanks 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
    baskint over 8 years
    i 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
    Admin almost 8 years
    Yes! Promise is the best! No more manual synchronizing and nested callbacks!
  • fregante
    fregante almost 5 years
    If you want to use Promises, nowadays you can use Firefox’ browser.* APIs with their Chrome polyfill
  • danivicario
    danivicario over 4 years
    It 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
    Teepeemm about 2 years
    Instead of code, it appears that you can specify {func:transformPage}.