Posted 02/13/2008 in coldfusion

When building a large-scale AJAX application the size and number of supporting Javascript files grows quickly. There are many projects out there to aid in the optimized delivery of so many AJAX application assets which essentially consolidate several Javascript files into one file, "compress" via JSMin and finally compress via mod_deflate or simple gzip HTTP compression.

What I put together is just a simple homebrew package and deploy system to do all except the HTTP compression. This means I have a single Coldfusion process that finds all appropriate Javascript files to consolidate into one and JSMin'ify it. The example below resides in the <head></head> tags for compliance. It regenerates the core.js and core.jsgz file based off subdirectories (considered modules) each with .js files that will later be consolidated. It performs the JSMin and gzip calls as though it was a Linux server. This block of code is only intended on rebuilding the core.js and core.jsgz files in the development environment upon a forceful SHIFT+RELOAD. This relies on a deployment system that will deploy the appropriate core.js/gz files to the live server. This means that while making code changes to a .js file in a module's subdirectory in the development environment, you'll need to SHIFT+RELOAD to ensure your changes were updated in the core.js and core.jsgz files. The example below assumes you have two "module" subdirectories, "core" and "tickets" that contain any number of .js files to be consolidated into the core.js/gz files. This script also sets up a default object "namespace" based off the module name, i.e. "core" will setup a core object namespace.
<!--- refresh the core only if on dev AND Pragma header is sent (SHIFT+CTRL+RELOAD) --->
<cfif REFindNoCase("", CGI.http_host) and StructKeyExists(GetHttpRequestData().headers, "Pragma")>

	<!--- remove existing core - if it exists --->
	<cfset coreFile = GetDirectoryFromPath(GetTemplatePath()) & "core.js">
	<cfif FileExists(coreFile)>
		<cffile action="delete" file="#coreFile#">

	<!--- list of MODULES - their directory name and namespace are the same --->
	<cfloop list="core,tickets" index="module" delimiters=",">
		<cfset moduleDir = GetDirectoryFromPath(GetTemplatePath()) & module & "/">
		<!--- Check to see if the module directory exists --->
		<cfif DirectoryExists(moduleDir)>

			<!--- prepend the namespace for this module based off the module's directory to the core --->
			<cfset namespaceDef = "var #module# = {};">
			<cffile action="append" file="#coreFile#" output="#namespaceDef#" addNewLine="Yes" charset="utf-8">

			<!--- list the javascript files in the module directory - no need for recursion here single folder deep module dirs --->
			<cfdirectory action="list" directory="#moduleDir#" filter="*.js" name="moduleFiles">
   			<cfif moduleFiles.recordcount gt 0>
   				<!--- loop through the returned files --->
   				<cfloop query="moduleFiles">
   					<!--- make sure each "file" is not a directory --->
   					<cfif moduleFiles.type is "File">

						<!--- execute a bash shell to use JSMIN and pipe the file in question to STDIN --->
   						<cfexecute name="/bin/bash" arguments="-c ""/usr/local/bin/jsmin < #moduleDir &""" variable="jsMinString" timeout="10"></cfexecute>
   						<!--- append to the core --->
   						<cffile action="append" file="#coreFile#" output="#jsMinString#" addNewLine="Yes" charset="utf-8">


	<!--- now compress the core --->
	<cfif FileExists(coreFile)>
		<cfexecute name="/bin/bash" arguments="-c ""/bin/gzip -9 -c #coreFile# > #coreFile#gz""" variable="jsMinStringCompress" timeout="10"></cfexecute>


The above example also assumes you have a compiled version of jsmin.c residing in /usr/local/bin/ as well as a working version of gzip in the /bin/ directory. We're using the best gzip -9 compression as well.

The HTTP compression has to take place through another process due to the corruption of compressed Javascript files in very specific versions of MSIE 6.0 (pre-IE6 SP1).

Microsoft admission of the IE6 Javascript Compression issue: This process saves two versions of the consolidated and JSMin simplified javascript file using gzip. Coldfusion is then used to determin if the browser is capable of running the compressed version or non compressed version of the core.js file based off if using IE6 or a browser that contains "gzip" or "deflate" in the "Accept-Encoding" request header, as follows:
<!--- load uncompressed core javascript file for MSIE 6 browsers --->
<cfif 	StructKeyExists(GetHttpRequestData().headers, "Accept-Encoding") and
	REFindNoCase("[gzip|deflate]", GetHttpRequestData().headers["Accept-Encoding"]) gt 0 and
	REFindNoCase("MSIE 6", CGI.HTTP_USER_AGENT) eq 0>
	<script language="JavaScript" type="text/javascript" src="core.jsgz"></script>
	<script language="JavaScript" type="text/javascript" src="core.js"></script>

The fact that we pre-compress the Javascript file is important in many ways. If you do not have control over mod_deflate in a hosted environment, are running mod_cache which makes it tough (if not impossible) to run mod_deflate, or get so many hits that you see degradation of server performance under load seeing as every incoming request demands the core.js be compressed on a per-hit basis or some other reason that prohibits the use of mod_deflate. In my case, mod_deflate is too much of a performance penalty as well as mod_cache being used. That's why we pre-compress the file - much less work for these servers to do - especially seeing this is the largest file that gets served from this cluster of servers. We now need to coax Apache to force the appropriate headers, Content-Type and Content-Encoding if the browser requests the .jsgz file. The way to accomplish this is to put the <FilesMatch> declaration in the httpd.conf, .htaccess, or any <VirtualHost> declaration:
<FilesMatch "\.jsgz$">
	ForceType text/javascript
	Header set Content-Encoding gzip

In conclusion this seems to work with IE6+, FF*, and Safari in terms of the most efficient delivery of a boatload of Javascript code. This also allows for the development environment to both act as the live environment as well as build a deployable updated and consolidated Javascript file.
new comment
EMAIL (hidden)
MESSAGE TAGS ALLOWED: <code> <a> <pre class="code [tab4|tabX|inline|bash]"> <br>