As with CSS, our goal should be to serve only one JavaScript file to the client's browser. We do not want to force the user to make more than one request, because this is less efficient and it means that the web browser takes longer to process and display the content of the page. Nowadays, the client-side part of applications is fairly complex. As with complex systems, we split our logic into different modules. Often, different modules mean different files. Thankfully, Node.js is full of tools that can be used to pack JavaScript. Let's see two of the most popular tools.
Gulp, as a build system, has several modules to concatenate files. The one that we are interested in is called gulp-concat
. Let's add it to the package.json
file:
"dependencies": { "gulp": "3.8.8", "gulp-less": "1.3.6", "gulp-rename": "1.2.0", "gulp-minify-css": "0.3.11", "gulp-concat": "2.4.1" }
The next step is to write a task that uses it. Again, we will use the src
and dest
Gulp methods, and in between is the concatenation:
var concat = require('gulp-concat'); gulp.task('js', function() { gulp.src('./js/*.js') .pipe(concat('scripts.js')) .pipe(gulp.dest('./static/js')) });
It's important to mention that the files will be added to the final file in alphabetical order. So, we should be careful whenever there are some code dependencies. If this is the case, we should name the files in such a way that their names start with a unique number—01, 02, 03, and so on.
The next logical task that we will do is to minify our JavaScript. Like the Less compilation, we want to serve a file that is as small as possible. The module that will help us achieve this is gulp-uglify
. Again, we should add it to the package.json
file ("gulp-uglify": "1.0.1"
). After this, a little tweak to our newly created task will minify the JavaScript:
var concat = require('gulp-concat'); var uglify = require('gulp-uglify'); gulp.task('js', function() { gulp.src('./js/*.js') .pipe(concat('scripts.js')) .pipe(gulp.dest('./static/js')) .pipe(uglify()) .pipe(rename({suffix: '.min'})) .pipe(gulp.dest('./static/js')) });
Note that we used the gulp-rename
plugin again. This is necessary because we want to produce a different file.
While building software, one of the most important concepts to think about is the splitting of our system into modules. Node.js has a nice built-in system to write modules. We mentioned this in Chapter 1, Node.js Fundamentals. We encapsulate our code in a single file and use module.exports
or exports
to create the public API. Later, via the require
function, we access the created functionalities.
However, for the client-side JavaScript, we do not have such a built-in system. We need to use an additional library that allows us to define modules. There are several possible solutions. The first one that we will take a look at is RequireJS (http://requirejs.org/). We will download the library (version 2.1.16) from the official site and include it in our page like this:
<script data-main="scripts/main" src='#'> </script>
The key attribute here is data-main
. It tells RequireJS about our application's entry point. In fact, we should have the scripts/main.js
file in our project's folder to get the preceding line working. In main.js
, we can use the require
global function:
// scripts/main.js require(["modules/ajax", "modules/router"], function(ajax, router) { // ... our logic });
Let's say that our code in main.js
depends on two other modules—the Ajax wrapper and router. We describe these dependencies in an array and provide a callback, which is later executed with two parameters. These parameters are actually references to the necessary modules.
The defining of modules is possible with the help of another global function—define
. Here is how the Ajax wrapper looks:
// modules/ajax.js define(function () { // the Ajax request implementation ... // public API return { request: function() { ... } } });
By default, behind the scenes, RequireJS resolves the dependencies asynchronously. In other words, it performs an HTTP request for every required module. In some cases, this may lead to performance issues because every request takes time. Thankfully, RequireJS has a tool (optimizer) that solves the problem. It can bundle all the modules into a single file. The tool is available for Node.js too and it is distributed with the requirejs
package:
npm install -g requirejs
After a successful installation, we will have the r.js
command in our terminal. The basic call looks like this:
// in code_requirejs folder r.js -o build.js
As with Grunt and Gulp, we have a file that instructs RequireJS on how to work. The following is a snippet that covers our example:
// build.js ({ baseUrl: ".", paths: {}, name: "main", out: "main-built.js" })
The name
property is the entry point and out
is the resulting file. It's nice that we have the paths
property available. It is a place where we can describe the modules directly; for example, jquery: "some/other/jquery"
. Later in our code, we do not have to write the full path to the files. Just a simple require(['jquery'], ...)
is enough.
By default, the output of the r.js
command is minified. If we add an optimize=none
argument to the command in the terminal, we will get the following:
// main-built.js define('modules/ajax',[],function () { ... }); define('modules/router',[],function () { ... }); require(['modules/ajax', 'modules/router'], function(ajax, router) { ... }); define("main", function(){});
The main-built.js
file contains the main module and its dependencies.
RequireJS indeed solves the problem with modularity. However, it makes us write more code. Also, we should always describe our dependencies by following a strict format. Let's look at the code that we used in the previous section:
require(['modules/ajax', 'modules/router'], function(ajax, router) { ... });
It is indeed better if we use the following code:
var ajax = require('modules/ajax'); var router = require('modules/router');
The code is much simpler now. This is how we should fetch a module in the Node.js environment. It would be nice if we could use the same approach in the browser.
Browserify (http://browserify.org/) is a module that brings the require
module of Node.js to the browser. Let's install it first by using the following code:
npm install -g browserify
Similarly, to illustrate how the tool works, we will create the main.js
, ajax.js
and router.js
files. This time, we are not going to use a global function such as define
. Instead, we will use the usual Node.js module.exports
:
// main.js var ajax = require('./modules/ajax'); var router = require('./modules/router'); // modules/ajax.js module.exports = function() {}; // modules/router.js module.exports = function() {};
By default, Browserify comes as a command-line tool. We need to provide an entry point and an output file:
browserify ./main.js -o main-built.js
The result in the compiled file is as follows:
// main-built.js (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ var ajax = require('./modules/ajax'); var router = require('./modules/router'); },{"./modules/ajax":2,"./modules/router":3}],2:[function(require,module,exports){ module.exports = function() {}; },{}],3:[function(require,module,exports){ module.exports=require(2) },{".../modules/ajax.js":2}]},{},[1]);
Note that along with the modules, the compiled file also contains the require
function's definition and implementation. It's really just a few bytes of code that makes Browserify one of the most popular ways to deliver modular JavaScript in the browser. This is what we are going to use in the next few chapters.
We have started a Gulp setup. Let's add Browserify there. We have already made a concatenation of the JavaScript. Let's replace it with Browserify. We will add the module to the package.json
file as follows:
"dependencies": { "gulp": "3.8.8", "gulp-less": "1.3.6", "gulp-rename": "1.2.0", "gulp-minify-css": "0.3.11", "gulp-concat": "2.4.1", "gulp-uglify": "1.0.1", "gulp-browserify": "0.5.0" }
After running npm install
, we will get the plugin installed and ready to use. We need to make two changes, replacing concat
with browserify
and pointing out the application's main file:
var browserify = require('gulp-browserify'); var uglify = require('gulp-uglify'); gulp.task('js', function() { gulp.src('./js/app.js') .pipe(browserify()) .pipe(gulp.dest('./static/js')) .pipe(uglify()) .pipe(rename({suffix: '.min'})) .pipe(gulp.dest('./static/js')) });
Now, the src
method accepts only one file. It's our entry point. This is the place where Browserify starts resolving dependencies. The rest is the same. We still use uglify
for minification and rename
to change the file's name.