3
\$\begingroup\$

I have written a solution for managing dynamic loading of dependent JavaScript files (or executing JS functions) coupled with automatic versioning to refresh browser cache.

The goal was to be able to specify a single my.js file and have all dependent js files dynamically loaded with a filename which has been appended with its timestamp (.htaccess filters out the timestamp).

My hope is to get feedback on my methodology and code. Is this a methodology others would find useful?

UPDATE - I have posted a fully working version of this concept, updated with suggestions as a project on GitHub

To accomplish the goal:

  1. Within each .js file, I specify the list of dependent files with the associated created class object (ns) so I know when the dependent file has been loaded.

    For example, my.js may have the following declarations:

    var dependants = [
     {file: "custom-dialog.min.js", ns: "CustomDialog"},
     {file: function miscFn1(){doSomethingOnLoad()}, dependencies: [{file: function onload(){}}, {file: "dragdrop.min.js", ns: "DragDrop"}]}
    ]
    sourceFiles.add(dependencies);
    sourceFiles.load();
    

    Note: All declarations listed in a .js file are for files dependent on this file. Declarations can include dependencies which must be loaded or executed first. In this example, the file dragdrop.min.js must be loaded and the window's load event must have fired before the function doSomethingOnLoad is run. But the file custom-dialog.min.js will be loaded immediately.

  2. Within include.php, I load the single file my.js (the PHP version() function adds the file timestamp for this file).

    <script src="<?php echo version(STATIC_JS_COMMON, 'js-common/my.js') ?>"></script>
    
  3. Within include.php, I specify the directories to poll on the server. The doVersionChecking() function makes an HTTP request to the server which returns all file timestamps for the specified directories (I'm happy to post this code if anyone would find this helpful).

    <script>
     sourceFiles.doVersionChecking([
     // specify url of directories to read file times for
     $ms.STATIC_JS_COMMON,
     $ms.STATIC_JS_COMMON + "/subdir"
     ]);
    </script> 
    

    .htaccess code - to remove the timestamp from the filename

    RewriteEngine On
     #Rules for Versioned Static Files
     RewriteRule ^(js|js-common|css|css-common|img|img-common)/(.+)\.([0-9])+\.(js|css|php|jpg|gif|png)(.*)$ 1ドル/2ドル.4ドル5ドル [L]
    

Below is the JavaScript code. The only code missing is the PHP code for returning the files with their associated timestamps.

Note: all STATIC_ (PHP) and $ms.STATIC_ (JS) variables contain URL paths to the files on the server. The server translates the URL paths to absolute paths in order to find the files and read the timestamps.

// manage dynamic loading of source files (js, css, img) and js functions
var sourceFiles = {
 queued: [],
 loading: [],
 source: [],
 fileInfo: [],
 versionCheck: [], // array of version check requests
 versionCheckPaths: [], // list of paths checked with version checking
 staticRoot: function() {
 if (typeof $ms.STATIC_TOP_ROOT !== "undefined") {
 return $ms.STATIC_TOP_ROOT;
 } else {
 return window.location.origin ? window.location.origin + '/' : window.location.protocol + '/' + window.location.host;
 }
 },
 staticJsCommon: function() {
 if (typeof $ms.STATIC_JS_COMMON !== "undefined") {
 return $ms.STATIC_JS_COMMON;
 } else {
 return window.location.origin ? window.location.origin + '/' : window.location.protocol + '/' + window.location.host + "/js-common";
 }
 },
 staticImgCommon: function() {
 if (typeof $ms.STATIC_IMG_COMMON !== "undefined") {
 return $ms.STATIC_IMG_COMMON;
 } else {
 return window.location.origin ? window.location.origin + '/' : window.location.protocol + '/' + window.location.host + "/img-common";
 }
 },
 doVersionChecking: function(path) {
 // set function sourceFiles.versionChecking as a dependency
 // file times will be retrieved from server before any dependent js files
 // each js file will have file time added to the name to mangage file versions
 // .htaccess removes the file version from the file name 
 var dependencies = [
 // all functions must have a unique name - create one on the fly
 {
 file: new Function("return function versionChecking" + sourceFiles.versionCheck.length + "(){$ms.sourceFiles.versionChecking(" + JSON.stringify(path) + ")}")()
 }
 ];
 sourceFiles.add(dependencies);
 sourceFiles.load();
 },
 add: function(source) {
 if (!Array.isArray(source)) {
 source = [source];
 }
 for (var i = 0; i < source.length; i++) {
 // test if namespace specified and if already exists
 if (sourceFiles.alreadyLoadedNs(source[i].ns)) continue;
 if (typeof source[i].file == "function") {
 var funcName = /function ([^\(]+)?/.exec(source[i].file.toString())[0];
 source[i].baseFile = funcName;
 source[i].loaded = false;
 source[i].type = "function";
 if (funcName == "function onload") {
 if (document.readyState === "complete" || document.readyState === "interactive") {
 // condition already satisfied
 sourceFiles.onload({
 target: {
 src: sourceFiles.source[i].baseFile
 }
 });
 } else {
 window.addEventListener("load", sourceFiles.load);
 }
 }
 } else {
 var baseFile = source[i].file.substr(source[i].file.lastIndexOf("/") + 1);
 var split = baseFile.split("?");
 baseFile = split[0];
 source[i].baseFile = baseFile
 var ext = baseFile.substr(baseFile.lastIndexOf(".") + 1);
 if (ext == "js") {
 source[i].type = "js";
 } else if (ext == "css") {
 source[i].type = "css";
 } else if (ext == "php") {
 if (baseFile.indexOf("css.php") !== -1) {
 source[i].type = "css";
 } else {
 source[i].type = "php";
 }
 } else if (["jpg", "png", "gif"].indexOf(ext) !== -1) {
 source[i].type = "img";
 } else {
 source[i].type = "unknown";
 console.log("Source File unknown type for: " + source[i].file);
 }
 var dir = "";
 if (typeof source[i].dir == "undefined") {
 // default = js-common
 dir = sourceFiles.staticJsCommon();
 } else if (typeof source[i].dir == "js-common") {
 dir = sourceFiles.staticJsCommon();
 } else if (typeof source[i].dir == "css-common") {
 dir = sourceFiles.staticCssCommon();
 } else if (typeof source[i].dir == "img-common") {
 dir = sourceFiles.staticImgCommon();
 }
 var subDir = "";
 if (source[i].file.indexOf("/") !== -1) {
 // full directory explicitly set
 } else if (typeof source[i].subDir !== "undefined") {
 // relative to specified or default subDir
 subDir = "/" + source[i].subDir;
 }
 source[i].file = dir + subDir + "/" + source[i].file;
 source[i].loaded = false;
 }
 // test if file already added to load queue
 if (sourceFiles.queued.indexOf(source[i].baseFile) !== -1) continue;
 if (source[i].dependencies) {
 for (var j = 0; j < source[i].dependencies.length; j++) {
 // queue the dependencies
 sourceFiles.add(source[i].dependencies[j]);
 }
 } else {
 source[i].dependencies = [];
 }
 // add to queue to be loaded
 // flag file is queued for loading
 sourceFiles.queued.push(source[i].baseFile);
 sourceFiles.source.push(source[i]);
 }
 },
 load: function() {
 var versionCheckLength = sourceFiles.versionCheck.length;
 for (var i = 0; i < sourceFiles.versionCheck.length; i++) {
 if (sourceFiles.versionCheck[i].timeStamp > 0 && !sourceFiles.versionCheck[i].complete) {
 // if the response from the server not yet received - set interval to wait for it
 if (typeof sourceFiles.versionCheck[i].interval !== "undefined") {
 // interval already running
 return;
 }
 sourceFiles.versionCheck[i].interval = setInterval(function() {
 if (sourceFiles.versionCheck[i].complete || Date.now() - sourceFiles.versionCheck[i].timeStamp >= sourceFiles.versionCheckingTimeout) {
 clearInterval(sourceFiles.versionCheck[i].interval);
 if (!sourceFiles.versionCheck[i].complete) {
 console.log("Timeout checking js version (" + i + ")");
 }
 sourceFiles.versionCheck[i].complete = true;
 sourceFiles.load();
 }
 }, 10);
 return;
 }
 }
 for (var i = 0; i < sourceFiles.source.length; i++) {
 if (versionCheckLength !== sourceFiles.versionCheck.length) {
 // if version checking has been added - start over
 sourceFiles.load();
 return;
 }
 // remove dependencies that are already loaded
 sourceFiles.removeDependencies(sourceFiles.source[i]);
 // load all files with no dependencies
 if (sourceFiles.source[i].dependencies.length == 0) {
 if (sourceFiles.source[i].loaded) {
 // file already loaded
 continue;
 //} else if (sourceFiles.loading.indexOf(sourceFiles.source[i].baseFile) !== -1){
 // files already added to load queue
 //continue;
 } else if (typeof sourceFiles.source[i].file == "function") {
 // function - execute the function
 var funcName = /function ([^\(]+)?/.exec(sourceFiles.source[i].file.toString())[0];
 if (funcName == "function onload") {
 // special function that has no body
 var result = (document.readyState === "complete" || document.readyState === "interactive");
 } else {
 //v("executing " + sourceFiles.source[i].file.toString());
 var result = sourceFiles.source[i].file();
 }
 if (result !== false) {
 sourceFiles.source[i].loaded = true;
 sourceFiles.onLoad({
 target: {
 src: sourceFiles.source[i].baseFile
 }
 });
 }
 continue;
 }
 // test if namespace specified and if already exists
 if (sourceFiles.alreadyLoadedNs(sourceFiles.source[i].ns)) continue;
 if (sourceFiles.loading.indexOf(sourceFiles.source[i].baseFile) !== -1) continue;
 var version = "";
 if (sourceFiles.fileInfo.find(function(fileInfo) {
 if (fileInfo.baseFile == sourceFiles.source[i].baseFile) {
 version = '.' + fileInfo.time + '.';
 return true;
 }
 })) {
 // keep baseFile the same - change the full filename with version
 // replaces my.file.js with my.file.123456.js where 123456 is the file timestamp
 sourceFiles.source[i].file = sourceFiles.source[i].file.replace(/\.(?!.*?\.)/, version);
 }
 // flag loading file
 sourceFiles.loading.push(sourceFiles.source[i].baseFile);
 // file - load the source file
 loadSourceFile(sourceFiles.source[i].file, sourceFiles.source[i].type, sourceFiles.onLoad)
 }
 }
 },
 alreadyLoadedNs: function(ns) {
 // test if namespace specified and if already exists
 if (typeof ns == "undefined") return false;
 var exists = true;
 var path = ns.split(".");
 for (var j = 0; j < path.length; j++) {
 if (typeof $msRoot[path[j]] == "undefined") {
 // namespace not yet created
 return false;
 }
 }
 // namespace exists
 return true;
 },
 onLoad: function(e) {
 // flag file as loaded
 var baseFile = e.target.src.substr(e.target.src.lastIndexOf("/") + 1);
 var split = baseFile.split("?");
 baseFile = split[0];
 // remove the version timestamp from the filename
 baseFile = baseFile.replace(/(.+)\.([0-9])+\.(js|css|php|jpg|gif|png)$/, "1ドル.3ドル");
 for (var i = 0; i < sourceFiles.source.length; i++) {
 if (sourceFiles.source[i].baseFile == baseFile) {
 sourceFiles.source[i].loaded = true;
 if (sourceFiles.source[i].onLoad) {
 // custom onLoad
 sourceFiles.source[i].onLoad();
 }
 break;
 }
 }
 sourceFiles.load();
 },
 removeDependencies: function(source) {
 if (!source.loaded) {
 for (var j = source.dependencies.length - 1; j >= 0; j--) {
 // test if namespace specified and if already exists
 if (typeof source.dependencies[j].ns !== "undefined" && sourceFiles.alreadyLoadedNs(source.dependencies[j].ns)) {
 // loaded - remove the dependencey
 source.dependencies.splice(j, 1);
 continue;
 }
 for (var k = 0; k < sourceFiles.source.length; k++) {
 var source2 = sourceFiles.source[k];
 if (source2.baseFile == source.dependencies[j].baseFile) {
 // found the dependency
 if (source2.loaded) {
 // loaded - remove the dependencey
 source.dependencies.splice(j, 1);
 }
 break;
 }
 }
 }
 }
 },
 versionChecking: function(path) {
 // poll server for file times for specified directories
 if (!Array.isArray(path)) {
 path = [path];
 }
 sourceFiles.versionCheck.push({});
 var versionCheck = sourceFiles.versionCheck[sourceFiles.versionCheck.length - 1]
 versionCheck.id = sourceFiles.versionCheck.length;
 versionCheck.timeStamp = Date.now();
 versionCheck.complete = false
 versionCheck.path = path;
 var url = $ms.LINK_SITE_ROOT + "/moddate.php";
 var data = {
 path: path
 };
 data = JSON.stringify(data);
 var http = new XMLHttpRequest();
 var params = "id=moddate-js&url=" + url + "&otherData=" + data;
 http.open("POST", url, true);
 //Send the proper header information along with the request
 http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
 http.onreadystatechange = function() { //Call a function when the state changes.
 if (http.readyState == 4 && http.status == 200) {
 //alert(http.responseText);
 var response = http.responseText;
 var data = JSON.parse(response);
 var error = false;
 if (typeof data !== "object" || !data.status) {
 console.log("Invalid version checking response: " + response);
 error = true;
 } else if (data.status.toLowerCase().indexOf("error") !== -1) {
 console.log(data.status);
 error = true;
 } else if (data.status.toLowerCase().indexOf("success") == -1) {
 console.log("Unknown response (missing success): " + response);
 error = true;
 }
 if (!error) {
 console.log("Version information loaded");
 sourceFiles.fileInfo = sourceFiles.fileInfo.concat(data.result);
 }
 // if there was an error, will load files without version info
 versionCheck.complete = true;
 sourceFiles.load();
 }
 }
 http.send(params);
 }
}
// dynamically load a js or css file
function loadSourceFile(filename, filetype, onloadFn) {
 if (typeof filetype == "undefined") {
 filetype = filename.substr(filename.lastIndexOf('.') + 1)
 }
 if (filetype == "js") {
 // load js file
 var item = document.createElement('script');
 item.type = "text/javascript";
 item.src = filename;
 } else if (filetype == "css") {
 //load CSS file
 var item = document.createElement("link");
 item.rel = "stylesheet";
 item.type = "text/css";
 item.href = filename;
 } else if (filetype == "img") {
 // preloading images
 var item = document.createElement("img");
 item.style.display = "none";
 item.src = filename;
 }
 if (typeof onloadFn !== "undefined") {
 item.onload = onloadFn;
 //item.onreadystatechange = runFn;
 }
 if (typeof item != "undefined") {
 if (filetype == "img") {
 document.body.appendChild(item);
 } else {
 document.head.appendChild(item);
 }
 }
}
asked May 20, 2017 at 7:36
\$\endgroup\$
2
  • \$\begingroup\$ Note: RewriteRule ^(js|js-common|css|css-common|img|img-common) could be RewriteRule ^(js|css|img)(-common)? \$\endgroup\$ Commented May 25, 2017 at 13:46
  • 1
    \$\begingroup\$ Also, this: file: function miscFn1(){doSomethingOnLoad()} could be file: doSomethingOnLoad. (unless you really need to alias doSomethingOnLoad as miscFn1) \$\endgroup\$ Commented May 25, 2017 at 13:50

1 Answer 1

1
\$\begingroup\$

Well, I did not check or run the whole code, but I do see lots of code repetition that you should avoid. For example, returning a windows.location should use a help function.

So these kind of lines

return window.location.origin ? window.location.origin + '/' : window.location.protocol + '/' + window.location.host + "/js-common";

could be:

return buildURL("/js-common")

It's not only about the size of the code, it is mainly for other reasons, like, reusability, readability, maintainability, etc. Some times we prefer more size if it comes with one or more of these pros.

UPDATE

Checking once more your code, there is a bunch of repeted code you should turn into methods. For instance, this whole part

var dir = "";
if (typeof source[i].dir == "undefined") {
 // default = js-common
 dir = sourceFiles.staticJsCommon();
} else if (typeof source[i].dir == "js-common") {
 dir = sourceFiles.staticJsCommon();
} else if (typeof source[i].dir == "css-common") {
 dir = sourceFiles.staticCssCommon();
} else if (typeof source[i].dir == "img-common") {
 dir = sourceFiles.staticImgCommon();
}

should become one method with one param

var dir = sourceFiles.handleFileDir(source[i].dir)

and all the methods below

staticRoot : function () {
 if (typeof $ms.STATIC_TOP_ROOT !== "undefined") {
 return $ms.STATIC_TOP_ROOT;
 } else {
 return window.location.origin ? window.location.origin + '/' : window.location.protocol + '/' + window.location.host;
 }
},
staticJsCommon : function () {
 if (typeof $ms.STATIC_JS_COMMON !== "undefined") {
 return $ms.STATIC_JS_COMMON;
 } else {
 return window.location.origin ? window.location.origin + '/' : window.location.protocol + '/' + window.location.host + "/js-common";
 }
},
staticImgCommon : function () {
 if (typeof $ms.STATIC_IMG_COMMON !== "undefined") {
 return $ms.STATIC_IMG_COMMON;
 } else {
 return window.location.origin ? window.location.origin + '/' : window.location.protocol + '/' + window.location.host + "/img-common";
 }
}

are really only one method. Could be something like

handleFileDir : function (pathPart) {
 // decide on type ref and define the var
 var typeToCheck = function (_path) {
 if (_path.indexOf("js") > -1) return "STATIC_JS_COMMON";
 if (_path.indexOf("css") > -1) return "STATIC_CSS_COMMON";
 if (_path.indexOf("img") > -1) return "STATIC_IMG_COMMON";
 return "STATIC_TOP_ROOT"; 
 }(pathPart);
 if (typeof $ms[typeToCheck] !== "undefined") {
 return $ms[typeToCheck];
 } else {
 return window.location.origin ? window.location.origin + '/' : window.location.protocol + '/' + window.location.host + pathPart;
 }
}

Always be suspicious of code smell when parts of your code look alike so much...

answered May 24, 2017 at 9:35
\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the suggestion. With one liners I tend to resist creating another function. But I can do better with the other more significant cases. \$\endgroup\$ Commented May 25, 2017 at 6:59
  • \$\begingroup\$ It's not only about the size, it is mainly for other reasons, like, reusability, readability, maintainability, etc \$\endgroup\$ Commented May 25, 2017 at 11:48

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.