Dynamically loading JavaScript synchronously

后端 未结 18 2173
小蘑菇
小蘑菇 2020-11-27 13:55

I\'m using the module pattern, one of the things I want to do is dynamically include an external JavaScript file, execute the file, and then use the functions/variables in t

相关标签:
18条回答
  • 2020-11-27 14:51

    I had the following problem(s) with the existing answers to this question (and variations of this question on other stackoverflow threads):

    • None of the loaded code was debuggable
    • Many of the solutions required callbacks to know when loading was finished instead of truly blocking, meaning I would get execution errors from immediately calling loaded (ie loading) code.

    Or, slightly more accurately:

    • None of the loaded code was debuggable (except from the HTML script tag block, if and only if the solution added a script elements to the dom, and never ever as individual viewable scripts.) => Given how many scripts I have to load (and debug), this was unacceptable.
    • Solutions using 'onreadystatechange' or 'onload' events failed to block, which was a big problem since the code originally loaded dynamic scripts synchronously using 'require([filename, 'dojo/domReady']);' and I was stripping out dojo.

    My final solution, which loads the script before returning, AND has all scripts properly accessible in the debugger (for Chrome at least) is as follows:

    WARNING: The following code should PROBABLY be used only in 'development' mode. (For 'release' mode I recommend prepackaging and minification WITHOUT dynamic script loading, or at least without eval).

    //Code User TODO: you must create and set your own 'noEval' variable
    
    require = function require(inFileName)
    {
        var aRequest
            ,aScript
            ,aScriptSource
            ;
    
        //setup the full relative filename
        inFileName = 
            window.location.protocol + '//'
            + window.location.host + '/'
            + inFileName;
    
        //synchronously get the code
        aRequest = new XMLHttpRequest();
        aRequest.open('GET', inFileName, false);
        aRequest.send();
    
        //set the returned script text while adding special comment to auto include in debugger source listing:
        aScriptSource = aRequest.responseText + '\n////# sourceURL=' + inFileName + '\n';
    
        if(noEval)//<== **TODO: Provide + set condition variable yourself!!!!**
        {
            //create a dom element to hold the code
            aScript = document.createElement('script');
            aScript.type = 'text/javascript';
    
            //set the script tag text, including the debugger id at the end!!
            aScript.text = aScriptSource;
    
            //append the code to the dom
            document.getElementsByTagName('body')[0].appendChild(aScript);
        }
        else
        {
            eval(aScriptSource);
        }
    };
    
    0 讨论(0)
  • 2020-11-27 14:51

    same as Sean's answer, but instead of creating a script tag, just evaluate it. this ensures that the code is actually ready to use.

    0 讨论(0)
  • 2020-11-27 14:54

    the accepted answer is not correct:

    the script.async = false; directive only means that html parsing will be paused during script execution. this does not guarantee in which order javascript code will run. see https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/loading-third-party-javascript/

    the easiest and most elegant solution which was yet to be mentioned here is using promises, like so:

        function loadScript(url) {
          return new Promise((resolve, reject) => {
            var script = document.createElement('script')
            script.src = url
            script.onload = () => {
              resolve()
            }
            script.onerror = () => {
              reject('cannot load script '+ url)
            }
            document.body.appendChild(script)
          })
        }
    

    and then when you want to execute scripts in order:

            loadScript('myfirstscript.js').then(() => {
              console.log('first script ran');
              loadScript('index.js').then(() => {
                console.log('second script ran');
              })
            })
    
    0 讨论(0)
  • 2020-11-27 14:55

    I may be late to answering this question.

    My current solution is to recursively add <script> tags such that the addition of the subsequent script is in the callback of its predecessor. It assumes that each function contains one function and that function is the same as the file name (minus the extension). This probably isn't the best way to do things, but it works ok.

    Code to consider

    Code directory structure:

    - directory
    ---- index.html
    ---- bundle.js
    ---- test_module/
    -------- a.js
    -------- b.js
    -------- log_num.js
    -------- many_parameters.js
    

    index.html

    <head>
      <script src="bundle.js"></script>
    </head>
    

    bundle.js

    // Give JS arrays the .empty() function prototype
    if (!Array.prototype.empty){
        Array.prototype.empty = function(){
            return this.length == 0;
        };
    };
    
    function bundle(module_object, list_of_files, directory="") {
      if (!list_of_files.empty()) {
        var current_file = list_of_files.pop()
        var [function_name, extension] = current_file.split(".")
        var new_script = document.createElement("script")
        document.head.appendChild(new_script)
    
        new_script.src = directory + current_file
    
        new_script.onload = function() {
          module_object[function_name] = eval(function_name)
          bundle(module_object, list_of_files, directory)
          /*
          nullify the function in the global namespace as - assumed -  last
          reference to this function garbage collection will remove it. Thus modules
          assembled by this function - bundle(obj, files, dir) - must be called
          FIRST, else one risks overwritting a funciton in the global namespace and
          then deleting it
          */
          eval(function_name + "= undefined")
        }
      }
    }
    
    var test_module = {}
    bundle(test_module, ["a.js", "b.js", "log_num.js", "many_parameters.js"], "test_module/")
    

    a.js

    function a() {
      console.log("a")
    }
    

    b.js

    function b() {
      console.log("b")
    }
    

    log_num.js

    // it works with parameters too
    function log_num(num) {
      console.log(num)
    }
    

    many_parameters.js

    function many_parameters(a, b, c) {
      var calc = a - b * c
      console.log(calc)
    }
    
    0 讨论(0)
  • 2020-11-27 14:56

    This is the code that I'm using for multiple file load in my app.

    Utilities.require = function (file, callback) {
        callback = callback ||
        function () {};
        var filenode;
        var jsfile_extension = /(.js)$/i;
        var cssfile_extension = /(.css)$/i;
    
        if (jsfile_extension.test(file)) {
            filenode = document.createElement('script');
            filenode.src = file;
            // IE
            filenode.onreadystatechange = function () {
                if (filenode.readyState === 'loaded' || filenode.readyState === 'complete') {
                    filenode.onreadystatechange = null;
                    callback();
                }
            };
            // others
            filenode.onload = function () {
                callback();
            };
            document.head.appendChild(filenode);
        } else if (cssfile_extension.test(file)) {
            filenode = document.createElement('link');
            filenode.rel = 'stylesheet';
            filenode.type = 'text/css';
            filenode.href = file;
            document.head.appendChild(filenode);
            callback();
        } else {
            console.log("Unknown file type to load.")
        }
    };
    
    Utilities.requireFiles = function () {
        var index = 0;
        return function (files, callback) {
            index += 1;
            Utilities.require(files[index - 1], callBackCounter);
    
            function callBackCounter() {
                if (index === files.length) {
                    index = 0;
                    callback();
                } else {
                    Utilities.requireFiles(files, callback);
                }
            };
        };
    }();
    

    And this utilities can be used by

    Utilities.requireFiles(["url1", "url2",....], function(){
        //Call the init function in the loaded file.
        })
    
    0 讨论(0)
  • 2020-11-27 14:58

    There actually is a way to load a list of scripts and execute them synchronously. You need to insert each script tag into the DOM, explicitly setting its async attribute to false:

    script.async = false;
    

    Scripts that have been injected into the DOM are executed asynchronously by default, so you have to set the async attribute to false manually to work around this.

    Example

    <script>
    (function() {
      var scriptNames = [
        "https://code.jquery.com/jquery.min.js",
        "example.js"
      ];
      for (var i = 0; i < scriptNames.length; i++) {
        var script = document.createElement('script');
        script.src = scriptNames[i];
        script.async = false; // This is required for synchronous execution
        document.head.appendChild(script);
      }
      // jquery.min.js and example.js will be run in order and synchronously
    })();
    </script>
    
    <!-- Gotcha: these two script tags may still be run before `jquery.min.js`
         and `example.js` -->
    <script src="example2.js"></script>
    <script>/* ... */<script>
    

    References

    • There is a great article by Jake Archibald of Google about this called Deep dive into the murky waters of script loading.
    • The WHATWG spec on the tag is a good and thorough description of how tags are loaded.
    0 讨论(0)
提交回复
热议问题