When I got to RC, I knew I wanted to wrap my head around how the web works: I had struggled to wrap my head around clients, servers, and HTTP requests. Tom suggested that I build a simple HTTP server and play around with it. I had followed nodejs tutorials before, so this time I decided I would build everything by reading only the docs. I also wanted to get a better handle on I/O - as the internals of the file system were a mystery to me, so I decided that my little app would search for and serve text from files on my computer.
My first version of this server used standard JavaScript callbacks:
var http = require('http'),
fs = require('fs'),
url = require('url');
var server = http.createServer(function(req, res) {
//Get the request parameter from the url
var name = url.parse(req.url, true).query.name;
//If there is a request parameter, hunt through the files for a matching filename
if (name) {
fs.readdir(process.cwd(), searchForFile);
} else {
var searchError = new Error('To search for a file, add a ?name=... to your url')
writeError(searchError);
}
function searchForFile(err, files) {
var found = false;
if (!err) {
files.forEach(matchFile);
if (!found) {
var noFile = new Error('Sorry, there is no such file!\n' +
'Did you mean to select one of the following files:\n' +
files.join(', '));
writeError(noFile);
}
} else {
writeError(err);
}
function matchFile(item) {
//Magical RegExp code to get the name without the file extension
var itemname = item.replace(/(.*)\.[^.]+$/, '$1');
//If a matching file has been found, write contents to response
if (itemname === name) {
found = true;
fs.readFile(item, writeSucccess);
}
}
}
//Success Handler
function writeSucccess(err, data) {
res.writeHead(200);
res.write(data.toString());
res.end();
}
//Error Handler
function writeError(err) {
res.writeHead(500);
console.log(err);
res.write(err.message);
res.end();
}
});
Things I learned from writing this version:
+Scope is important. Drawing diagrams helps me figure out control flow and variable access. +Callbacks are super ugly, and abstracting out functionality to avoid unnecessary repetition can create scope problems. +Creating custom errors is easy and important.
I had heard that event emitters and file streams were neat, so I decided to write another version of the same server using different patterns. The server looked like this:
var http = require('http'),
fs = require('fs'),
FileFinder = require('./filefinder.js');
var server = http.createServer(function(req, res) {
var filefinder = new FileFinder();
filefinder.on('error', function(err) {
console.log('Error!');
res.writeHead(500);
res.write(err.message);
res.end();
});
filefinder.on('filefound', function(file) {
console.log('back to server!');
var readStream = fs.createReadStream(file);
readStream.pipe(res);
});
filefinder.begin(req.url);
});
server.listen(8080);
So much cleaner! I put the file-finding code inside a separate module:
var util = require('util'),
EventEmitter = require('events').EventEmitter,
url = require('url'),
fs = require('fs');
var FileFinder = function() {
EventEmitter.call(this);
//Get the url & parse the name
var _parse = function (requesturl) {
var filename = url.parse(requesturl, true).query.name;
if (filename) this.emit('findlist', filename);
else this.emit('error', new Error('Try again using name parameter'));
};
var _findfilelist = function (filename) {
fs.readdir(process.cwd(), getFileList.bind(this));
function getFileList(err, filelist) {
if (filelist) {
this.emit('filelistfound', filelist, filename);
} else {
this.emit('error', new Error('Cannot retrieve files'));
}
}
};
var _findfile = function(files, filename) {
var found = false;
files.forEach(findFile.bind(this));
function findFile(item) {
itemname = item.replace(/(.*)\.[^.]+$/, '$1');
if (itemname === filename) {
found = true;
this.emit('filefound', item);
}
};
if (!found) {
console.log('No File found');
this.emit('error', new Error('Cannot find file named: '+ filename));
}
};
this.begin = function(url) {
this.emit('begin', url);
};
this.on('begin', _parse);
this.on('findlist', _findfilelist);
this.on('filelistfound', _findfile);
}
util.inherits(FileFinder, EventEmitter);
module.exports = FileFinder;
Things I learned from writing this version:
+function.prototype.bind is AMAZING. It allowed me to generalize functionality, without losing fine-tuned control over variable access. +Object Oriented prototyping in JavaScript is quite unglamorous +Control flow using event emitters is quite clean, as long as nothing requires multiple events. Branching, however, can get ugly.
I had also heard about this thing called Promises that many JS engineers are excited about, and that will become native in ES6. So I decided to try my hand at them to see what the fuss is about:
var http = require('http'),
fs = require('fs'),
url = require('url'),
Promise = require('promise');
//Promisify node fs functionality
var readDir = Promise.denodeify(fs.readdir);
var read = Promise.denodeify(fs.readFile);
var server = http.createServer(function(req, res) {
var filename = url.parse(req.url, true).query.name;
readDir(process.cwd())
.then(findFile.bind(undefined, filename))
.then(read)
.then(write.bind(this, res))
.catch(write_error.bind(this, res))
});
function findFile(filename, listOfFiles) {
if (!filename) {
throw new Error("Must request a file using the name search parameter");
}
var file = listOfFiles.filter(function(item) {
return (item.replace(/(.*)\.[^.]+$/, '$1') === filename);
})[0];
if (file) {
return file;
} else {
throw new Error("No file found with name: " + filename);
}
}
function write(res, data) {
res.write(data);
res.end();
}
function write_error(res, err) {
res.write(err.message);
res.end();
}
server.listen(8080);
What I learned:
+The server code is way more clean, especially after I named and separated out each function. That said, as with event emitters, this depends on control flow that is pretty linear. JavaScript is still single-threaded, after all. +bind is still magical +I am glad that someone else has written some generic wrapper code that converts callbacks into promises. It apparently works only when functions adhere to the standard callback function(err, data) pattern, which is limiting. Exploring the source would probably be a good idea.
Overall, this was a useful project that wasn’t too difficult. This week, I also enjoyed pairing with Tom and Steve on their JS projects: I helped Tom write a lazy HTML canvas, that collects operations to be executed later. Some unexpected behavior led us to discover that elements of the canvas API (e.g: FillStyle) that don’t act as regular functions actually wrap a getter and setter function.