zazvorniki
zazvorniki

Reputation: 3602

Coldfusion combining linked JavaScript files

I have been tasked with combining all the external JavaScript files into one call. It seems a bit silly, but this is the task I have been given.

While I thought it would be easy and just use a plugin like https://github.com/zefer/Combine it appears that they are linking files in an odd way.

They are using cfset's to define where these files are. IS there anyway I can put these together in one call or use the plugin above. I've been trying to come up with solutions, but my brain is officially running on empty.

<cfset Application.globalObj.addJsFile(jsfile="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js",location="footer",priority=1) />

Upvotes: 1

Views: 642

Answers (2)

Jules
Jules

Reputation: 2021

You can use a modified version (pasted below) of the tinyMCE compressor I ported many years ago. https://github.com/tinymce/tinymce_compressor/blob/master/tinymce.gzip.cfm

You simply add JS files by adding them to the url comma delimited:

<script src="/includes/packages/cfm-concat/js.cfm//cdnjs.cloudflare.com/ajax/libs/jquery/1.11.1/jquery.min.js,/includes/Packages/lightview-3.4.0-licensed/js/spinners/spinners.min.js"></script>

It handles remote js files, too! The code is not the super duper cleanest, but it does in fact work. Compression and minification is removed, for that is offloaded to a CDN.

<!---
//  This file compresses the TinyMCE JavaScript using GZip and enables
//  the browser to do two requests instead of one for each .js file.
//  Notice: This script defaults the button_tile_map option to true
//  for extra performance.
--->

<cfsavecontent variable="credits">
//  --------------------------------------------------------------------
//  This file was concatenated (and most likely also cached and gzipped)
//  by TinyMCE CF GZIP, a ColdFusion based Javascript Concatenater,
//  Compressor, and Cacher for TinyMCE.
//  V1, Mon Feb 9 9:00:00 -0500 2009
//
//  Copyright (c) 2009 Jules Gravinese (http://www.webveteran.com/)
//
//  TinyMCE CF GZIP is licensed under LGPL license.
//  More details can be found here: http://tinymce.moxiecode.com/license.php
//
//  The gzip functions were adapted and incorporated by permission
//  from Artur Kordowski's Zip CFC 1.2 : http://zipcfc.riaforge.org/
//</cfsavecontent>

<!--- HEADERS --->
<cfheader name="Content-type" value="text/javascript">
<cfheader name="Vary" value="Accept-Encoding">  <!--- HANDLE PROXIES --->
<cfheader name="Expires" value="#dateFormat(dateAdd('d', 10, now()), "dddd, dd mmm yyyy")# #timeFormat(now(), "hh:mm:ss")# GMT">
<cfheader name="Last-Modified" value="Wednesday, 01 Jan 2014 00:00:00 GMT">

<!--- DEFAULT INPUTS --->
<cfparam name="url.diskCache" default="true">

<!--- GET INPUTS --->
<cfparam name="url.files" default="#cgi.path_info#">
<cfset files = listToArray(url.files)>

<cfparam name="url.compress" default="0">

<cfif url.files contains "compress=0">
    <cfset url.compress=0>
</cfif>

