Loading Everything at Once - HTML, CSS & JavaScript
Some Sakai3 front-end developers had a dream:
For our widget loading mechanism we want to load the HTML / CSS and JavaScript in one request (without asking the user to use inline CSS or JavaScript)
It felt like quite a big challenge and I wondered if it would be possible from a front-end point of view or not. So I gave it a go.
What we need from the back-end
In Nakamura (the Sakai3 back-end) we are using Apache Sling in combination with Apache Jackrabbit. Which means that when we go to a URL like twitter.2.json we get information back about the structure of it's contents:
{
"jcr:createdBy": "admin",
"jcr:created": "Mon Apr 19 2010 11:08:18 GMT+0100",
"jcr:primaryType": "sling:Folder",
"css": {
"jcr:createdBy": "admin",
"jcr:created": "Mon Apr 19 2010 11:08:18 GMT+0100",
"jcr:primaryType": "sling:Folder",
"twitter.css": {
"jcr:createdBy": "admin",
"jcr:created": "Mon Apr 19 2010 11:08:18 GMT+0100",
"jcr:primaryType": "nt:file"
}
},
"javascript": {
"jcr:createdBy": "admin",
"jcr:created": "Mon Apr 19 2010 11:08:18 GMT+0100",
"jcr:primaryType": "sling:Folder",
"twitter.js": {
"jcr:createdBy": "admin",
"jcr:created": "Mon Apr 19 2010 11:08:18 GMT+0100",
"jcr:primaryType": "nt:file"
}
},
"twitter.html": {
"jcr:createdBy": "admin",
"jcr:created": "Mon Apr 19 2010 11:08:18 GMT+0100",
"jcr:primaryType": "nt:file",
"jcr:content": {
"jcr:lastModifiedBy": "admin",
":jcr:data": 2465,
"jcr:primaryType": "nt:resource",
"jcr:uuid": "65305673-e757-4ff8-a62a-26f10b11a020",
"jcr:mimeType": "text/html",
"jcr:lastModified": "Mon Apr 19 2010 11:07:54 GMT+0100"
}
}
}
If we remove all the “jcr:” properties on those nodes, it makes it even more clear:
{
"css": {
"twitter.css": {}
},
"javascript": {
"twitter.js": {}
},
"twitter.html": {}
}
We basically get the hierarchical folder structure back for the twitter widget. If we change default GET servlet for sling or if we make a URL to something like twitter.content.json, it would be possible to get something like this back:
{
"css": {
"twitter.css": {
"content": "body {background:#000000}"
}
},
"javascript": {
"twitter.js": {
"content": "alert(\"loaded\");"
}
},
"twitter.html": {
"content": "<!DOCTYPE html><html lang=\"en\"><head><meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\"><title>Loaded page</title><link rel=\"stylesheet\" href=\"css/twitter.css\" type=\"text/css\" /><script type=\"text/javascript\" src=\"javascript/twitter.js\"></script></head><body><h1>Testing whether this could actually work</h1></body></html>"
}
}
And that, would be genuinely awesome. This would mean that our widget could be loaded into one request! Except for the images – but those could get base64 encoded…
About the example
Before I send a request to the back-end to have this feature enabled, I want to make sure that we could actually make it work in cross-browser, fast and easy to use way.
Basically a Sakai3 page process looks like this:
- Load a Sakai3 page with all it's resources (HTML / CSS / JavaScript / Images)
- When this page is loaded, load all the widgets you embedded on that page.
The Sakai3 widget process goes a bit deeper:
- For each widget type, send one GET request to it's path. (e.g. devwidgets/twitter.3.json)
- Run over all the properties in that file and divide it into 3 sections: HTML / CSS and JavaScript.
- Save those 3 sections in separate objects / arrays in the JavaScript memory.
- Remove the original <link> and <script> tags from the widget HTML.
- Add the widget HTML to the container on the Sakai3 page.
- Load the CSS and JavaScript dynamically without doing an extra request.
So instead of doing a request to an HTML file, a CSS file and a JavaScript file, we would just make one request to a JSON file.
vs.
Too be honest I got everything working quite fast (+/- 1,5 hour) but it took a bit longer to make everything cross-browser. Especially the last part, loading the JavaScript and CSS dynamically.
I knew jQuery just recently added the $.getScript but that added an extra Ajax call which I wanted to avoid in the first place. After lurking through the jQuery API and source code, I found out that the $.globalEval function did exactly what I wanted for the JavaScript part.
Maybe I didn't look well enough, but I actually couldn't find any native jQuery method that would load the CSS dynamically. So I wrote something that is heavily based on a post by Stoyan Stephanov:
var head = document.getElementsByTagName('head').item(0);
var tag = $('<link/>');
tag.attr(attributes);
if (tagname === 'style' && tag[0].styleSheet) {
tag[0].styleSheet.cssText = content;
} else {
tag.text(content);
}
head.appendChild(tag[0]);
All the code that I wrote so far is pretty much WIP and in non-development ready state. For instance at the moment I load all the CSS/JavaScript files I get back from the JSON where I should only use the ones defined in the HTML. That and some other little things still need to be fixed. But those are minor things though and I definitely think I have an awesome proof of concept.
I love to share interesting ideas.