I just want to know if this is reasonably good implementation of including local/remote JavaScript files.
The function takes a dictionary with (string) .src
whitespace-separated list of urls to include/execute in global context, and (func) .done/.error/.load
callbacks to run in corresponding cases, resolves URLs to absolute, temporarily inserts <script>
blocks in page header, caches loaded addresses, and attaches few static properties: (object) .defaults
and (func) .ls/.reload
to main includejs()
function.
// #includejs
;((function (name, def) {
this[name] = def(document);
}).call(
this, "includejs", function (doc) {
// will be used to reference private includejs() version
var _include;
// holds cached script urls
var imported = {};
// no-op callback default
var pass = function () {};
// helpers
var _ = {
// <a> helper element to resolve urls to absolute
anchor: doc.createElement("a"),
// calculates difference of two arrays
// used by includejs() to filter new urls to load
arrdiff: function (a1, a2) {
return a1.filter(_.cbuniq).filter(_.cbdiff, a2);
},
// calculates intersection of two arrays
// used by includejs.reload() to filter cached urls
arrinters: function (a1, a2) {
return a1.filter(_.cbuniq).filter(_.cbinters, a2);
},
cbdiff: function (node) {
//this: a2[]
return this.indexOf(node) == -1;
},
cbinters: function (node) {
//this: a2[]
return this.indexOf(node) != -1;
},
cbpropcp: function (name) {
//this: {src{}, target{}}
this.target[name] = this.src[name];
},
cbrmprop: function (name) {
//this: target{}
try {
delete this[name];
} catch (e) {}
},
cbuniq: function (node, idx, arr) {
return idx <= arr.indexOf(node);
},
// shallow copies an (object) node
// used by includejs.ls() to list cached urls
cp: function (node) {
var nodecp = {};
_.keys(node).forEach(_.cbpropcp, {src: node, target: nodecp});
return nodecp;
},
// default settup
defs: {
src : "",
done : pass,
error : pass,
load : pass
},
// removes passed properties from (object) node
// used in includejs.reload() to filter cached urls
del: function (node) { //, ...props
_.arrinters(_.keys(imported), _.slc(arguments, 1))
.forEach(_.cbrmprop, node);
return node;
},
// helper for wssplit() for filter out empty strings
fnid: function (node) {
return node;
},
// <head> reference for temporarily injecting <script>-s
h: doc.getElementsByTagName("head")[0],
// attaches properties of (object) items to (object) target
// used in few places to assign object properties
init: function (target, items) {
for (var name in items) {
if (items.hasOwnProperty(name)) {
target[name] = items[name];
}
}
return target;
},
isfn: function (node) {
return "function" == typeof node;
},
isplainobj: function (node) {
return "[object Object]" == Object.prototype.toString.call(node);
},
isstr: function (node) {
return "[object String]" == Object.prototype.toString.call(node);
},
keys: function (node) {
return Object.keys(Object(node));
},
now: Date.now,
// calculates absolute url coresponding to given (string) url
// not sure if this works on old ies
path2abs: function (url) {
_.anchor.href = ""+ url;
return _.anchor.href;
},
// matches whitespace globaly
// copy-pasted from es5-shim.js
// https://github.com/es-shims/es5-shim.git
rews: /[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]+/g,
// used for converting dynamic arguments object to static array
slc: function () {
return Array.prototype.slice.apply(Array.prototype.shift.call(arguments), arguments);
},
// empties an object
// used in includejs.reload() to empty private url cache
// forcing reload
vacate: function (node) {
for (var name in node) {
if (!Object.prototype.hasOwnProperty(name)) {
delete node[name];
}
}
return node;
},
// splits whitespace-separated string to it's components
// returns array of uniqe (string) urls
// used by includejs() to turn (string) .src urls to array
wssplit: function (str) {
return (""+ str).split(_.rews).filter(_.cbuniq).filter(_.fnid);
}
};
// main function
_include = function (settup) {
var opts = {};
_.isplainobj(settup) || (settup = {});
// take only uncached absolute script urls
opts.src =
_.arrdiff(
_.wssplit(_.isstr(settup.src) ? settup.src : "").map(_.path2abs),
_.keys(imported)
);
opts.done = _.isfn(settup.done) ? settup.done : _.defs.done;
opts.error = _.isfn(settup.error) ? settup.error : _.defs.error;
opts.load = _.isfn(settup.load) ? settup.load : _.defs.load;
// acts as counter to track download progress
opts.i = 0;
// holds <script> nodes for caching, and for callback clean-up afterward
opts.s = [];
// for tracking download progress, when opts.i == opts.len download is done
opts.len = opts.src.length;
if (opts.len) {
opts.src.forEach(_requireloadcb, opts);
} else {
// asyncs _requireloadcompletecb()
setTimeout(function () {
_requireloadcompletecb(opts);
});
}
return opts.src.slice(0);
};
// export
// adds 'includejs' function identifier to global scope
return _.init(_include, {
defaults: _.defs,
// query cached urls
ls: function () {
return _.cp(imported);
},
// reloads cached .src urls
// takes same-format-object as includejs()
reload: function (settup) {
if (_.isstr(settup))
settup = {src: settup};
_.isplainobj(settup) || (settup = {});
if (!settup.hasOwnProperty("src")) {
settup.src = _.keys(imported).join(" ");
_.vacate(imported);
} else {
_.del.apply(null,
[imported]
.concat(_.wssplit(_.isstr(settup.src) ? settup.src : "").map(_.path2abs))
);
}
_include(settup);
}
});
// helpers
// nulls .onload/.onerror handlers
// detaches loaded <script> node
// used by _requireloadcompletecb() to perform cleanup
function _requiregcloadcb (nodescript) {
nodescript.onload = null;
nodescript.onerror = null;
_.h.removeChild(nodescript);
}
// generates new <script> and appends it to <head>
// executing <script>.src file in global context
// used by includejs() to download/execute script files
function _requireloadcb (fileurl) {
//this: {src, done, error, load, i, len, s}
var opts = this;
var nodescript = _.init(doc.createElement("script"), {
onerror : function () { // ...e
//this: <script>
opts.i += 1;
opts.error.apply(this, arguments);
_requireloadcompletecb(opts);
},
onload : function () { // ...e
//this: <script>
opts.i += 1;
imported[fileurl] = _.now();
opts.load.apply(this, arguments);
_requireloadcompletecb(opts);
},
defer : false,
src : fileurl,
type : "application/javascript"
});
//opts.s.push(_.h.removeChild(_.h.appendChild(nodescript)));
opts.s.push(_.h.appendChild(nodescript));
}
// cleans up after scripts load/fail-to-load
// nulls .onload/.onerror handlers
// empties settup (object) opts
function _requireloadcompletecb (opts) {
if (opts.i == opts.len) {
opts.done.apply(doc, opts.src);
opts.s.forEach(_requiregcloadcb);
// opts.s.splice(0, 1/0);
opts.s.splice(0);
_.vacate(opts);
}
}
}
));
//
// use:
//
// includejs({
// src: "lib/_.js //cdnjs.cloudflare.com/ajax/libs/modernizr/2.7.1/modernizr.min.js",
// done: function (scripturls) {
// // _doStuff(scripturls);
// console.log("done", this, arguments);
// },
// load: function (e) {
// // _srcLoaded(this);
// console.log(e, this);
// },
// error: function (e) {
// // _srcFailed(this);
// console.log(e, this);
// }
// });
//
// // console:
// load <script src="http://localhost/sites/xsite/lib/_.js" type="application/javascript">
// load <script src="http://cdnjs.cloudflare.com/ajax/libs/modernizr/2.7.1/modernizr.min.js" type="application/javascript">
// done Document index.htm ["http://localhost/sites/xsite/lib/_.js", "http://cdnjs.cloudflare.../2.7.1/modernizr.min.js"]
//
// console.log(includejs.ls());
// // console:
// Object {
// http://localhost/sites/xsite/lib/_.js = 1394306633258,
// http://cdnjs.cloudflare.com/ajax/libs/modernizr/2.7.1/modernizr.min.js = 1394306633369
// }
//
// includejs.reload({
// done: function (urls) {
// // _doStuffOnReload(urls);
// console.log("reloaded", this, arguments);
// }
// });
//
// // console:
// reloaded Document index.htm ["http://localhost/sites/xsite/lib/_.js", "http://cdnjs.cloudflare.../2.7.1/modernizr.min.js"]
//
-
\$\begingroup\$ There are many script loader libraries out there, some extremely small and simple. What new functionality are you offering over all these other offerings. Or put another way, what benefit do you have to reinventing a solution that has been offered in many tried/true libraries? Here's one for comparison: labjs.com. \$\endgroup\$jfriend00– jfriend002014年03月08日 19:19:36 +00:00Commented Mar 8, 2014 at 19:19
-
\$\begingroup\$ Yes, there are dozens of tested solutions out there, there's nothing new and extraordinary revolutionary in this peace of code I've posted. I was hoping to get/implement critics for this implementation because it is critical in javascript utility library I'm working on... \$\endgroup\$Nikola Vukovic– Nikola Vukovic2014年03月08日 19:53:24 +00:00Commented Mar 8, 2014 at 19:53
-
\$\begingroup\$ I'd suggest that maybe you write up a list of features it has. As your question is now, we're left to try to discern from the code what it is supposed to do and then try to evaluate how well it does that. I rather doubt you will get much feedback without providing a lot more info. And, why can't you use an existing, tested, supported open source library that already exists in your own utility library? Why rewrite from scratch? \$\endgroup\$jfriend00– jfriend002014年03月08日 19:57:19 +00:00Commented Mar 8, 2014 at 19:57
1 Answer 1
Your code is interesting,
you are clearly smart and know JavaScript, but this is a maintenance nightmare. I am assuming you will not take my advice to heart but here goes:
- Do not abbreviate:
isfn
->isFunction
,isstr
->isString
,cbpropcp
-> ? ,slc
->slice
,_requiregcloadcb
-> ? etc. etc. etc. etc. - Also, use lowerCamelCase
- Also, avoid
_xx
for private variables, as per Crockford Name functions for what they do, not how they are used:
fnid: function (node) { return node; },
fnid
is a terrible name, if possible I would refactor the code so that I would not need this function. A better name might bevalue
?- JSHint could not find anything wrong, except that your event handlers do not use
e
, so you do not need to declaree
as a parameter - You use
cbrmprop
and similar functions only once, consider in-lining them settup
->setup
;)opts.s.splice(0, 1/0);
->opts.s.splice();
defer : false,
-> I think this should have been an option
-
\$\begingroup\$ Fully private variables as per Crockford: javascript.crockford.com/private.html -- I see that as problematic though because each time you create a new object, a new set of functions and variables are created when really only the variables should be duplicated. Not having such private variables means lighter code... right? \$\endgroup\$Alexis Wilke– Alexis Wilke2014年03月11日 07:05:19 +00:00Commented Mar 11, 2014 at 7:05
-
\$\begingroup\$ It's more that
_
does not do anything in the language, and it's one more version of Hungarian notation in a way. Trying to find a link.. \$\endgroup\$konijn– konijn2014年03月11日 11:22:08 +00:00Commented Mar 11, 2014 at 11:22