This final exercise puts several of the previous pieces together and goes back to the roots of using node as a web server while illustrating some more advanced concepts like how to use npm, and how a simple chat app can be easily put together using a library like socket.io.
It also introduces a very common usage of node - that of a task runner - in the form of writing a simple gulpfile for the project, to transpile various static assets.
This time around you'll be making a proper node app from scratch, the first thing to do is create an entry point for the app, so make a file called index.js in this folder.
Now open a command prompt and initialise a new node project in this folder using npm init.
npm initTake the default answer for each of the questions it asks you, and you'll find it makes you a package.json file in this folder, now we can start work on the gulpfile.
Gulp is a task runner that runs on node (for more about gulp see here), you compose the build steps you need using plain javascript in a build file (known as a gulpfile) which is, by convention, called gulpfile.js and placed in the root directory of your project.
To start writing our gulpfile we need to do three things
- Make a file called
gulpfile.jsin the3-chatdirectory - Install gulp as a development dependency (using
--save-dev), which both installs it and saves it to ourpackage.json(as a way to persist our use of the package back into source control)
npm install gulp --save-dev- Globally install gulp on your machine (the
-gmeans global) - this allows us to use the commandgulpdirectly from any command prompt
npm install gulp -gThe skeleton of our gulpfile can now be written
var gulp = require('gulp');
gulp.task('default', function() {
console.log('Default task!');
});This introduces the important function gulp.task, which is used to declare tasks which can be invoked by name using the gulp command in a command prompt (in this case we created the special, default task which is invoked when you don't supply a task name to the gulp command).
So, to see it in action, just run
gulpfrom the 3-chat directory and it'll log out "Default task!".
Now we'll make the first build task - scripts, which just copies the js files from client/scripts to dist/scripts.
var gulp = require('gulp');
gulp.task('scripts', function() {
return gulp.src('./client/scripts/**/*.js').
pipe(gulp.dest('./dist'));
});
/* Default task ignored for now */Here we see another key feature of gulp - that of a stream (specifically a vinyl-fs stream), created using gulp.src which is then piped through to an output, using gulp.dest.
If the
**/*.jssyntax is unfamiliar to you, it's a glob expression and works like a simple form of regex for matching filepaths - see here for more info.
If you now run
gulp scriptsit won't quite do what it's supposed to - it'll copy the js files, but will omit the scripts folder, as it ignores the paths the files were found at relative to the root.
We can fix this by specifying a base for the src
var gulp = require('gulp');
gulp.task('scripts', function() {
return gulp.src('./client/scripts/**/*.js', {
// Files will be copied to './dist' with their
// path relative to './client'
base: './client'
}).
pipe(gulp.dest('./dist'));
});
/* Default task ignored for now */Gulp can be used simply for copying files, but it is more comonly used for transforming them in some way - concatenating, minifying, transpiling, etc. - which is done by pipeing through plugins which transform the files along the way.
We'll now make a task which reads styl files (stylus - a language which transpiles to css) and outputs plain old css files, by pipeing through the gulp-stylus plugin
var gulp = require('gulp'),
stylus = require('gulp-stylus');
gulp.task('styles', function() {
return gulp.src('./client/styles/**/*.styl', {
base: './client'
}).
pipe(stylus()).
pipe(gulp.dest('./dist'));
});
/* Other tasks ignored */This looks almost exactly the same as the scripts task - except we pipe it through gulp-stylus (transforming it from stylus to css) before pipeing it out to its destination (using gulp.dest).
If you try to run gulp styles though it will fail since we haven't installed gulp-stylus, so lets do that now, along with a few other packages we need for our gulpfile (you can list any number of packages separated by spaces for npm install, likewise for several other npm commands)
npm install --save-dev gulp-stylus gulp-jade del run-sequence browser-syncOnce that completes you can test the task by running gulp styles which should output main.css to dist/styles.
Next we'll add the task to turn our jade view files into regular html, and then we'll connect all three of our tasks into a single build task
var gulp = require('gulp'),
stylus = require('gulp-stylus'),
jade = require('gulp-jade');
gulp.task('views', function() {
return gulp.src('./client/**/*.jade', {
base: './client'
}).
pipe(jade()).
pipe(gulp.dest('./dist'));
});
// You can specify an array of task names for a gulp.task to run
// In this case, 'build' runs our other three tasks
gulp.task('build', ['scripts', 'styles', 'views']);
/* Other tasks ignored */When we rebuild the project we'll want to make sure we do it into a clean directory, so we'll add a cleanup task (and also re-do our default)
var gulp = require('gulp'),
stylus = require('gulp-stylus'),
jade = require('gulp-jade'),
runSequence = require('run-sequence'),
del = require('del');
gulp.task('clean', function() {
return del('./dist/**/*');
});
// The default task cleans THEN builds, the array syntax used for 'build'
// runs tasks in parallel, so we use 'run-sequence' to make them sequential
gulp.task('default', function(done) {
runSequence('clean', 'build', done);
});
/* Other tasks ignored */The last thing we need to do is setup a development task which will watch our source files and rebuild things when needed (it'll also reload our browser for us, using browser-sync, when needed)
var gulp = require('gulp'),
stylus = require('gulp-stylus'),
jade = require('gulp-jade'),
runSequence = require('run-sequence'),
del = require('del'),
browserSync = require('browser-sync');
// TODO: server = require('./server/index');
// The array means 'build' runs before 'dev' starts
gulp.task('dev', ['build'], function() {
// TODO: Launch server on 8111: "server(8111);"
// We init browser-sync to proxy our application
browserSync.init({
proxy: 'http://localhost:' + 8111
});
// Watch all files in 'client' ending with '.jade',
// and run 'views' when any of them change
gulp.watch('./client/**/*.jade', ['views']).
// We can also listen for 'change' and run a browser reload
on('change', browserSync.reload);
gulp.watch('./client/scripts/**/*.js', ['scripts']).
on('change', browserSync.reload);
// Styles are different in that we can update them without
// reloading the page, to do this we modify the task itself
gulp.watch('./client/styles/**/*.styl', ['styles']);
});
gulp.task('styles', function() {
return gulp.src('./client/styles/**/*.styl', {
base: './client'
}).
pipe(stylus()).
pipe(gulp.dest('./dist')).
// This line triggers css-injection if browser-sync is running
pipe(browserSync.stream());
});
/* Other tasks ignored */We're done with the gulpfile for now, we'll see the finished product once we're able to deal with those TODOs a little later.
Our previous web server example was the ultra-basic 'hello world', which used only built-in modules to serve some plain test. This time round we'll use express to provide us a higher-level web server for serving some static content.
The first step is to install express and save it as a (production) dependency (using --save)
npm install express --saveThen we make our server/index.js file
var http = require('http'),
path = require('path'),
express = require('express');
// This file exports a function which starts a server on the specified port
module.exports = function(port) {
var app = express(),
// We use 'http' to create a server which calls our express app
// with each request
server = http.createServer(app);
// Make the server listen on the specified port
server.listen(port, function () {
console.log('Server listening at port %d', port);
});
// Use express to serve the files in '../dist'
app.use(express.static(path.resolve(__dirname, '../dist')));
};This example introduced another couple of the global objects present in node (a previously encountered one being require).
One was module - which can be used for exporting content from a file (module) using its exports property. The exports object is present in two forms - module.exports and also under the alias exports, which is a reference to module.exports.
We also made use of __dirname, which is the absolute path to the directory this file is in (there is a similar variable, __filename, which is the absolute filename).
An example of importing and exporting between files
// file1.js
// Make sure we do 'exports =', else exports !== module.exports after
// the assignment
exports = module.exports = function() { return 'default export'; };
exports.a = function() { return 'a'; };// file2.js
var f1 = require('./file1');
f1(); // => default export
f1.a(); // => aIf you now make another file, index.js, and put this in it
// Launch a server on port 3001
require('./server/index')(3001);You can run
node index.jsfrom command line and go to http://localhost:3001 to view the chat application (provided you ran gulp to build it).
Currently, the application doesn't actually do anything, so lets install some packages that we can use to make a working chat app
npm install --save socket.io haikunatorWe'll now update our app to include socket.io, which allows us to communicate with clients using websockets
var http = require('http'),
path = require('path'),
express = require('express'),
socketio = require('socket.io'),
haikunate = require('haikunator');
// This file exports a function which starts a server on the specified port
module.exports = function(port) {
var app = express(),
// We use 'http' to create a server which calls our express app
// with each request
server = http.createServer(app),
// We can re-use the server to also receive websocket connections
// through it, using socket.io
io = socketio(server);
// Make the server listen on the specified port
server.listen(port, function () {
console.log('Server listening at port %d', port);
});
// Use express to serve the files in '../dist'
app.use(express.static(path.resolve(__dirname, '../dist')));
// Listen for websocket connections
io.on('connection', function (socket) {
// TODO: Do stuff with socket
});
};Communicating between clients is made simple with the use of socket.io's rooms feature
var http = require('http'),
path = require('path'),
express = require('express'),
socketio = require('socket.io'),
haikunate = require('haikunator');
// This file exports a function which starts a server on the specified port
module.exports = function(port) {
var app = express(),
// We use 'http' to create a server which calls our express app
// with each request
server = http.createServer(app),
// We can re-use the server to also receive websocket connections
// through it, using socket.io
io = socketio(server);
// Make the server listen on the specified port
server.listen(port, function () {
console.log('Server listening at port %d', port);
});
// Use express to serve the files in '../dist'
app.use(express.static(path.resolve(__dirname, '../dist')));
io.on('connection', function (socket) {
// First thing we do is generate a random username for all connections
var username = haikunate();
// Log that we got a new connection
console.log('Client connected (username: %s)', username);
// Make the connection join the 'main' room
socket.join('main');
// Tell the new client what their username is
socket.emit('username', username);
// Let everyone else in the room know a new user has joined
socket.to('main').emit('in', username);
// When we receive a message from this client
socket.on('msg', function (msg) {
// Echo it to everyone else in the 'main' room
socket.to('main').emit('msg', { username, msg });
});
// Finally, when this client disconnects
socket.on('disconnect', function () {
// Tell everyone still in the room about it
socket.to('main').emit('out', username);
});
});
};We can now complete the gulpfile to launch our server for us as part of the dev task
var gulp = require('gulp'),
stylus = require('gulp-stylus'),
jade = require('gulp-jade'),
runSequence = require('run-sequence'),
del = require('del'),
browserSync = require('browser-sync'),
server = require('./server/index');
gulp.task('clean', function() {
return del('./dist/**/*');
});
gulp.task('scripts', function() {
return gulp.src('./client/scripts/**/*.js', {
// Files will be copied to './dist' with their
// path relative to './client'
base: './client'
}).
pipe(gulp.dest('./dist'));
});
gulp.task('styles', function() {
return gulp.src('./client/styles/**/*.styl', {
base: './client'
}).
pipe(stylus()).
pipe(gulp.dest('./dist')).
// This line triggers css-injection if browser-sync is running
pipe(browserSync.stream());
});
gulp.task('views', function() {
return gulp.src('./client/**/*.jade', {
base: './client'
}).
pipe(jade()).
pipe(gulp.dest('./dist'));
});
// You can specify an array of tasks for a gulp task to run
// In this case, 'build' runs our other three tasks
gulp.task('build', ['scripts', 'styles', 'views']);
// The array means 'build' runs before 'dev' starts
gulp.task('dev', ['build'], function() {
// Launch our server on port 8111
server(8111);
// We init browser-sync to proxy our application
browserSync.init({
proxy: 'http://localhost:' + 8111
});
// Watch all files in 'client' ending with 'jade',
// and run 'views' when any of them change
gulp.watch('./client/**/*.jade', ['views']).
// We can also listen for 'change' and run a browser reload
on('change', browserSync.reload);
gulp.watch('./client/scripts/**/*.js', ['scripts']).
on('change', browserSync.reload);
// Styles are different in that we can update them without
// reloading the page, to do this we modify the task itself
gulp.watch('./client/styles/**/*.styl', ['styles']);
});
// The default task cleans THEN builds, the array syntax used for 'build'
// runs tasks in parallel, so we use 'run-sequence' to make them sequential
gulp.task('default', function(done) {
runSequence('clean', 'build', done);
});Running
gulp devwill cause browser-sync to open http://localhost:3000 (browser-sync's proxy runs on port 3000) in your default browser, where you should find a working chat app - open up a couple of tabs and try chatting.
If you change any of the source files in /client, you should see they get automatically rebuilt, and your browser refreshed, as required based on what changed