Skip to content

Instantly share code, notes, and snippets.

@addyosmani
Created February 20, 2012 00:43
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save addyosmani/1866817 to your computer and use it in GitHub Desktop.
Save addyosmani/1866817 to your computer and use it in GitHub Desktop.
basket.js - Script loader for caching scripts using localStorage
/* Basket.js
* A script-loader that handles caching scripts in localStorage
* where supported.
* http://addyosmani.com/
* Credits: Addy Osmani, Mathias Bynens, Ironsjp.
* Copyright (c) 2012 Addy Osmani;
* Licensed MIT, GPL
*/
; (function (w, d) {
function basketLoader() {
var
_storagePrefix = "ll-",
_localStorage = function (a, b) {
try {
return (a = localStorage).setItem(b, a), a.removeItem(b), !0
} catch (c) {}
}(),
scripts = [],
scriptsExecuted = 0,
waitCount = 0,
waitCallbacks = [],
// Minimalist Cross-browser XHR
// from https://gist.github.com/991713
getXMLObj = function (s, a) {
a = [a = "Msxml2.XMLHTTP", a + ".3.0", a + ".6.0"];
do
try {
s = a.pop();
return new(s ? ActiveXObject : XMLHttpRequest)(s)
} catch (e) {;
}
while (s)
},
getUrl = function (url, callback) {
var xhr = getXMLObj();
xhr.open("GET", url, true);
xhr.onreadystatechange = function (e) {
if (xhr.readyState === 4) {
callback(xhr.responseText);
}
};
xhr.send();
},
injectScript = function (text) {
var script = d.createElement("script"),
head = d.head || d.getElementsByTagName("head")[0];
script.appendChild(d.createTextNode(text));
head.insertBefore(script, head.firstChild);
},
queueExec = function (waitCount) {
var script, i, j, callback;
if (scriptsExecuted >= waitCount) {
for (i = 0; i < scripts.length; i++) {
script = scripts[i];
if (!script) {
// loading/executed
continue;
}
scripts[i] = null;
injectScript(script);
scriptsExecuted++;
for (j = i; j < scriptsExecuted; j++) {
if (callback = waitCallbacks[j]) {
waitCallbacks[j] = null;
callback();
}
}
}
}
};
return {
add: function (path) {
var key = _storagePrefix + path,
scriptIndex = scripts.length,
_waitCount = waitCount;
scripts[scriptIndex] = null;
if (_localStorage && _localStorage.getItem(key)) {
scripts[scriptIndex] = _localStorage.getItem(key);
queueExec(_waitCount);
} else {
getUrl(path, function (text) {
(_localStorage) && _localStorage.setItem(key, text);
scripts[scriptIndex] = text;
queueExec(_waitCount);
});
}
return this;
},
wait: function (callback) {
waitCount = scripts.length;
if (callback) {
(scriptsExecuted >= waitCount - 1) ? callback() : waitCallbacks[waitCount - 1] = callback;
}
return this;
}
};
}
w['basket'] = basketLoader();
})(this, document);
<html>
<head>
<title>Test</title>
</head>
<body>
<script src="basket.js"></script>
<script>
basket
.add("jquery-1.7.1.min.js").wait()
.add("underscore-min.js")
.add("backbone-min.js").wait(function() {
alert("woot! \o/");
});
</script>
</body>
</html>
@mathiasbynens
Copy link

In injectScript, the script.type = "text/javascript"; line is not needed at all. Also, it’s probably better to append to the first <script> element than to <head> to avoid issues when the script is called from the <head>.

If you insist on using the <head> approach though, d.getElementsByTagName("head")[0] could be d.head || d.getElementsByTagName("head")[0].

The way localStorage is used here might throw an error.

(_localStorage) ? _localStorage.setItem(key, text) : null; could be (_localStorage) && _localStorage.setItem(key, text);.

If you want to save bytes, you could get rid of the getLocalLoader function and just use w['localLoad'] = localLoader(); instead of w['localLoad'] = getLocalLoader();

@addyosmani
Copy link
Author

Thanks for looking at this @mathiasbynens! I'll update accordingly :)

@sindresorhus
Copy link

I would just steal the localStorage feature test from Modernizr.

Whyw['localLoad'] over w.localLoad? Closure Compiler Advanced mode?

Why not just inject it into the body?

injectScript = function (text) {
    var script = d.createElement("script");
    script.appendChild(d.createTextNode(text));
    d.body.appendChild(script);
},

@mathiasbynens
Copy link

@sindresorhus Injecting it into body has the exact same issues as injecting it into head. http://mths.be/ieoa

The localStorage pattern I linked to results in smaller file size after minification compared to just using the Modernizr feature test + using localStorage afterwards. I mentioned this because Addy asked me to look for possible file size / performance optimizations.

@sindresorhus
Copy link

@mathiasbynens Didn't know about that that, neat... Oh, so happy I don't have to support IE7 anymore ;)

Just noticed the link. Interesting article (read some of your other posts too, good stuff ;)), though you don't really save that many bytes that it makes a difference.

Didn't mean hijack the gist, just found it interesting :)

Codegolfed the feature test if your really care about byte size savings:
var l=function(a,b){try{return(a=localStorage).setItem(b,a),a.removeItem(b),!0}catch(c){}}();

@addyosmani
Copy link
Author

Thanks for the input, all. I'm wondering if beyond what's there at the moment if this project should be attempting to do anything else. I don't feel there's a huge amount of value in trying to turn it into (yet another) generic script loader, but if there are features you think are missing happy to take your feedback on board.

Noticed LabJS covers cacheBust and allowDuplicates options (which could be added) but don't know if thats OTT. Also wondering if I should consider expanding this into a generic localStorage asset cacher (e.g to cover templates too) or whether I should just leave that to something else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment