Created
April 16, 2012 17:48
-
-
Save staylor/2400270 to your computer and use it in GitHub Desktop.
a re-write of how eMusic handles site-wide AJAX
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*globals window, document, $, jQuery */ | |
(function ($) { | |
"use strict"; | |
var loaded = false, | |
hashbang = '#!', | |
anychar = '.*?', | |
the_hash, | |
lastPath, | |
thePath, | |
lastXhr, | |
master = {}, | |
archive = {}, | |
archives = {}, | |
events = ['init', 'start', 'always-before', 'before', 'execute', 'always-after', 'after', 'end'], | |
wrapper, | |
title, | |
head, | |
re_head = new RegExp('<head[^>]+>(.+)</head>'), | |
re_body = new RegExp('<body[^>]+?>.+?</body>'), | |
re_title = new RegExp('<title>(.+)</title>'), | |
re_content = new RegExp('<div id="the-content">(.+)<!--the-content--></div>'), | |
re_body_classes = new RegExp('<body.*?class="([^"]+)".*?>'), | |
re_metas = new RegExp('<meta[^>]+/?>', 'g'), | |
re_linked = new RegExp('<link rel=[\'"](?:alternate|prev|next)[\'"][^>]+?>', 'g'), | |
re_hrefs, | |
re_srcs; | |
function quotify(name, value) { | |
return [name, '=', '[\'"]', value, '[\'"]'].join(''); | |
} | |
function urlify(ext) { | |
return ['(https?://', window.location.host, '/.+?', ext, '.*?)'].join(''); | |
} | |
re_hrefs = new RegExp( | |
['<link', quotify('rel', 'stylesheet'), quotify('href', urlify('.css')), '/?>'].join(anychar), | |
'g' | |
); | |
re_srcs = new RegExp( | |
['<script', quotify('type', 'text/javascript'), quotify('src', urlify('.js')), '></script>'].join(anychar), | |
'g' | |
); | |
/** | |
* Support AJAX-legacy URLs (e.g. /listen/#/browse/album/all/) | |
* Support poorly pasted URLs (e.g. /browse/album/all/#/browse/book/all/) | |
* Redirect hashbang URLs (e.g. /browse/album/all/#!/browse/book/all/) | |
* | |
* Redirect to their hash value | |
* (this can't be done on the server) | |
* | |
* Hashbang URLs sent to server as ?_escaped_fragment_=/browse/album/all/ | |
* get 301 redirected to their value | |
* | |
*/ | |
the_hash = window.location.hash; | |
if (the_hash && '#' !== the_hash && -1 < the_hash.indexOf('/')) { | |
window.location.href = the_hash.replace(hashbang, '').replace('#', ''); | |
return; | |
} | |
function remove_excess_ws(data) { | |
return data.replace(/\t|\n|\r/g, '').replace(/\s+/g, ' '); | |
} | |
function extract_head(data) { | |
var parsed = re_head.exec(data); | |
return parsed[1]; | |
} | |
function extract_body(data) { | |
var parsed = re_body.exec(data); | |
return parsed[0]; | |
} | |
function parseURL(url) { | |
var a = document.createElement('a'); | |
a.href = url; | |
return { | |
source: url, | |
protocol: a.protocol.replace(':', ''), | |
host: a.hostname, | |
port: a.port, | |
query: a.search, | |
params: (function () { | |
var ret = {}, | |
seg = a.search.replace(/^\?/, '').split('&'), | |
len = seg.length, | |
i = 0, | |
s; | |
for (i; i < len; i += 1) { | |
if (seg[i]) { | |
s = seg[i].split('='); | |
ret[s[0]] = s[1]; | |
} | |
} | |
return ret; | |
}()), | |
file: (a.pathname.match(new RegExp('/([^/?#]+)$', 'i')) || ['', ''])[1], | |
hash: a.hash.replace('#', ''), | |
path: a.pathname.replace(new RegExp('^([^/])'), '/$1'), | |
relative: (a.href.match(new RegExp('tps?://[^/]+(.+)')) || ['', ''])[1], | |
segments: a.pathname.replace(new RegExp('^/'), '').split('/') | |
}; | |
} | |
function add_action(action, callback) { | |
if (jQuery.isFunction(callback) && jQuery.inArray(action, events)) { | |
if (master.hasOwnProperty(action)) { | |
master[action].push(callback); | |
} else { | |
master[action] = [callback]; | |
} | |
} | |
} | |
function do_action(value) { | |
if (jQuery.inArray(value, events) && master.hasOwnProperty(value)) { | |
$(master[value]).each(function () { | |
this(value, thePath); | |
}); | |
} | |
} | |
function do_actions(actions) { | |
$(actions).each(function () { | |
do_action(this); | |
}); | |
} | |
function flush_action(action) { | |
if (master.hasOwnProperty(action)) { | |
delete master[action]; | |
} | |
} | |
function flush_actions(actions) { | |
$(actions).each(function () { | |
flush_action(this); | |
}); | |
} | |
function empty_content() { | |
wrapper.html(''); | |
} | |
function transition_content(body_classes, body) { | |
document.body.className = body_classes; | |
wrapper.html(body); | |
} | |
function transition_title(text) { | |
title.html(text); | |
} | |
function transition_meta(metas) { | |
$('meta').remove(); | |
$(metas).each(function () { | |
head.append($(this)); | |
}); | |
} | |
function transition_links(links) { | |
$('link[rel!="stylesheet"]').remove(); | |
$(links).each(function () { | |
head.append($(this)); | |
}); | |
} | |
/** | |
* Skip the minified global-styles file | |
* In Minify is turned off, wait until we hit style.css to start messing with files | |
* These files are hardcoded in a list, not dynamically joined | |
* | |
*/ | |
function after_global_styles(href) { | |
return -1 < href.indexOf('style.css') || -1 < href.indexOf('minify-global-styles'); | |
} | |
function transition_styles(stylesheets) { | |
var at_style = false, link; | |
$(archive.stylesheets).each(function () { | |
if (!at_style && after_global_styles(this)) { | |
at_style = true; | |
} | |
if (at_style) { | |
$('link[href="' + this + '"]').remove(); | |
} | |
}); | |
at_style = false; | |
$(stylesheets).each(function () { | |
if (!at_style && after_global_styles(this)) { | |
at_style = true; | |
} | |
if (at_style) { | |
link = $('<link />').attr({ | |
'href' : this, | |
'rel' : 'stylesheet', | |
'added' : 'ajax' | |
}); | |
head.append(link); | |
} | |
}); | |
} | |
/** | |
* On page load, page-specific scripts will be loaded at the end of <div id="the-content"> | |
* On the first AJAX request, those scripts will get wiped out | |
* When scripts are loaded after AFTER AJAX content has been replaced, | |
* they are appended to the end of the body, not #the-content | |
* After initial page load, scripts with added="ajax" are targeted for removal | |
* | |
*/ | |
function transition_scripts(scripts) { | |
var body = $(document.body); | |
$('script[added="ajax"]').remove(); | |
$(scripts).each(function () { | |
var script = $('<script />').attr({ | |
'src' : this, | |
'text' : 'text/javascript', | |
'added' : 'ajax' | |
}); | |
body.append(script); | |
}); | |
} | |
/** | |
* Use RegEx to parse a giant text blob | |
* | |
*/ | |
function extract_items(data, regx) { | |
var parsed = [], temp = true; | |
while (temp) { | |
temp = regx.exec(data); | |
if (temp) { | |
parsed.push(temp[1]); | |
} | |
} | |
return parsed; | |
} | |
function extract_item(data, regx) { | |
var temp = regx.exec(data); | |
if (null !== temp) { | |
return temp[1]; | |
} | |
return temp; | |
} | |
function extract_raw(data, regx) { | |
var parsed = [], temp = true; | |
while (temp) { | |
temp = regx.exec(data); | |
if (temp) { | |
parsed.push(temp[0]); | |
} | |
} | |
return parsed; | |
} | |
/** | |
* Create a map of data | |
* - - - - - - - - - - - - - | |
* <title> | |
* <meta property=.... | |
* <meta name=.... | |
* <link rel=.... | |
* <link rel="stylsheet"... | |
* <script src=".... | |
* contents of <div id="the-content"> | |
* <body> classes | |
* | |
*/ | |
function get_archive(url, head, body) { | |
var archive = {}; | |
archive.title = extract_item(head, re_title); | |
archive.metas = extract_raw(head, re_metas); | |
archive.links = extract_raw(head, re_linked); | |
archive.stylesheets = extract_items(head, re_hrefs); | |
archive.scripts = extract_items(body, re_srcs); | |
archive.body = extract_item(body, re_content).replace(re_srcs, ''); | |
archive.body_classes = extract_item(body, re_body_classes); | |
/** | |
* Ignore inline scripts, we don't need them / they're bad | |
* If an inline script doesn't load, the bug is its inclusion at all | |
* | |
*/ | |
if (!cache_blacklist(url)) { | |
archives[url] = archive; | |
} | |
console.log(archive); | |
return archive; | |
} | |
function parse_archive(new_archive) { | |
if (new_archive === archive) { | |
return; | |
} | |
empty_content(); | |
transition_styles(new_archive.stylesheets); | |
transition_content(new_archive.body_classes, new_archive.body); | |
transition_title(new_archive.title); | |
transition_meta(new_archive.metas); | |
transition_links(new_archive.links); | |
transition_scripts(new_archive.scripts); | |
archive = new_archive; | |
} | |
function cache_blacklist(path) { | |
/** | |
* Add rules here for when NOT to cache | |
* | |
* 1. Homepage - recs and programmed records need to re-order on pageload | |
*/ | |
return '/' === parseURL(path).path; | |
} | |
function load_page() { | |
if (archives.hasOwnProperty(thePath) && !cache_blacklist(thePath)) { | |
parse_archive(archives[thePath]); | |
do_actions(['always-after', 'after', 'end']); | |
} else { | |
lastXhr = $.ajax({ | |
type : 'get', | |
dataType: 'text', | |
url : thePath, | |
cache : false, | |
success : function (data) { | |
var head, body; | |
data = remove_excess_ws(data); | |
head = extract_head(data); | |
body = extract_body(data); | |
parse_archive(get_archive(thePath, head, body)); | |
do_actions(['always-after', 'after', 'end']); | |
}, | |
error : function () { | |
window.console.log('error :('); | |
} | |
}); | |
} | |
} | |
/** | |
* Cross-domain function from jQuery | |
* | |
*/ | |
function is_cross_domain(url) { | |
// https://github.com/jquery/jquery/blob/master/src/ajax.js | |
var ajaxLocation, ajaxLocParts, parts, rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/; | |
try { | |
ajaxLocation = window.location.href; | |
} catch (e) { | |
ajaxLocation = window.document.createElement("a"); | |
ajaxLocation.href = ""; | |
ajaxLocation = ajaxLocation.href; | |
} | |
ajaxLocParts = rurl.exec(ajaxLocation.toLowerCase()) || []; | |
parts = rurl.exec(url.toLowerCase()); | |
return !!(parts && | |
(parts[1] !== ajaxLocParts[1] || parts[2] !== ajaxLocParts[2] || | |
(parts[3] || (parts[1] === "http:" ? 80 : 443)) !== | |
(ajaxLocParts[3] || (ajaxLocParts[1] === "http:" ? 80 : 443)))); | |
} | |
/** | |
* Ideally, we would loop through a list of registered RegEx patterns | |
* | |
*/ | |
function parse_ignore_paths(href) { | |
return href.match(/\/(?:info|tools)\//) || href.match(/\.mp3|\.m3u|\.emx|\.emp/); | |
} | |
function pathchange_callback() { | |
var hash = window.location.hash; | |
/** | |
* Save the relative path of the current URL | |
* | |
*/ | |
thePath = $.pathchange.detectHistorySupport() ? | |
parseURL(window.location.href).relative : | |
hash.replace(hashbang, ''); | |
/** | |
* Don't load when the page is already loaded | |
* | |
*/ | |
if (thePath !== lastPath) { | |
do_actions(['start', 'always-before', 'before']); | |
flush_actions(['before', 'after']); | |
lastPath = thePath; | |
if (lastXhr) { | |
lastXhr.abort(); | |
} | |
load_page(); | |
return false; | |
} | |
return true; | |
} | |
function bind_pathchange() { | |
$.pathchange.init({interceptLinks: false}); | |
$('a[href]:not(.no-ajax)') | |
.live('click', function () { | |
var rel, url = this.href, ignore = parse_ignore_paths(this.href); | |
/** | |
* Ignore links that match: | |
* 1. Cross-domain | |
* 2. rel="external" | |
* 3. aren't ignored by our regex matcher | |
* | |
*/ | |
if (!is_cross_domain(url) && 'external' !== this.rel && !ignore) { | |
rel = parseURL(url).relative; | |
/** | |
* Modern browsers | |
* | |
*/ | |
if ($.pathchange.detectHistorySupport()) { | |
$.pathchange.changeTo(rel); | |
/** | |
* Old as dirt browsers | |
* | |
*/ | |
} else { | |
window.location.hash = '!' + rel; | |
pathchange_callback(); | |
} | |
return false; | |
} | |
return true; | |
}); | |
$(window).bind('pathchange', pathchange_callback); | |
} | |
window.add_action = add_action; | |
window.onload = function () { | |
var the_head = '', the_body = ''; | |
lastPath = thePath = parseURL(window.location.href).relative; | |
if (!loaded) { | |
loaded = true; | |
/** | |
* actions fired in the 'init' action | |
* only fire once per full page load | |
* | |
*/ | |
do_action('init'); | |
/** | |
* Cache frequently-accessed jQuery selectors | |
* | |
*/ | |
wrapper = $('#the-content'); | |
head = $('head'); | |
title = head.find('title'); | |
/** | |
* When the page loads fully, we can parse the loaded content | |
* to determine our initial archive of data | |
* | |
* Remove whitespace to improve RegEx performance | |
* | |
*/ | |
the_head = remove_excess_ws($('head').html()); | |
the_body = remove_excess_ws(['<body class="', document.body.className, '">', $('body').html(), '</body>'].join('')); | |
/** | |
* Set inital "archive" of page data | |
* | |
*/ | |
archive = get_archive(thePath, the_head, the_body); | |
/** | |
* bind Pathchange whether we degrade or not | |
* | |
*/ | |
bind_pathchange(); | |
do_actions(['always-after', 'after', 'end']); | |
} | |
}; | |
}(jQuery)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment