Lightweight Dynamic JavaScript Loader with Dependency Handling
I write a lightweight dynamic JavaScript loader with dependency handling, which means it can dynamically loads JavaScript files with the order you want. The following is the source code for the dynamic JavaScript loader:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | /** * @fileoverview Simple JavaScript Loader (with dependency handling) */ /** * Class to load JavaScript files according to dependency. * * @param {Array} jsNames The file names of JavaScript files to be loaded. * @param {object} jsDependencies The dependencies of JavaScript files to be * loaded. * @param {object} jsLocations The locations of JavaScript files to be loaded. * @constructor */ MySimpleJSLoader = function(jsNames, jsDependencies, jsLocations) { // check data sanity if (Object.prototype.toString.apply(jsNames) != '[object Array]') throw "In MySimpleJSLoader constructor: jsNames is not Array"; if (typeof jsDependencies != 'object') throw "In MySimpleJSLoader constructor: jsDependencies is not object"; if (typeof jsDependencies != 'object') throw "In MySimpleJSLoader constructor: jsLocations is not object"; for (var i=0; i < jsNames.length; i++) { if (!jsDependencies.hasOwnProperty(jsNames[i])) { throw "In MySimpleJSLoader constructor: no dependency info of " + jsNames[i]; } if (!jsLocations.hasOwnProperty(jsNames[i])) { throw "In MySimpleJSLoader constructor: no location info of " + jsNames[i]; } } /** * The dependencies of JavaScript files to be loaded. * @const * @type {object} * @private */ this.jsDependencies_ = jsDependencies; /** * Indicate if the JavaScript file is loaded. * @type {object} * @private */ this.isLoaded_ = {}; /** * Contains the content of the JavaScript file * @type {object} * @private */ this.jsContent_ = {}; /** * Indicate if the content of JavaScript file is loaded. * @type {object} * @private */ this.isJSContentDownloaded_ = {}; // initialize internal private variable for (var i=0; i < jsNames.length; i++) { this.isLoaded_[ jsNames[i] ] = false; this.jsContent_[ jsNames[i] ] = null; this.isJSContentDownloaded_[ jsNames[i] ] = false; } // Load all JavaScript files for (var i=0; i < jsNames.length; i++) { this.loadJSFile(jsNames[i], jsLocations[ jsNames[i] ]); } }; /** * Load one JavaScript file by XMLHttpRequest. * @param {string} jsName The name of the JavaScript file to be loaded. * @param {string} jsLocation The location of the JavaScript file to be loaded. * @private */ MySimpleJSLoader.prototype.loadJSFile = function(jsName, jsLocation) { /** * XMLHttpRequest variable. * @type {object} * @private */ var xmlhttp; if (window.XMLHttpRequest) { xmlhttp = new XMLHttpRequest(); } else { xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); } xmlhttp.onreadystatechange = function(jsName, jsLocation) { if (xmlhttp.readyState == 4) { if (xmlhttp.status == 200) { // The content of the JavaScript file is downloaded. this.jsContent_[jsName] = xmlhttp.responseText; this.isJSContentDownloaded_[jsName] = true; if ( this.isDependencySatisfied(jsName) ) { this.insertJS(jsName); } } else { // fail to get the content of the JavaScript file throw 'cannot load file ' + jsName + ' at ' + jsLocation; } } }.bind(this, jsName, jsLocation); xmlhttp.open("GET", jsLocation, true); xmlhttp.send(); }; /** * Check if the dependency of the JavaScript file is satisfied. * @param {string} jsName The name of the JavaScript file to be checked. * @private */ MySimpleJSLoader.prototype.isDependencySatisfied = function(jsName) { if (this.jsDependencies_[jsName] == null) { console.log(jsName + ' has no dependency.'); console.log(jsName + ' dependency satisfied? true'); console.log('---'); return true; } /** * The JavaScript files that the JavaScript file with jsName is dependent on. * @type {Array} * @private */ var dependentJSFiles = this.jsDependencies_[jsName].split(','); for (var i=0; i < dependentJSFiles.length; i++) { // Remove whitespace in the beginning and end of the string dependentJSFiles[i] = dependentJSFiles[i].replace(/(^\s+)|(\s+$)/g, ""); } console.log(jsName + ' depends on :'); for (var i=0; i < dependentJSFiles.length; i++) { console.log(dependentJSFiles[i]); } /** * Indicate whether all dependencies is satisfied. * @type {boolean} * @private */ var isAllDependenciesSatisfied = true; for (var i=0; i < dependentJSFiles.length; i++) { if (!this.isLoaded_[dependentJSFiles[i]]) { isAllDependenciesSatisfied = false; } } if (isAllDependenciesSatisfied) { console.log(jsName + ' dependency satisfied? true'); } else { console.log(jsName + ' dependency satisfied? false'); } console.log('---'); return isAllDependenciesSatisfied; }; /** * Insert the JavaScript file to document. * @param {string} jsName The name of the JavaScript file to be inserted. * @private */ MySimpleJSLoader.prototype.insertJS = function(jsName) { /** * DOM element of HTML script tag * @type {DOM Element} * @private */ var script = document.createElement('script'); script.setAttribute("type", "text/javascript"); /** * The content of the JavaScript file * @type {DOM Element} * @private */ var textNode = document.createTextNode( this.jsContent_[jsName] ); script.appendChild(textNode); document.getElementsByTagName("head")[0].appendChild(script); this.isLoaded_[jsName] = true; console.log(jsName + ' loaded'); console.log('---'); // Load other JavaScript files dependent on this inserted JavaScript file. this.checkAfterInsertion(); }; /** * When a JavaScript is inserted to the docuement, dependencies of other * JavaScript files may be satisfied after the insertion. As a result, we need * to check dependencies of all JavaScript files after insertion. * @private */ MySimpleJSLoader.prototype.checkAfterInsertion = function() { for (var jsName in this.isLoaded_) { if (!this.isLoaded_[jsName] && this.isJSContentDownloaded_[jsName] && this.isDependencySatisfied(jsName)) { this.insertJS(jsName); } } }; |
Usage
I will show how to use the JavaScript loader by example.
Assume that we need to load a.js, b.js, c.js, d.js.
They are located at http://example.com/static/js/.
a.js has no dependency.
b.js has to be loaded after a.js is loaded.
c.js has to be loaded after a.js is loaded.
d.js has to be loaded after b.js and c.js are loaded.
The following is the usage code for above example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | /** * The file names of JavaScript files to be loaded */ var jsNames = ['a.js', 'b.js', 'c.js', 'd.js']; /** * A JavaScript file A is dependent on a JavaScript file B if B must be loaded * before A is loaded. To describe the dependencies, use: * * 'file name of A' : 'file name of B' * * If a JavaScript file A is not dependent on other JavaScript, use: * * 'file name of A' : null * * If a JavaScript file is dependent on multiple JavaScript files, the * multiple files is separated by comma. */ var jsDependencies = { 'a.js': null, 'b.js': 'a.js', 'c.js': 'a.js', 'd.js': 'b.js, c.js' }; /** * The locations of JavaScript files, usually are URLs */ var jsLocations = { 'a.js': 'http://example.com/static/js/a.js', 'b.js': 'http://example.com/static/js/b.js', 'c.js': 'http://example.com/static/js/c.js', 'd.js': 'http://example.com/static/js/d.js' }; var myloader = new MySimpleJSLoader(jsNames, jsDependencies, jsLocations); |
Enjoy!