Introduction
I am writing this article with interest to give an overview for setting up build process for front end applications whenever any code changes happen. I will not go into details of each technology used for demo, e.g., Angular, but my aim with this article is to give a simple example on what kind of tasks we can setup at the time of building code and how to do that.
Build Process
Over many years, applications development processes have been improved a lot. Nowadays, building an application includes many tasks that every developer follows other than just compilation, before the code gets ready for deployment in a different environment.
- Writing code (at least)
- Writing unit test cases to cover code (either code first or unit test first, i.e., TDD approach)
- Whenever code changes happen, execute all test cases to prevent breaking of existing functionality
- Check for quality of code (coding styles / code analysis tools)
- Generate executables (built with release mode) to be deployed in various environments
If you are being at server side developer, you may have seen enforcement of such similar practices with support of some tools (e.g. MS-Build).
Front end development is different from server side programming in various ways. Firstly, it's typically a mixture of artifacts, i.e., rendered by browsers (like Java scripts, CSS and HTML code) which doesn't even use compilers, hence it is difficult to find errors. Secondly, it has a different set of tools to create / execute test cases. Lastly, an important factor is minification of scripts, CSS which improves UI performance. It is very essential to have some build process in place to enforce every developer to validate coding styles, test cases execution / coverage before the code goes to source control. And once all checks are done, it should also prepare output files after minifications.
I will be using one of the popular tools Gulp to enable such automated process in our example, which will help to perform the below tasks whenever any code changes will happen.
- "Step 1 - Check Coding Styles" - Validate any errors in JavaScript code, if any error, build process will fail
- "Step 2 - Execute Test Cases" - Execute all test cases in application to ensure changes hasn't impacted any existing functionality, if any test case will break, build process will fail
- "Step 3 - Minify Files" - Prepare minified version of all required JavaScript files for Output folder
- "Step 4 - Copy Output files" - Copy all HTML file(s), JavaScript files into Output folder
- "Step 5 - Copy Dependencies" - Copy minified versions of dependency file(s) into Output folder
- "Step 6 - Replace References" - Replace references in HTML page to use minified JavaScript files
Once all the above steps will be completed successfully, Output folder will be ready to get deployed in any environment.
Using the Code
In our example, we will create one HTML page to display the total number of books in an library. While creating this demo, I used the below tools, but it's not a strict choice.
- Editor - Visual Studio 2015
- JavaScript framework - Angular JS
- Testing - Jasmine / Karma
- Build Process - Gulp JS.
First of all, we need to install some packages Angular, Karma, Jasmine, Gulp and its plug in(s). I have created a batch file that you can download from links given at the top of this article.
Make sure you have Node JS installed on your machine.
call npm init --save-dev
call npm install gulp --save-dev
call npm install gulp-jshint --save-dev
call npm install gulp-jscs --save-dev
call npm install gulp-uglify --save-dev
call npm install gulp-replace --save-dev
call npm install gulp-clean-dest --save-dev
call npm install gulp-rename --save-dev
call npm install jasmine --save-dev
call npm jasmine init --save-dev
call npm install angular --save-dev
call npm install angular-mocks --save-dev
call npm angular init --save-dev
call npm install karma --save-dev
call npm install karma-jasmine --save-dev
call npm install karma-chrome-launcher --save-dev
call npm install karma-Phantomjs-launcher --save-dev
call npm karma init --save-dev
We can install all these plug-in(s) by running commands independently. But keeping such pre-requisites in batch file makes maintenance easier, especially when there is change in pre-requisite or some new developer in team is setting up his / her local environment for the first time.
Once batch file is completed, you can find a new folder "node_modules" in your solution. We will copy the below files from "node_modules" and place into root folder.
- package.json - file contains all plug-ins
- karma.conf.js - file conaints settings for karma (test runner). You will need to update settings about JavaScript files, browser, etc. you want to use
- .jscsrc - ruleset for Java Script styles. You may update some rules as per your needs
After installing all pre-requisites, we will add a JavaScript file "BooksList.js" into your project under scripts folder. This script will contain a function to return "Total number of books". I have created Angular controller to introduce function "GetTotalNumberOfBooks
" which returns hard-coded value 10
. If you are new to Angular, modules in Angular works as a container which contains other parts like controllers, services, etc. Angular controller is a JavaScript class that runs business logic for view, and $scope
is an Angular object that is used to share data between controller and view. Tons of Angular tutorials are available on the internet for more details.
var booksModule = angular.module("booksModule",[]);
booksModule.controller("booksCtrl",["$scope", function ($scope) {
$scope.GetTotalNumberOfBooks = function () {
return "10";
};
}]);
});
Now our function is ready, let's add another JavaScript file to introduce a test case which will test result of function. We will be using JasmineJS to write test case. Angular-mock-js will provide you objects for angular to be used in test case. In Jasmine, we start test cases with "describe
" block which can contain multiple test cases defined as "it
" block. BeforeEach
works as TestSetup
where we can write some code that will be executed by all test cases. Currently our BooksList.js has only one test case in our example, but as we are injecting angular objects in BeforeEach
, it will work for any new tests within the same describe block.
var scope, ctrl;
describe("BookTests", function () {
beforeEach(angular.mock.module("booksModule"));
describe("BooksCount", function () {
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller("booksCtrl", { $scope: scope });
}));
it("GetTotalNumberOfBooks", function () {
expect(scope.GetTotalNumberOfBooks()).toBe(10);
})
})
})
Let's add App.HTML file for user to see the result of total number of books on browser. Angular directives are used to access angular objects in UI. We are using two directives - "ng-app
" is used to load our module "booksModule
", and then "ng-controller
" to access controller. Also, please note we are using two references "angular.js" and "BooksList.js". Later in this article, we will create some automated tasks in our build process to change our angular dependency with "angular.min.js" and minify "BooksList.js" to "BooksList.min.js" and replace the references in HTML automatically.
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="../node_modules/angular/angular.js"></script>
<script src="/scripts/BooksList.js"></script>
<meta charset="utf-8" />
</head>
<body>
<div ng-app="booksModule" ng-controller="booksCtrl">
<p>Total number of books are </p> {{GetTotalNumberOfBooks()}}
</div>
</body>
</html>
Now we have our UI and JavaScript functions ready, we will start creating our build process. To do this, we need to create another JavaScript file "Gulpfile.js". In this file, we will enable all steps in our build process by creating Gulp tasks. But before creating tasks, we need to import plug-ins using "require
" method. We can import all plugins using "require(gulp-load-plugins)", but for this example, we are not going for this, so we can discuss every plug-in we will use.
var gulp = require("gulp");
var jscs = require("gulp-jscs");
var notify = require("gulp-notify")
var uglify = require("gulp-uglify");
var clean = require("gulp-clean-dest");
var util = require("gulp-util");
var replace = require("gulp-replace");
var Server = require('karma').Server;
Now we will create task for "Step 1 - Check Coding Styles", to do this, we will use "gulp
" and "gulp-jscs
". gulp
object will be used to create task to invoke function to perform style checks on JavaScript file. gulp.src function is used to provide names of source files, this function takes a single value or array in input parameter (in case there are multiple source files).
Pipe function in Gulp is used to chain the execution of plugins we want to use in specific task. Here in our "Validating Styles" task, we are invoking two plug-ins in pipe. JSCS will perform all style check based on rules defined .jscsrc file (that we copied into root folder in previous steps).
gulp.task("ValidatingStyles", function () {
return gulp.src("scripts/BooksList.js")
.pipe(jscs())
.pipe(jscs.reporter())
})
});
Reporter function is used to provide details on all style errors on screen if it fails to match with rules. It will display errors as shown in the below screenshot.
Adding reporter('fail')
will result in fail
our build process, in case there are some errors found.
gulp.task("ValidatingStyles", function () {
return gulp.src("scripts/BooksList.js")
.pipe(jscs())
.pipe(jscs.reporter())
.pipe(jshint.reporter('fail'))
})
});
After successful validation of Coding styles in JavaScript function, we will create another gulp task to implement our "Step 2 - Execute Test Cases". Here, we will execute all test cases with Karma. Karma is a test runner for JavaScript test cases. We will use PhantomJS to launch Karma. PhantomJS is a browser without any user interface but it starts the environment for test cases. We will pass configuration file "karma.conf.js" for this task that we copied to root folder in previous steps. Once gulp task will run, it will first start karma server by launching PhantomJS and will execute all test cases in files defined in settings of "karma.conf.js".
gulp.task("RunTests", function (done) {
new Server({
configFile: __dirname + '\\karma.conf.js',
singleRun: true
}, done).start();
});
This is how the outcome of RunTests
will look like:
Once we ensure code quality after style validations and execution of test cases, we will go for creating next step "Step 3 - Minify Files". minify our JavaScript file. We will create task "Uglfying" which will take "BooksList.js" as source file. Gulp-Uglify plug in will be invoked to minify file.
Also, as we want this minify file to be used for environments, we will place into our destination folder "Output". Dest function is used to specify destination location. Gulp-rename plug in will be used to rename "BooksList.js" to "BooksList.min.js" when it will be placed in output folder.
This will acutally enable our next step "Step 4 - Copy Output files" partially by copying JavaScript file into Output folder.
gulp.task("Uglfying", function () {
return gulp.src(["scripts\/BooksList.js"])
.pipe(clean("./output"))
.pipe(uglify())
.pipe(rename({suffix: ".min"}))
.pipe(gulp.dest("./output"))
})
"Step 5 - Copy Dependencies" - now we will ensure that all required dependencies are placed in output folder as well. In our case, angular.min.js is required for HTML file. So we will copy angular.min.js to output folder. But as this is dependency only, we don't want this file to be copied every time when we build our application. We will use "fs.existsSync
" function that will be used to check if file exists in output folder or not.
gulp.task("CheckDependencies", function () {
var sourceFile = "node_modules/angular/angular.min.js";
var destFile = "output/angular.min.js";
if (!(fs.existsSync(destFile))) {
return gulp.src(sourceFile)
.pipe(gulp.dest("./output"))
}
});
"Step 6 - Replace References" is the final step of process. Now we have JavaScript minified files in output folder. We will create another task now that will copy "App.html", also it will replace JavaScript references to use minified files. Gulp-replace
plug in is used here to replace the file names.
gulp.task("ReplaceTask", function () {
return gulp.src("App.html")
.pipe(replace("scripts\/BooksList.js", "BooksList.min.js"))
.pipe(replace("node_modules\/angular\/angular.js", "angular.min.js"))
.pipe(gulp.dest("./output"));
})
So, our build process is ready, now we will create another Gulp task to sequence our tasks.
"Build All" task will be dependent on "ValidateCode
", "RunTests
", "MinifyFiles
", "CopyFiles
", "RenameReferences
" that means, when we will execute "BuildAll
", it will first check all coding styles, then it will execute test After successful execution of test case, it will minify JavaScript file and copy to output folder along with Angular JS and HTML file. At the end, it will finally replace all JavaScript references in HTML page to minified versions. It's easy.
gulp.task("BuildAll", ["ValidatingStyles", "RunTests", "Uglfying",
"CheckDependencies", "ReplaceTask"], function () {
console.log("Build completed succesfully");
})
Most of the times, the above task should be enough for enabling our expected build tasks, but Gulp will activate all these tasks in parallel which may not execute the correct sequence as we want.
To run all tasks in sync, we can use "gulp-sync
" plug in:
gulp.task("BuildAll", gulpsync.sync
(["ValidatingStyles", "RunTests", "Uglfying", "CheckDependencies", "ReplaceTask"]), function () {
console.log("Build completed succesfully");
})
Now open command prompt and execute test case using Gulp BuildAll and it will perform all tasks in sequence to prepare Output folder.
Once BuildAll
task is completed successfully, you may find Output folder created under root folder of your project. Try browse App.html and see view source. You will find all reference files are mapped to minified files under output folder.
This is how we can manage quality of code changes, I have placed all scripts files used in this demo under download link. You can download these files under any empty web application and run "build.bat" file to install all plug-ins.
Points of Interest
In this article, we explored few of Gulp plugins to setup some tasks that can ensure quality of build process. There are a number of useful plug-in(s) that can help us to do a lot more, for example, reporting code coverage, logging of failures, etc. In later articles, we will go deeper into more best practices.