Now that we have a solid understanding of isomorphic JavaScript, it’s time to go from theory to practice! In this chapter we lay the foundation that we’ll progressively build upon until we have a fully functioning isomorphic application. This foundation will be comprised of the following main technologies:
Node.js will be the server runtime for our application. It is a platform built on Chrome’s JavaScript runtime for easily building fast, scalable network applications. Node.js uses an event-driven, nonblocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.
Hapi.js will be used to power the HTTP application server portion of our application. It is a rich framework for building applications and services. Hapi.js enables developers to focus on writing reusable application logic rather than spending time building infrastructure.
Gulp.js will be used to compile our JavaScript (ES6 to ES5), create bundles for the browser, and manage our development workflow. It is a streaming build system based on Node streams: file manipulation is all done in memory, and it doesn’t write any files until you tell it to do so.
Babel.js will allow us to begin leveraging ES6 syntax and features now by compiling our code to an ES5-compatible distributable. It is the compiler for writing next-generation JavaScript.
If you are already comfortable with using Node, npm, and Gulp then feel free to skip this chapter and install the project that is the end result by running npm install thaumoctopus-mimicus@"0.1.x".
Installing Node is very easy. Node will run on Linux, Mac OS, Windows, and Unix. You can install it from source via the terminal, using a package manager (e.g., yum, homebrew, apt-get, etc.), or by using one of the installers for Mac OS or Windows.
This section outlines how to install Node from source. I highly recommend using one of the installers or a package manager, but if you are one of those rare birds who enjoy installing software from source, then this is the section for you. Otherwise, jump ahead to “Interacting with the Node REPL”.
The first step is to download the source from Nodejs.org:
$wgethttp://nodejs.org/dist/v0.12.15/node-v0.12.15.tar.gz
node-v0.12.15.tar.gz was the latest stable version at the time of writing. Check https://nodejs.org/download/release/ for a more recent version and replace v0.12.15 in the URL of the wget example with the latest stable version.
Next, you’ll need to extract the source in the downloaded file:
$tarzxvfnode-v0.12.15.tar.gz
This command unzips and extracts the Node source. For more information on the tar command options, execute man tar in your terminal.
Now that you have the source code on your computer, you need to run the configuration script in the source code directory. This script finds the required Node dependencies on your system and informs you if any are missing on your computer:
$cdnode-v0.12.15$./configure
Once all dependencies have been found, the source code can then be compiled to a binary. This is done using the make command:
$make
The last step is to run the make install command. This installs Node globally and requires sudo privileges:
$sudomakeinstall
If everything went smoothly, you should see output like the following when you check the Node.js version:
$node-vv0.12.15
Node is a runtime environment with a REPL (read–eval–print loop), which is a JavaScript shell that allows one to write JavaScript code and have it evaluated upon pressing Enter. It is like having the console from the Chrome developer tools in your terminal. You can try it out by entering a few basic commands:
$node>(newDate()).getTime();1436887590047>(^Cagaintoquit)>$
This is useful for testing out code snippets and debugging.
npm is the package manager for Node. It allows developers to easily reuse their code across projects and share their code with other developers. npm comes packaged with Node, so when you installed Node you also installed npm:
$npm-v2.7.0
There are numerous tutorials and extensive documentation on the Web for npm, which is beyond the scope of this book. In the examples in the book we will primarily be working with package.json files, which contain metadata for the project, and the init and install commands. We’ll use our application project as working example for learning how to leverage npm.
Aside from source control, having a way to package, share, and deploy your code is one of the most important aspects of managing a software project. In this section, we will be using npm to set up our project.
The npm CLI (command-line interface) is terminal program that allows you to quickly execute commands that help you manage your project/package. One of these commands is init.
If you are already familiar with npm init or if you prefer to just take a look at the source code, then feel free to skip ahead to “Installing the Application Server”.
init is an interactive command that will ask you a series of questions and create a package.json file for your project. This file contains the metadata for your project (package name, version, dependencies, etc.). This metadata is used to publish your package to the npm repository and to install your package from the repository. Let’s take a walk through the command:
$npminitThisutilitywillwalkyouthroughcreatingapackage.jsonfile.Itonlycoversthemostcommonitems,andtriestoguesssanedefaults.See`npmhelpjson`fordefinitivedocumentationonthesefieldsandexactlywhattheydo.Use`npminstall<pkg>--save`afterwardstoinstallapackageandsaveitasadependencyinthepackage.jsonfile.Press^Catanytimetoquit.
The computer name, directory path, and username (fantastic-planet:thaumoctopus-mimicus jstrimpel $) in the terminal code examples have been omitted for brevity. All terminal commands from this point forward are executed from the project directory.
The first prompt you’ll see is for the package name. This will default to the current directory name, which in our case is thaumoctopus-mimicus.
name:(thaumoctopus-mimicus)
“thaumoctopus-mimicus” is the name of the project that we will be building throughout Part II. Each chapter will be pinned to a minor version. For example, this chapter will be 0.1.x.
Press enter to continue. The next prompt is for the version:
version: (0.0.0)
0.0.0 will do fine for now since we are just getting started and there will not be anything of significance to publish for some time. The next prompt is for a description of your project:
description:
Enter “Isomorphic JavaScript application example”. Next, you’ll be asked for for the entry point of your project:
entrypoint:(index.js)
The entry point is the file that is loaded when a user includes your package in his source code. index.js will suffice for now. The next prompt is the for the test command. Leave it blank (we will not be covering testing because it is outside the scope of this book):
test command:
Next, you’ll be prompted for the project’s GitHub repository. The default provided here is https://github.com/isomorphic-javascript-book/thaumoctopus-mimicus.git, which is this project’s repository. Yours will likely be blank.
git repository:(https://github.com/isomorphic-javascript-book/thaumoctopus-mimicus.git)
The next prompt is for keywords for the project:
keywords:
Enter “isomorphic javascript”. Then you’ll be asked for the author’s name:
author:
Enter your name here. The final prompt is for the license. The default value will be (ISC) MIT or (ISC), depending on the NPM version, which is what we want:
license:(ISC)MIT
If you navigate to the project directory you will now see a package.json file with the contents shown in Example 5-1.
{"name":"thaumoctopus-mimicus","version":"0.0.0","description":"Isomorphic JavaScript application example","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" && exit 1"},"repository":{"type":"git","url":"https://github.com/isomorphic-javascript-book/thaumoctopus-mimicus.git"},"keywords":["isomorphic","javascript"],"author":"Jason Strimpel","license":"MIT","bugs":{"url":"https://github.com/isomorphic-javascript-book/thaumoctopus-mimicus/issues"},"homepage":"https://github.com/isomorphic-javascript-book/thaumoctopus-mimicus"}
In the previous section, we initialized our project and created a package.json file that contains the metadata for our project. While this is a necessary process, it does not provide any functionality for our project, nor is it very exciting—so let’s get on with the show and have some fun!
All web applications, including isomorphic JavaScript applications, require an application server of sorts. Whether it is simply serving static files or assembling HTML document responses based on service requests and business logic, it is a necessary part and a good place to begin our coding journey together. For our application server we will be using hapi. Installing hapi is a very easy process:
$npminstallhapi--save
This command not only installs hapi, but also adds a dependency entry to the project’s package.json file. This is so that when someone (including you) installs your project, all the dependencies required to run the project are installed as well.
Now that we have hapi installed, we can write our first application server. The goal of this first example is to respond with “hello world”. In your index.js file, enter the code shown in Example 5-2.
varHapi=require('hapi');// Create a server with a host and portvarserver=newHapi.Server();server.connection({host:'localhost',port:8000});// Add the routeserver.route({method:'GET',path:'/hello',handler:function(request,reply){reply('hello world');}});// Start the serverserver.start();
To start the application execute node . in your terminal, and open your browser to http://localhost:8000/hello. If you see “hello world”, congratulations! If not, review the previous steps and see if there is anything that you or I might have missed. If all else fails you can try copying this gist into your index.js file.
ECMAScript 6, or ES6, is the latest version of JavaScript, which adds quite a few new features to the language. The specification was approved and published on June 17, 2015. People have mixed opinions about some features, such as the introduction of classes, but overall ES6 has been well received and at the time of writing is being widely adopted by many companies.
ES6 classes are just syntactic sugar on top of prototypal inheritance, like many other nonnative implementations. They were likely added to make the language more appealing to a larger audience by providing a common frame of reference to those coming from classical inheritance languages. We will be utilizing classes throughout the book.
The general consensus in the industry is to begin leveraging the new features afforded by ES6, which are not yet widely supported, now and to compile down to an ECMAScript version that is widely supported (i.e., ES5). Part of the compilation process is to provide polyfills for the ES6 features missing in the target version. Given this industry trend and readily available compilation support, we will be leveraging ES6 for the rest of the examples in this book. Let’s start with updating the code from the last example.
The first thing we want to change is how we are importing our dependency, hapi, in index.js. Replace the first line in the index.js file with this line:
importHapifrom'hapi';
ES6 introduces the concept of a module system to JavaScript, which is a concept that has been missing since its inception. In the absence of a native module interface other patterns have emerged to fill the gap, such as AMD and CommonJS. The require syntax from the original index.js (Example 5-2) is the CommonJS format.
The reason there is not a native module interface is that when JavaScript was created it was not intended to power applications like it does today. It was intended to be additive and enrich the client experience.
The next change we need to make is to our server variable declaration. Replace the second line in the file with:
constserver=newHapi.Server();
const is one of the new variable declarations available in ES6. When a variable is declared using const, the reference cannot be changed. For example, if you create an object using const and add properties to the object, that will not throw an error because the reference has not changed. However, if you attempted to point the variable to another object, that would result in an error because the reference would change. We use const because we don’t want to accidentally point our server variable to another reference.
const is not a value that does not change. It is a constant reference to a value in memory.
Example 5-3 shows what the index.js file looks like with these changes in place.
importHapifrom'hapi';// Create a server with a host and portconstserver=newHapi.Server();server.connection({host:'localhost',port:8000});// Add the routeserver.route({method:'GET',path:'/hello',handler:function(request,reply){reply('hello world');}});// Start the serverserver.start();
Now that we have updated index.js to use the latest and greatest ES6 syntax, let’s try running it. Depending on what version of Node you are running, your results may vary—that is, the browser might not render “hello world”. This is because ES6 may not be supported by the version of Node you are running. Fortunately, that is not a problem because there is a compiler that will take our ES6 code and compile it to code that runs in Node 4+.
One of the tools mentioned at the beginning of this chapter was Babel, a JavaScript compiler. We will be using Babel throughout the rest of the book to compile our JavaScript, but first we need to install Babel and the ES2015 transformations:
$npminstall--save-devbabel-clibabel-preset-es2015
By default Babel itself does not do anything. It uses plugins to transform code. The previous command line installed a preset, which is a preconfigured plugin or set of plugins for transforming ES6 code to ES5 code so that it will run in legacy browsers and older versions of Node.
Just like the hapi installation command, the previous command will add Babel as a dependency to your package.json file. The only difference is that we passed --save-dev instead of --save, so the dependency will be listed under devDependencies in the package.json file as opposed to the dependencies property. The reason for this is because Babel is not required to run your application, but it is a development requirement. This distinction makes it clear to consumers what dependencies are needed for production vs. development. When your project is installed using the --production flag, the Babel CLI will not be installed.
Now that we have successfully installed Babel we can compile index.js, but before we do that we should restructure our project a bit.
If we were to leave our project structure as is and compile index.js, then we would overwrite our source code—and losing your source code without a backup is not a great experience. The fix for this is to specify an output file for Babel. A common location for a project’s distributable is dist, and the common directory for source code is src. I’m not one for inventing new standards, so let’s just stick with those:
$mkdirdist$mkdirsrc$mvindex.jssrc/index.js
Now that we have a project structure that works well for compiling our source, let’s make a change to our package.json. The first change is pointing the main property to our new distributable by changing the "main": "index.js," line to "main": "./dist/index.js",. This informs Node of the new entry point for our application, so that when we execute node . it loads the correct script. We are now ready for our first compilation! Enter the following at the command line:
$babelsrc/index.js--out-filedist/index.js
If you receive a “Command not found!” error when executing this command, then you might need to specify the path to the executable, ./node_modules/.bin/babel, or install babel globally with npm install -g babel-cli instead.
The previous command takes our source, src/index.js, compiles it, and creates our distributable, dist/index.js. If everything went well then we should be able to start up our server again using node ..
We’ve barely scratched the surface of Babel and ES6 in the last few sections. For more information, visit https://babeljs.io.
We now have the ability to compile at will and restart the server via the command line, which is a great accomplishment, but stopping development each time to execute those two commands at the terminal will get old very quickly. Luckily, we are not the first people to have the need to automate repetitive tasks and optimize our workflow, so there are a few different automation choices at our disposal. Naturally we will want to pick the latest and greatest choice, so that our code will remain relevant for the next six months. The most recent addition to the JavaScript build world is Gulp, a streaming build system in which you create tasks and load plugins from the extensive community-driven ecosystem. These tasks can then be chained together to create your build process. Sounds great, right? Well, let’s get started. First we need to install Gulp:
$npminstallgulp--save-dev
The next thing we need to do is to create a gulpfile.js filr at the root of our project and define a default task, as shown in Example 5-4.
vargulp=require('gulp');gulp.task('default',function(){console.log('default task success!');});
Now that we have Gulp installed and a default task defined, let’s run it:
$gulp[20:00:11]Usinggulpfile./gulpfile.js[20:00:11]Starting'default'...defaulttasksuccess![20:00:11]Finished'default'after157μs
All absolute paths in the Gulp output have been altered to relative paths (e.g., ./gulpfile.js) so they will fit within the margins of the code blocks. In the output printed to your screen, you will see the absolute path.
If you receive a “Command not found!” error when executing the gulp command, you might need to specify the path to the executable, ./node_modules/.bin/gulp, or install gulp globally using npm install -g gulp instead.
Success! We have Gulp running. Now let’s create a task of substance, like compiling our source. This will require the gulp-babel plugin, which we can install as follows:
$npminstallgulp-babel--save-dev
In our gulpfile.js we now need to modify our default task to handle the compilation. Replace the contents of the file you created earlier with the code shown in Example 5-5.
vargulp=require('gulp');varbabel=require('gulp-babel');gulp.task('compile',function(){returngulp.src('src/**/*.js').pipe(babel({presets:['es2015']})).pipe(gulp.dest('dist'));});gulp.task('default',['compile']);
Now let’s run Gulp again:
$gulp[23:55:13]Usinggulpfile./gulpfile.js[23:55:13]Starting'compile'...[23:55:13]Finished'compile'after251ms[23:55:13]Starting'default'...[23:55:13]Finished'default'after17μs$
This is great, but we are no better off than we were before. The only thing we have really done is to reduce the amount of code we have to type at the terminal. In order for the inclusion of Gulp to really add value we need to automate the compilation and server restarting on a source file save.
Gulp has a built-in file watcher, gulp.watch, that accepts file globs, options, and a task list or callback to be executed when a file changes that matches a glob. This is just what we need to run our Babel task, so let’s configure it. Add the following task to gulpfile.js:
gulp.task('watch',function(){gulp.watch('src/**/*.js',['compile']);});
This should give us just the behavior we are seeking, so let’s add it to our default task:
gulp.task('default',['watch','compile']);
If you run gulp in your terminal and make a file change you should now see something similar to the following:
$gulp[00:04:35]Usinggulpfile./gulpfile.js[00:04:35]Starting'watch'...[00:04:35]Finished'watch'after12ms[00:04:35]Starting'compile'...[00:04:35]Finished'compile'after114ms[00:04:35]Starting'default'...[00:04:35]Finished'default'after16μs[00:04:39]Starting'compile'...[00:04:39]Finished'compile'after75ms
This is really nice because now we don’t have to run our compile command every time we make a source code change. Now we just need to get the server to restart automatically.
To watch for changes to the distribution file, dist/index.js, we will be using the gulp-nodemon plugin, which wraps nodemon. nodemon is a utility that watches for changes and automatically restarts your server when changes are made. Before we can begin leveraging gulp-nodemon, we have to install it:
$npminstallgulp-nodemon--save-dev
Next we need to install run-sequence:
$npminstallrun-sequence--save-dev
This will run a sequence of Gulp tasks in a specified order. This is necessary because we need to ensure that our distributable exists before we try to start the server.
Now we need to code the imperative bits in our gulpfile.js file that instruct gulp-nodemon when to restart the server and include run-sequence. Add the following lines to the file:
varnodemon=require('gulp-nodemon');varsequence=require('run-sequence');gulp.task('start',function(){nodemon({watch:'dist',script:'dist/index.js',ext:'js',env:{'NODE_ENV':'development'}});});
Lastly, we need to add our new nodemon task to our default task and use run-sequence to specify the task execution order:
gulp.task('default',function(callback){sequence(['compile','watch'],'start',callback);});
Now when you run the default task and make a source code change you should see something similar to the following:
$gulp[16:51:43]Usinggulpfile./gulpfile.js[16:51:43]Starting'default'...[16:51:43]Starting'compile'...[16:51:43]Starting'watch'...[16:51:43]Finished'watch'after5.04ms[16:51:44]Finished'compile'after57ms[16:51:44]Starting'start'...[16:51:44]Finished'start'after849μs[16:51:44]Finished'default'after59ms[16:51:44][nodemon]v1.4.0[16:51:44][nodemon]torestartatanytime,enter`rs`[16:51:44][nodemon]watching:./dist/**/*[16:51:44][nodemon]starting`nodedist/index.js`[16:51:47]Starting'compile'...[16:51:47]Finished'compile'after19ms[16:51:48][nodemon]restartingduetochanges...[16:51:48][nodemon]starting`nodedist/index.js`
Your completed gulpfile.js should look like Example 5-6.
vargulp=require('gulp');varbabel=require('gulp-babel');varnodemon=require('gulp-nodemon');varsequence=require('run-sequence');gulp.task('compile',function(){returngulp.src('src/**/*.js').pipe(babel({presets:['es2015']})).pipe(gulp.dest('dist'));});gulp.task('watch',function(){gulp.watch('src/**/*.js',['compile']);});gulp.task('start',function(){nodemon({watch:'dist',script:'dist/index.js',ext:'js',env:{'NODE_ENV':'development'}});});gulp.task('default',function(callback){sequence(['compile','watch'],'start',callback);});
We covered quite a bit of material in this chapter, from installing Node to optimizing our development workflow for our first hapi application server, including the usage of ES6. All of this was in preparation for modern JavaScript application development. This knowledge also doubles as the foundation for the working example application we will begin building in Chapter 6.
You can install the completed code examples from this chapter by executing npm install thaumoctopus-mimicus@"0.1.x" in your terminal.