Search code examples
javascriptgoogle-chrome-extensionbrowser-plugin

How to sequentially insert scripts into the page context using <script> tags


I am having troubles loading all the scripts that I am inserting into the page context with <script> tags from a content_script script, because they are required to be executed in the correct loading order, as some depend on others. In an actual HTML file I guess there is a queue to load the files, but with inserting <script> tags it seems like if one script delays a little time, the next one starts loading and then is immediately executed notwithstanding it had to wait for its dependency library that is still loading.

Below is the network output with the error caused because of x-tag-core.min.js is loaded before primeui-all.min.js and eventPage.js which uses jquery-ui.min.js, is loaded before it:

Chrome network's output

// manifest.js
"content_scripts": [
{
  "matches": [
    "<all_urls>"
  ],
  "js": [
    "js/jquery-3.1.1.min.js",
    "js/main.js"
  ]
}
]
// main.js
var s = document.createElement('script');
s.src = chrome.extension.getURL('js/jquery-3.1.1.min.js');
$(document.head).append(s);

s = document.createElement('script');
s.src = chrome.extension.getURL('js/jquery-ui.min.js');
$(document.head).append(s);

s = document.createElement('script');
s.src = chrome.extension.getURL('js/primeui-all.min.js');
$(document.head).append(s);

s = document.createElement('script');
s.src = chrome.extension.getURL('js/x-tag-core.min.js');
$(document.head).append(s);

s = document.createElement('script');
s.src = chrome.extension.getURL('js/primeelements.min.js');
$(document.head).append(s);

s = document.createElement('script');
s.src = chrome.extension.getURL('js/eventPage.js');
$(document.head).append(s);

