Making angular project production-ready with gulp

Cover image

Recently, I had a chance to make some adjustments to the file structure for the project I worked on in the company. There wasn’t any source folder for developer and production folder just for deployment. Everything was in one folder which was not ideal for production. There wasn’t a deployment process except “git push”. Therefore, I had a chance to fix it by introducing gulp into the game. Of course there would be a transition period within the team but overall it was worth the time.

Minifying JavaScript

In your angularjs application, you probably have a lot of codes that look like:

angular.modoule("sample").controller("Main", function($timeout){  
  // Your code  
});

This works fine when you are doing development. However, we would like to uglify these JavaScript files to save as much space as possible. Uglifying means it will not only taking out all spaces but also run an algorithm to replace those variables into, for example, a letter instead of really long names. You can save space as well as protect your source code. The problem is that “$timeout” is just a parameter so during uglifying, it would actually get replaced and angularjs will not recognize what that is anymore. gulp-ng-annotate will apply the special structure introduced by angularjs to prevent this from happening.

var plumber = require("gulp-plumber");  
var ngAnnotate = require("gulp-ng-annotate");  
var uglify = require("gulp-uglify");

gulp.task("angular-app", function() {  
  return gulp.src(\['scr/scripts/\*\*/\*.js'\])  
    .pipe(plumber())  
    .pipe(ngAnnotate({  
      add: true  
    }))  
    .pipe(uglify({  
      beautify: true,  
      mangle: true  
    }))  
    .pipe(plumber.stop())  
    .pipe(gulp.dest('dist/scripts'));  
});

gulp-plumber is to prevent pipe from breaking when there is error. After it annotates but before it uglifies, your angular code would become something like:

angular.modoule("sample").controller("Main", \['$timeout', function($timeout){  
  // Your code  
}\]);

So now no matter how arguments and variables get replaced, the string ‘$timeout’ will always stay and angular would know which dependency it needs to grab. Note that putting ng-strict-di is strongly suggested when using this annotation technique as angular would throw you error when dependency injection is not being implemented properly.

Cache Busting

Browser usually cached files that it loads so if you deploy with the same names for those scripts files, they will most likely get cached. Caching is good but it becomes a problem when you need to deploy new version of your site. the gulp plugin I use it called gulp-cachebust. It will either generate a new file name depending on the checksum of the file or generate a random SHA1 hash on all files.

var CacheBuster = require("gulp-cachebust");  
var cachebust = new CacheBuster({  
  random: true  // generate SHA1 hash, default is false  
});

gulp.task("scripts-cache-resource", function() {  
  return gulp.src(\["src/scripts/\*\*/\*.js"\])  
    .pipe(cachebust.resources())  
    .pipe(gulp.dest('dist/scripts'));  
});

gulp.task("build-html", function() {  
  return gulp.src(\["./index.html"\])  
    .pipe(cachebust.references())  
    .pipe(gulp.dest("dist"));  
});

This will generate new names for the files when calling resource() method then replaces the old reference with new reference names. Now when you deploy your new version of site, since the name of the script files are new now, browser would think they are different files so it will load those new files again. This cache busting also works on CSS files.

Optional: HTML cache busting

My project leader would like to put everything on the CDN so I would need to use the same cache bust technique on the template views. This was when I needed to use SHA1 hash instead of checksum during cache busting. I didn’t want to go through all views and determine when I should change the names so checksum is correct. This is what I had in the gulpfile.js and I believe that can be improved, but for now it does the job.

var CacheBuster = require("gulp-cachebust");  
var cachebust = new CacheBuster({  
  random: true  
});  
var fileTrack = require("gulp-track-filenames")();  
var gulpReplace = require("gulp-replace-task");

var fileObject = {};

gulp.task("cache-html", function() {  
  return gulp.src("src/views/\*\*/\*.html")  
    .pipe(cachebust.resources())  
    .pipe(gulp.dest("dist/views"));  
});

gulp.task("get-file-name", function() {  
  var session = fileTrack.create();  
  return gulp.src(\["src/views/\*\*/\*.html", "dist/views"\])  
    .pipe(session.before())  
    // .pipe(gulp.dest("dist/views"))  
    .pipe(session.after())  
    .on("data", function(file) {  
      var filepath = file.path.split("/")\[file.path.split("/").length - 1\];  
      var firstpath = filepath.split(".")\[0\];  
      if (!fileObject.hasOwnProperty(firstpath)) {  
        fileObject\[firstpath\] = {};  
      }

  // if it is the original file  
      if (filepath.split(".")\[1\] == "html") {  
        fileObject\[firstpath\]\["match"\] = new RegExp(filepath, 'g');  
      } else {  
        fileObject\[firstpath\]\["replacement"\] = filepath;  
      }  
    });  
});

gulp.task("replace-html", function() {  
  var fileArray = \[\];  
  for (var prop in fileObject) {  
    fileArray.push(fileObject\[prop\]);  
  }

  return gulp.src("dist/views/\*\*/\*.html")  
    .pipe(gulpReplace({  
      patterns: fileArray  
    }))  
    .pipe(gulp.dest("dist/views"));  
});

I first did a cache bust on the template files first, then I created an object to hold the original name of the file and the new name. Due to the naming convention we have, if the second word was not ‘html’ after string splitting, then this name was that one that had hash. It is better if you use RegExp to find the files you need to replace and everybody’s situation is different so find one that suits you. Finally, I changed that object into array so it matched the structure the gulp-replace-task required to replace texts. There were other formats the plugin would accept so you could choose one that met your needs.

I still have lots to learn but would like to share what I have learned so it clears the path for some people that are like me.