<cfset cachePath = expandPath(".\")>
<cfset expiresOffset = createTimeSpan(10,0,0,0)> <!--- Cache for 10 days in browser cache --->
<cfset content = "">
<cfset encodings = arrayNew(2)>
<cfset supportsGzip = false>
<cfset enc = "">
<cfset cacheKey = "">

<!--- COMPRESS OVERRIDE --->
<cfif cgi.HTTP_ACCEPT_ENCODING does not contain "gzip">
    <cfset compress = 0>
</cfif>

<!--- CUSTOM EXTRA JAVASCRIPTS TO PACK --->
<cfset custom = arrayNew(2)>

<!--- SETUP CACHE INFO --->
<cfset cacheKey = cacheKey & url.files>
<cfloop from="1" to="#arrayLen(custom)#" index="a">
    <cfset cacheKey = cacheKey & custom[a]>
</cfloop>
<cfset cacheKey = hash(cacheKey, "md5")>
<cfif diskCache eq 1>
    <cfset fileBase = cachePath & cacheKey>
    <cfset fileJS = fileBase & ".js">
    <cfif compress eq 1>
        <cfset fileGZ = fileJS & ".gz">
        <cfif not fileExists(fileGZ)>
            <cfset makeJS(file=fileJS)>
            <cfset makeGZ(fileJS=fileJS)>
        </cfif>
        <cfset serveGZ(file=fileGZ)>
    <cfelse>
        <cfif not fileExists(fileJS)>
            <cfset makeJS(file=fileJS)>
        </cfif>
        <cfset serveJS(file=fileJS)>
    </cfif>

<cfelse>
    <cfset fileBase = cachePath>
    <cfset fileJS = fileBase & "temp.js">
    <cfset makeJS(file=fileJS)>
    <cfif compress eq 1>
        <cfset fileGZ = fileBase & "temp.js.gz">
        <cfset makeGZ(fileJS=fileJS)>
        <!--- CANNOT DO MORE WORK AFTER CFCONTENT, SO DELETE THE TEMP JS NOW --->
        <cfset del = deleteFile(file=fileJS)>
        <cfset serveGZ(file=fileGZ, delete=1)>
    <cfelse>
        <cfset serveJS(file=fileJS, delete=1)>
    </cfif>
</cfif>


<cffunction name="makeJS">
    <cfargument name="file" required="true" >

    <!--- ADD PLUGINS --->
    <cfloop from="1" to="#arrayLen(files)#" index="p">

        <cfset content = content & chr(10) & chr(13) & getFileContents(files[p])>

    </cfloop>

    <!--- ADD CUSTOM FILES --->
    <cfloop from="1" to="#arrayLen(custom)#" index="c">
        <cfset content = content & getFileContents(custom[c])>
    </cfloop>

<!--- HOW BIG IS THE UNCOMPRESSED JS? --->
<cfsavecontent variable="heading"><cfoutput>
#credits#
//  This uncompressed concatenated JS: #numberformat((content.length() + credits.length())/1024, .00)# KB
//  --------------------------------------------------------------------

</cfoutput></cfsavecontent>

    <!--- WRITE THE JS FILE --->
    <cffile action="write" output="#trim(content)#" charset="iso-8859-1" file="#arguments.file#" addNewLine="no">
</cffunction>


<cffunction name="serveJS">
    <cfargument name="file" required="true" >
    <cfargument name="delete" default="0" >

    <cfcontent file="#arguments.file#" deleteFile="#arguments.delete#">
</cffunction>


<cffunction name="makeGZ">
    <cfargument name="fileJS" required="true" >

    <cfscript>

        /* Create Objects */
        ioInput     = CreateObject("java","java.io.FileInputStream");
        ioOutput    = CreateObject("java","java.io.FileOutputStream");
        gzOutput    = CreateObject("java","java.util.zip.GZIPOutputStream");

        /* Set Variables */
        this.os = Server.OS.Name;

        if(FindNoCase("Windows", this.os)) this.slash = "\";
        else                               this.slash = "/";

        /* Default variables */
        l = 0;
        buffer     = RepeatString(" ",1024).getBytes();
        gzFileName = "";
        outputFile = "";

        /* Convert to the right path format */
        arguments.gzipFilePath = PathFormat(cachePath);
        arguments.filePath     = PathFormat(arguments.fileJS);

        /* Check if the 'extractPath' string is closed */
        lastChr = Right(arguments.gzipFilePath, 1);

        /* Set an slash at the end of string */
        if(lastChr NEQ this.slash)
            arguments.gzipFilePath = arguments.gzipFilePath & this.slash;

        try
        {

            /* Set output gzip file name */
            gzFileName = getFileFromPath(arguments.filePath) & ".gz";
            outputFile = arguments.gzipFilePath & gzFileName;

            ioInput.init(arguments.filePath);
            ioOutput.init(outputFile);
            gzOutput.init(ioOutput);

            l = ioInput.read(buffer);

            while(l GT 0)
            {
                gzOutput.write(buffer, 0, l);
                l = ioInput.read(buffer);
            }

            /* Close the GZip file */
            gzOutput.close();
            ioOutput.close();
            ioInput.close();

            /* Return true */
            return true;
        }

        catch(Any expr)
        { return false; }

    </cfscript>

</cffunction>


<cffunction name="PathFormat" access="private" output="no" returntype="string" hint="Convert path into Windows or Unix format.">
    <cfargument name="path" required="yes" type="string" hint="The path to convert.">

    <cfif FindNoCase("Windows", this.os)>
        <cfset arguments.path = Replace(arguments.path, "/", "\", "ALL")>
    <cfelse>
        <cfset arguments.path = Replace(arguments.path, "\", "/", "ALL")>
    </cfif>

    <cfreturn arguments.path>
</cffunction>


<cffunction name="serveGZ">
    <cfargument name="file" required="true" >
    <cfargument name="delete" default="0" >

    <cfheader name="Content-Encoding" value="gzip">
    <cfheader name="Content-Disposition" value="inline; filename=""#cacheKey#.jgz""">
    <cfcontent file="#arguments.file#" deleteFile="#arguments.delete#">
</cffunction>


<cffunction name="deleteFile">
    <cfargument name="file" required="true" >

    <cftry>
        <cffile action="delete" file="#arguments.file#">
        <cfcatch></cfcatch>
    </cftry>
</cffunction>


<cffunction name="getFileContents">
    <cfargument name="path">

    <cfif files[p] does not contain '/compress='>
        <cfif not directoryExists(expandPath("#arguments.path#")) AND not fileExists(expandPath("#arguments.path#"))>
            <cfhttp url="http:/#files[p]#" userAgent="#cgi.http_user_agent#" />
            <cfset content = content & cfhttp.fileContent>
        <cfelse>
            <cffile action="read" file="#expandpath('#arguments.path#')#" variable="content">
        </cfif>
    <Cfelse>
        <cfset content = ''>
    </cfif>

    <cfreturn content>
</cffunction>

Upvotes: 0

Adrian J. Moreno
Adrian J. Moreno

Reputation: 14859

Ok, so first, this is not a silly request. You can load 5 JS files to load jQuery and 4 plugins or you can load all of that code in a single, compressed file. This will speed up your page loading and can reduce the amount of content coming to the browser from 25 - 70% related to JS library code.

We have a similar ColdFusion object that allows us to specify what JS file is required for a particular page to work correctly. The list of files is collected on the server and then rendered to the page either in the <head> or below the <body> (where a JS file is requested also affects page loading speed).

What they are asking you to do is collect their standard set of JS libraries (and the related CSS files) and create a build, which will produce a pair of JS and CSS files that can be loaded in one request instead of many.

This is not something you should do with ColdFusion.

By this, I mean, you won't create the compiled files on the fly in production, you'll create them ahead of time and deploy them to your server, where the application will reqeust them instead of the individual JS library files.

You need to use a build tool like Grunt or Gulp, which require Node. If you've never used Node before, it'll take you a day or two to get all of this up and running with a build. There are tons of examples online, but I'll give you an example of a build we have for a legacy app that still uses the Adobe Spry library.

var gulp = require('gulp');
var uglify = require('gulp-uglify');
var concat = require('gulp-concat');
var rename = require('gulp-rename');
var cssmin = require('gulp-cssmin');
var compileDest = 'dist/compiled';
var jsSrc = [];
var cssSrc = [];

jsSrc.push('app/spry/1.6.1/widgets/SpryAccordion.js');
jsSrc.push('app/spry/1.6.1/widgets/SpryHTMLPanel.js');
jsSrc.push('app/spry/1.6.1/SpryData.js');
jsSrc.push('app/spry/1.6.1/SpryHTMLDataSet.js');
jsSrc.push('app/spry/1.6.1/SpryJSONDataSet.js');
jsSrc.push('app/spry/1.6.1/SpryNestedXMLDataSet.js');
jsSrc.push('app/spry/1.6.1/xpath.js');
jsSrc.push('app/spry/1.6.1/SpryPagedView.js');
cssSrc.push('app/spry/1.6.1/widgets/SpryAccordion.css');
cssSrc.push('app/spry/1.6.1/widgets/SpryHTMLPanel.css');

gulp.task('compileScripts',function(){
    //JS
    gulp.src(jsSrc)         
        .pipe(concat('equator-spry.1.6.1.js'))
        .pipe(uglify())
        .pipe(rename('equator-spry.1.6.1.min.js'))
        .pipe(gulp.dest(compileDest));
    //CSS
    gulp.src(cssSrc)
        .pipe(concat('equator-spry.1.6.1.css'))
        .pipe(cssmin())
        .pipe(rename('equator-spry.1.6.1.min.css'))
        .pipe(gulp.dest(compileDest));
});
  • Instead of 8 JS files, we load 1: equator-spry.1.6.1.min.js
  • Instead of 2 CSS files, we load 1: equator-spry.1.6.1.min.css

Your CF code would then just reference

<cfset Application.globalObj.addJsFile(jsfile="/js/equator-spry.1.6.1.js",location="footer",priority=1) />

You would also need to update addJsFile() to ignore requests to the individual JS files to avoid refactoring existing code and to avoid a potential conflict in library versions when you eventually update your build as the underlying libraries get updated.

You would lose your ability to load from a public CDN, but you would gain a reduction in page loading speed by users retrieving the new compiled files for your site from browser cache.

Edit

This is a ten+ year old project and has over 100 pages and a custom cms system. Previous developers have tried and failed miserably. That was why I was trying to use a simpler solution and not bring a new tech into this stack that is already a complete mish mash.

You're introducing no new tech to the ColdFusion application itself.

What you are going to do is introduce a new process for producing JS and CSS files that will be used by the CF application.

In my case, there is a global layout file. I just added a line to load the new compiled JS and CSS files onto that layout.

<cfset rc.oResourceService.addHeadContent(type='js', src='/resource/compiled/spry/1.6.1/equator-spry.1.6.1.min.js')  />
<cfset rc.oResourceService.addHeadContent(type='css', src='/resource/compiled/spry/1.6.1/equator-spry.1.6.1.min.css')  />

Now they're available to every screen.

Then I would find the object that defines Application.globalObj, locate the function addJsFile() and update it to ignore a list of the existing individual JS file names.

<!--- ResourceService, function addHeadContent() --->
var ignoreList = [
    'jquery\.js'
    , 'SpryAccordion\.css'
    , 'SpryHTMLPanel\.css'
    , 'SpryAccordion\.js'
    , 'SpryHTMLPanel\.js'
    , 'SpryData\.js'
    , 'SpryHTMLDataSet\.js'
    , 'SpryJSONDataSet\.js'
    , 'SpryNestedXMLDataSet\.js'
    , 'xpath\.js'
    , 'SpryPagedView\.js'
];

Now, anytime a file in that list is requested, it gets ignored and no <script> tags gets rendered for it.

<cfset rc.oResourceService.addHeadContent(type='js', src='/includes/spry/includes/SpryData.js') />
<cfset rc.oResourceService.addHeadContent(type='js', src='/includes/spry/includes/xpath.js') />
<cfset rc.oResourceService.addHeadContent(type='js', src='/includes/spry/includes/SpryPagedView.js') />
<cfset rc.oResourceService.addHeadContent(type='js', src='/includes/spry/includes/SpryJSONDataSet.js') />
<cfset rc.oResourceService.addHeadContent(type='js', src='/includes/spry/widgets/htmlpanel/SpryHTMLPanel.js') />

This means that thousands (yes, THOUSANDS in my case) of CFM files do not have to be updated to remove the individual references to those now deleted files.

You can absolutely do this!

You're already ahead of the game since you're used those build tools before. You know what you need to produce and what you're trying to accomplish. You just need to figure out what small changes need to be made in your existing CF code to make it happen. I've outlined what I had to do and it seems very similar to what you're dealing with.

Don't take an improper approach that "just happens to work" and make it the new thing. Make the current tech thing the new thing.

Now, when you need to implenent a NodeJS API on top of existing CF logic or have to integrate Angular JS apps on top of your legacy CF code, gimme a holler and we can talk about what we're doing with that too. :)

Upvotes: 5

Related Questions