Solution

  • Duplicating your problem in a snippet

    The following snippet duplicates your problem by sequentially inserting <script> elements. I am using network resources, because there is no method of storing such scripts on Stack Overflow. Given that you only supplied version information for jQuery, I have had to guess at appropriate versions for the other libraries which you are using.

    In order to stay close to the code which you are using in your Chrome extension, chrome.extension.getURL() is faked. In these snippets, that function returns a functional network URL for the libraries which you are using. Obviously, in your Chrome extension, you will want to continue to use the library files which you ave included with your extension.

    In addition, the code for eventPage.js is faked by having some code that reports if jQuery exists, if $(document).puidialog is a function, and/or if xtag is defined. The errors you are seeing are that $([something]).puidialog was not a function and xtag was not defined. This fakeEventPageJS code accurately shows if the scripts have been properly loaded.

    In this duplication of your problem, an error is also produced from the various libraries as the subsequent libraries can not find prior libraries.

    var s = document.createElement('script');
    s.src = chrome.extension.getURL('js/jquery-3.1.1.min.js');
    document.head.appendChild(s);
    
    s = document.createElement('script');
    s.src = chrome.extension.getURL('js/jquery-ui.min.js');
    document.head.appendChild(s);
    
    s = document.createElement('script');
    s.src = chrome.extension.getURL('js/primeui-all.min.js');
    document.head.appendChild(s);
    
    s = document.createElement('script');
    s.src = chrome.extension.getURL('js/x-tag-core.min.js');
    document.head.appendChild(s);
    
    s = document.createElement('script');
    s.src = chrome.extension.getURL('js/primeelements.min.js');
    document.head.appendChild(s);
    
    s = document.createElement('script');
    //s.src = chrome.extension.getURL('js/eventPage.js');
    //Fake js/eventPage.js with an actual script.
    s.textContent = fakeEventPageJS;
    document.head.appendChild(s);
    <!-- The JavaScript code included in this HTML section is used to fake the chrome API
         and part of faking the existence of a js/eventPage.js file by inserting code. -->
        
    <!-- Using HTML <script> tags to test that loading it via these works prior to testing
           using JavaScript inserts. If you want to verify that the code works when the
           scripts are included in the original HTML, you can uncomment the <script>
           tags here. -->
    
    <!-- jquery-3.1.1.min.js -->
    <!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script><!--  -->
    <!-- jquery-ui.min.js -->
    <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
    <!-- <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script><!--  -->
    
    <!-- primeui-all.min.js -->
    <link rel="stylesheet" href="https://cdn.rawgit.com/primefaces/primeui-distribution/master/primeui-all.min.css">
    <!-- <script src="https://cdn.rawgit.com/primefaces/primeui-distribution/master/primeui-all.min.js"></script><!--  -->
    
    <!-- x-tag-core.min.js -->
    <!-- <script src="https://cdn.rawgit.com/x-tag/core/master/dist/x-tag-core.min.js"></script>
    
    <!-- primeelements.min.js -->
    <!-- <script src="https://cdn.rawgit.com/primefaces/primeui-distribution/master/primeelements.min.js"></script> <!-- -->
    
    <script>
      //Fake chrome.extension.getURL
      var netScriptLoations = {
        'js/jquery-3.1.1.min.js':'https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js',
        'js/jquery-ui.min.js':'https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js',
        'js/primeui-all.min.js':'https://cdn.rawgit.com/primefaces/primeui-distribution/master/primeui-all.min.js',
        'js/x-tag-core.min.js':'https://cdn.rawgit.com/x-tag/core/master/dist/x-tag-core.min.js',
        'js/primeelements.min.js':'https://cdn.rawgit.com/primefaces/primeui-distribution/master/primeelements.min.js'
    }
    if(typeof chrome !== 'object'){
      var chrome = {};
    }
    if(typeof chrome.extension !== 'object'){
      chrome.extension = {};
    }
    if(typeof chrome.extension.getURL !== 'function'){
      chrome.extension.getURL = function(script){
        //console.log(netScriptLoations[script]);
        return netScriptLoations[script];
      };
    }
    
    var fakeEventPageJS = 
          'var testResult = "Scripts did NOT load correctly. "' 
        + '               + "$(document).puidialog is NOT a function.";'
        + 'var passedChecks=0;'
        + 'if(typeof $ === "function"){'
        + '    var puidialogNot = " NOT";'
        + '    if(typeof $(document).puidialog === "function") {'
        + '        puidialogNot = "";'
        + '    }'
        + '    console.log("$(document).puidialog is" + puidialogNot + " a function");'
        + '} else {'
        + '    console.log("No jQuery");'
        + '}'
        + 'var xtagNot = " NOT";'
        + 'if(typeof xtag !== "undefined") {'
        + '    xtagNot = "";'
        + '}'
        + 'console.log("xtag is" + xtagNot + " defined.");'
        + 'if(puidialogNot + xtagNot === "") {'
        + '    testResult = "Scripts loaded CORRECTLY. "' 
        + '}'
        + 'console.log(testResult);';
    </script>

    Insert each <script> in the onload event handler of the prior script

    It is clear that the inserted <script> elements are executed asynchronously. In order to force them to be executed synchronously, we need to insert the next script after the prior one has completed execution. This can be done by utilizing the load event for each script.

    The following code loads each subsequent script in the load event handler of the prior script.

    The function createScriptElement creates an individual <script> element. This function can be somewhat simplified in your code due to not needing to fake the eventPage.js script.

    The function createScriptSequence creates a sequence of <script> elements which each inserts the next script in its onload listener. This uses script.addEventListerner('load',...) in order to be immune to the script being loaded changing the script.onload property/attribute.

    var scriptsToInsert = [
        'js/jquery-3.1.1.min.js',
        'js/jquery-ui.min.js',
        'js/primeui-all.min.js',
        'js/x-tag-core.min.js',
        'js/primeelements.min.js',
        'js/eventPage.js'
    ]
    
    function createScriptElement(script){
        let scriptEl = document.createElement('script');
        let scriptElSource = chrome.extension.getURL(script);
        if(scriptElSource){
            scriptEl.src = scriptElSource;
        } else {
            //Only need this `else` because we are faking having js/eventPage.js by using
            //  some code to indicate if $(document).puidialog is a function.
            scriptEl.textContent = fakeEventPageJS;
        }
        return scriptEl;
    }
    
    function createScriptSequence(scriptArray){
        var scriptEls = [];
        //Create all the script elements
        scriptArray.forEach((script,index)=>{
            //console.log(script);
            scriptEls.push(createScriptElement(script));
            if(index>0){
                //Add an onload listener for each script (except the last) which loads
                //  the next one in the sequence.
                scriptEls[index - 1].addEventListener('load',function oneTime(){
                    //Probably don't need to remove this, but better to clean things up.
                    scriptEls[index - 1].removeEventListener('load',oneTime,false);
                    document.head.appendChild(scriptEls[index]);
                },false);
            }
        });
        //Return the first script in the sequence
        return scriptEls[0];
    }
    
    document.head.appendChild(createScriptSequence(scriptsToInsert));
    <!-- The JavaScript code included in this HTML section is used to fake the chrome API
         and part of faking the existence of a js/eventPage.js file by inserting code. -->
    
    <!-- jquery-ui.min.js -->
    <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
    <!-- primeui-all.min.js -->
    <link rel="stylesheet" href="https://cdn.rawgit.com/primefaces/primeui-distribution/master/primeui-all.min.css">
    
    <script>
      //Fake chrome.extension.getURL
      var netScriptLoations = {
        'js/jquery-3.1.1.min.js':'https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js',
        'js/jquery-ui.min.js':'https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js',
        'js/primeui-all.min.js':'https://cdn.rawgit.com/primefaces/primeui-distribution/master/primeui-all.min.js',
        'js/x-tag-core.min.js':'https://cdn.rawgit.com/x-tag/core/master/dist/x-tag-core.min.js',
        'js/primeelements.min.js':'https://cdn.rawgit.com/primefaces/primeui-distribution/master/primeelements.min.js'
    }
    if(typeof chrome !== 'object'){
      var chrome = {};
    }
    if(typeof chrome.extension !== 'object'){
      chrome.extension = {};
    }
    if(typeof chrome.extension.getURL !== 'function'){
      chrome.extension.getURL = function(script){
        //console.log(netScriptLoations[script]);
        return netScriptLoations[script];
      };
    }
    
    var fakeEventPageJS = 
          'var testResult = "Scripts did NOT load correctly. "' 
        + '               + "$(document).puidialog is NOT a function.";'
        + 'var passedChecks=0;'
        + 'if(typeof $ === "function"){'
        + '    var puidialogNot = " NOT";'
        + '    if(typeof $(document).puidialog === "function") {'
        + '        puidialogNot = "";'
        + '    }'
        + '    console.log("$(document).puidialog is" + puidialogNot + " a function");'
        + '} else {'
        + '    console.log("No jQuery");'
        + '}'
        + 'var xtagNot = " NOT";'
        + 'if(typeof xtag !== "undefined") {'
        + '    xtagNot = "";'
        + '}'
        + 'console.log("xtag is" + xtagNot + " defined.");'
        + 'if(puidialogNot + xtagNot === "") {'
        + '    testResult = "Scripts loaded CORRECTLY. "' 
        + '}'
        + 'console.log(testResult);';
    </script>