I recently did some work on a large web application that had several .js and .css files. Per industry best practice, I wanted to use something like YUI Compressor or Google Closure Compiler to merge and minify all the JavaScript and CSS into a single .js file and a single .css file. Also, I wanted an easy way to switch from these optimized files–which are hard to read–to the original files during development (or to debug an issue in production).
In this case, the JS/CSS needed to be minified at build-time. The project was built via Maven, so it made sense to do this with a Maven project. After a lot of research, I chose a plugin called wro4j because it allows you to use an external XML file to configure exactly which files should be minified/optimized. And by making this XML file part of the website itself, you can use the information it contains (i.e., the list of individuals .js and .css files) to dynamically switch from the compressed files to the “raw” ones. The main benefit? The list of .js and .css files exists in only one place–the XML config file. Here’s what it looks like:
/css/reset.css /css/master.css /css/typography.css /js/lib/swfaddress/swfaddress.js /js/constants.js /js/util.js ...
As I said, this configuration is used by the wro4j plugin when it minifies/compresses/optimizes the code at build-time. I won’t go into the details of how to configure wro4j here; see the wro4j website for info on that.
So assuming you have wro4j configured to generate myapp-all-compressed.js and myapp-all-compressed.css, the next thing you’ll do is modify your HTML doc to use those files:
<html>
<head>
<link rel="stylesheet" type="text/css" href="myapp-all-compressed.css"/>
<script type="text/javascript" src="myapp-all-compressed.js"></script>
</head>
...
</html>
So how can we easily switch from these hard-to-read files to the “original” files? We’ll use JavaScript to download the aforementioned XML config file (resources.xml), parse it, and dynamically add <script> tags for each .js and .css file. But first, we’ll modify our HTML template (in this case, a JSP page) to check for a URL parameter and use our “dynamic script loader” instead of the production files:
<html>
<head>
<% if( request.getParameter("debug") != null ) { %>
<%-- DEBUG MODE: Dynamically load individual .js/.css files --%>
<script type="text/javascript" src="loadIndividualJsCssFiles.js"></script>
<script type="text/javascript">
myapp.util.loadIndividualJsCssFiles('resources.xml');
</script>
<% } else { %>
<%-- PRODUCTION MODE --%>
<link rel="stylesheet" type="text/css" href="myapp-all-compressed.css"/>
<script type="text/javascript" src="myapp-all-compressed.js"></script>
</head>
...
</html>
As you can see, navigating to http://yoursite.com/ results in the “optimized for production” files being used, but something like http://yoursite.com/?debug causes the browser to instead download loadIndividualJsCssFiles.js and call myapp.util.loadIndividualJsCssFiles() with the path to resources.xml.
Finally, let’s take a look inside loadIndividualJsCssFiles.js. At a high level, it extracts the list of .js/.css files from resources.xml and dynamically adds <script> and <link> nodes to the browser’s DOM for each JavaScript and CSS file. Note: it’s easiest to start at the bottom and read up.
if( myapp === undefined ) { myapp = {}; }
if( myapp.util === undefined) { myapp.util = {}; }
/**
* Use this function to dynamically load a single CSS file.
*
* @param {String} path - URL path to the .css file that should be loaded
*/
myapp.util.loadStylesheetFile = function(path) {
var linkElement = document.createElement('link');
linkElement.setAttribute("type", "text/css");
linkElement.setAttribute("rel", "stylesheet");
linkElement.setAttribute("href", path);
// Append the new <link> element to the end of <head>. The browser should
// start downloading it immediately.
myapp.console.info('Loading '+ path);
document.getElementsByTagName("head")[0].appendChild(linkElement);
};
/**
* Use this function to dynamically load a single JavaScript file.
*
* @param {String} path - URL path to the .js file that should be loaded
* @param {Object} callbackFcn - (optional) A function that will be called if/when the script finishes loading
*/
myapp.util.loadJavaScriptFile = function(path, callbackFcn) {
var elementOnLoadFcn,
scriptElement = document.createElement('script');
scriptElement.setAttribute("type", "text/javascript");
if( callbackFcn != undefined && callbackFcn != null ) {
// Create a function that will be used as the value for the <script>
// element's "onLoad" and "onreadystatechange" attributes.
elementOnLoadFcn = function() {
// This function can work for both 'onlload' and 'onreadystatechange' events
// Note: When this code is run 'this' will refer to the <script> object.
if ( this.readyState && this.readyState != "complete" && this.readyState != "loaded" ) {
return;
}
this.onload = this.onreadystatechange = null; // ensure callback is only called once
// Execute callback that indicates the script was loaded
callbackFcn(this.src);
};
// Configure the <script> element's onload and onreadystatechange attributes
scriptElement.onload = elementOnLoadFcn;
scriptElement.onreadystatechange = elementOnLoadFcn;
}
scriptElement.setAttribute("src", path);
// Append the new <script> element to the end of <head>. The browser should
// start downloading it immediately; when finished, the elementOnLoadFcn
// function (if it exists) will run.
myapp.console.info('Loading '+ path);
document.getElementsByTagName("head")[0].appendChild(scriptElement)
};
/**
* Use this function to dynamically load multiple JavaScript files.
*
* @param {Array} filePathsArr - Array of URL paths to the .js files that should be loaded
*/
myapp.util.loadJavaScriptFiles = function(filePathsArr) {
var jsFileIndex = 0,
loadNextScriptFcn = function() {
if (jsFileIndex < filePathsArr.length) {
myapp.util.loadJavaScriptFile(filePathsArr[ jsFileIndex++ ], loadNextScriptFcn);
}
};
loadNextScriptFcn();
};
/**
* Use this function to dynamically load a single JavaScript file.
*
* @param {String} path - Path to a wro4j resources XML file.
*/
myapp.util.loadIndividualJsCssFiles = function(wro4jResourcesFile) {
// JS best practice: declare vars at start of fcn (for more info, search "javascript hoisting")
var i,
cssNodesArr,
jsNodesArr,
nodesArrLength,
request,
jsFilePathsArr = [],
resourcesDoc;
// Get the XML file which has the list of all individual .js and .css files
// used by the front-end.
if (window.XMLHttpRequest) { // IE7+, Firefox, Chrome, Opera, Safari
request = new XMLHttpRequest();
} else { // IE5, IE6
request = new ActiveXObject("Microsoft.XMLHTTP");
}
request.open("GET", wro4jResourcesFile, false);
request.send();
resourcesDoc = request.responseXML;
// Load all stylesheet files specified by <css> elements in resources file
cssNodesArr = resourcesDoc.getElementsByTagName("css");
nodesArrLength = cssNodesArr.length; // Best practice: iterate HTMLCollection by caching array length
for(i = 0; i < nodesArrLength; i++) {
myapp.util.loadStylesheetFile( cssNodesArr[i].childNodes[0].nodeValue );
}
// Load all JavaScript files specified by <js> elements in resources file
jsNodesArr = resourcesDoc.getElementsByTagName("js");
nodesArrLength = jsNodesArr.length; // Best practice: cache HTMLCollection length instead of re-calculating for each iteration
jsFilePathsArr = new Array();
for(i = 0; i < nodesArrLength; i++) {
jsFilePathsArr.push( jsNodesArr[i].childNodes[0].nodeValue );
}
myapp.util.loadJavaScriptFiles( jsFilePathsArr );
};
The end result is that you keep the list of .js/.css files in one place (resources.xml)–you don’t have to keep updating your .jsp or pom.xml file each time you need to add or remove .js/.css files. The loadJavaScriptFiles() function will ensure those files get picked up the next time you refresh the browser with the ‘debug’ URL param, and wro4j will similarly include those files in myapp-all-compressed.js/myapp-all-compressed.css the next time a build is done.

