Node.js Dojo webserver

Ondertussen heb ik al enkele tutorials geschreven over Node.js, Dojo Toolkit en Node.js als back-end + Dojo toolkit als front-end. In deze tutorial ga ik Dojo toolkit gebruiken als hulpmiddel bovenop Node.js waardoor ik de Dojo AMD style kan gebruiken in Node.js scripts.

Heads up!

In deze tutorial wordt met regelmaat verwezen naar mijn vorige Node.js tutorials. Het is dus handig dat je deze eens doorneemt.

Project opzetten

De opzet van dit project is iets ingewikkelder dan het opzetten van een gewoon Node.js project. De reden hiervoor is dat Node.js zijn eigen `require()` functie heeft. Hierdoor moet eigenlijk het werklijke Dojo/Node.js script gebootstrapped worden.

Maak een nieuw project aan en maak hierin de mappen src, logs en htdocs aan. Het eerste wat je nodig gaat hebben is de Dojo Toolkit SDK, download deze en extract de dojo map naar de src map. We hebben enkel de dojo-map nodig omdat de dijit-map widgets bevat (dus geen back-end functionaliteit) en DojoX is een combinatie van en uitenidelijk gaan we deze packages toch niet nodig hebben.

Maak in je src map tevens de mappen app en utils aan. In de app  package gaat het Node.js script terecht komen dat gebootstrapped wordt. In de utils package gaat dan weer een Dojo utility module terecht komen.
Maak in de app package een JavaScript bestand aan met de naam web-server-dojo.js en in de utils package een JavaScript bestand met de naam FileSystemUtils.js. In de root map van je project moet je nu enkel nog het JavaScript bestand bootstrap.js plaatsen.

De laatste stap is dat we weer eens de simple-mime module gaan gebruiken, open dus een terminal in je project en gebruik het volgende commando:

npm install simple-mime

Linux gebruikers zullen wellicht dit commando als super user moeten uitvoeren (sudo). Eenmaal klaar zou je project er moeten uitzien als in onderstaande afbeelding.

project-structure

Bootstrapping

Om Dojo werkende te krijgen onder Node.js moet je eerst enkele aanpassingen maken aan je Dojo configuratie via de dojoConfig variabel. De code hiervoor is:

dojoConfig = {
	baseUrl: "./src/",
	async: true,
	hasCache: {
		"host-node": true,
		"dom": false
	},
	packages: [{
		name: "dojo",
		location: "dojo"
	},
	{
		name: "app",
		location: "app"
	},
	{
		name: "utils",
		location: "utils"
	}],
	deps: [
		"app/web-server-dojo"
	]
};

for (var i = 0; i < dojoConfig.packages.length; i++) {
	if (dojoConfig.packages[i].name == "dojo") {
		require(dojoConfig.baseUrl + dojoConfig.packages[i].location + "/dojo.js");
	}
}

Wat we hier doen is eigenlijk vrij eenvoudig, we geven een base directory aan (de src map), we maken duidelijk dat we gebruik willen maken van de AMD style door async op true te zetten en uiteraard hebben we ook nog enkele packages nodig, namelijk app, utils en dojo. In het deps veld plaatsen we dan weer het script dat gebootstrapped moet worden, namelijk web-server-dojo.

Ten slotte moeten we enkel nog de Dojo core (dojo.js) inladen zoals je dat ook in je browser zou doen (door het script toe te voegen). Dit doen we met de require() functie.
Omdat de locatie van de Dojo module gekend is (is namelijk de baseUrl + de locatie van de Dojo package) haal ik het ook op die manier op door in een for-lus uit te zoeken wat de locatie van de Dojo package is.

FileSystemutil

Het eerste wat we gaan doen is een groot deel van de Node.js fs (filesystem) module omzetten naar de Dojo AMD style via de dojo/Deferred module. De code hiervoor is:

define(["dojo/Deferred", "dojo/node!fs"], function(Deferred, fs) {
	return {
		readFile: function(filename) {
			var def = new Deferred();
			fs.readFile(filename, function(err, data) {
				if (err) {
					def.reject(err);
				} else {
					def.resolve(data);
				}
			});
			return def.promise;
		},
		exists: function(filename) {
			var def = new Deferred();
			fs.exists(filename, function(isExisting) {
				if (isExisting) {
					def.resolve(isExisting);
				} else {
					def.reject(isExisting);
				}
			});
			return def.promise;
		},
		rename: function(file, newFile) {
			var def = new Deferred();
			fs.rename(file, newFile, function(err) {
				if (err) {
					def.reject(err);
				} else {
					def.resolve();
				}
			});
			return def.promise;
		}
	};
});

Deze module gaan we gebruiken als een soort van statische module waarbij de functies readFile(), exists() en rename() gebruikt kunnen worden. De code hiervan moet jullie vrij bekend voorkomen omdat deze module gebruik maakt van de standaard fs-module van Node.js.
Wat je wel zou moeten opmerken is dat we deze Node.js module nu niet meer op de ouderwetse manier importeren (dat gaat ook niet meer want door het bootstrappen is de oude require() functionaliteit overschreven door die van Dojo). Node.js modules importeren we nu door de module dojo/node te gebruiken met daarachter als parameter de naam van de Node.js module, bijvoorbeeld dojo/node!fs.

Het tweede dat je zou moeten merken is dat we de output van de callback abstract gemaakt hebben via de dojo/Deferred module. Als we een error tegen komen gebruiken we de reject() functie en als we een goede response terug krijgen gebruiken we resolve().

Bij de exists() functie heb ik dit gebruikt om als het bestand bestaat resolve() te gebruiken en indien het niet bestaat reject() te gebruiken.

Web server

Het volgende stuk code is uiteraard de web server zelf. Qua structuur is hier relatief weinig veranderd ten opzichte van het Node.js script dat we eerder al geschreven hebben om een web server te maken in deze tutorial. De code voor de webserver (src/app/web-server-dojo.js) is:

require([
	"dojo/node!http",
	"dojo/node!url",
	"dojo/node!fs",
	"dojo/node!simple-mime",
	"dojo/on",
	"dojo/aspect",
	"utils/FileSystemUtil"], function(http, url, fs, getMime, on, aspect, fsUtil) {

	var logFile = "logs/http.log";

	aspect.after(fs, "appendFile", function(file, data) {
		console.log(data.replace("\r\n", ""));
	}, true);

	http.createServer(function(request, response) {
		var myUrl = url.parse(request.url, true).pathname;
		myUrl = "htdocs" + (myUrl.charAt(myUrl.length - 1) == "/" ? "/index.html" : myUrl);

		fs.appendFile(logFile, request.method + " " + request.url + " HTTP/" + request.httpVersion + "\r\n");
		for (name in request.headers) {
			fs.appendFile(logFile, name + ": " + request.headers[name] + "\r\n");
		}

		on(request, "end", function() {
			fs.appendFile(logFile, "\r\n");
			fsUtil.exists(myUrl).then(function(exists) {
				fsUtil.readFile(myUrl).then(function(data) {
					response.writeHead(200, {
						"Content-Type": getMime(myUrl)
					});
					response.end(data, "UTF-8");
				}, function(err) {
					response.writeHead(500);
					response.end(err, "UTF-8");
				});
			}, function() {
				response.writeHead(404);
				response.end("File not found", "UTF-8");
			});
		});
	}).listen(8080, function() {
		fsUtil.exists(logFile).then(function(exists) {
			fsUtil.rename(logFile, logFile + "." + Math.round((new Date()).getTime() / 1000)).then(function() {
				fs.appendFile(logFile, "Server initialized\r\n\r\n");
			});
		});
	});
});

Net zoals in de FileSystemUtil maken we hier gebruik van de Dojo AMD-style require() functie om de Node.js modules (en Dojo modules) in te laden. Interessant hieraan is dus dat je een abstractielaag hebt waarmee je zowel Node.js als Dojo module kan inladen.

Wat nieuw is in deze tutorial is het gebruik van de dojo/aspect module waarmee je eigenlijk AOP features kan gaan gebruiken, maar dan in JavaScript. In dit geval ga ik het gebruiken om als er iets aan de logfile toegevoegd wordt, het ook op de console uit te printen zodat je eigenlijk niet steeds in je log moet kijken wat er gebeurt. De code hiervoor is:

aspect.after(fs, "appendFile", function(file, data) {
	console.log(data.replace("\r\n", ""));
}, true);

Wat deze code doet is ervoor zorgen dat nadat de functie fs.appendFile() aangeroepen wordt dat de meegegeven callback uitgevoerd wordt. De parameter true achteraan zorgt ervoor dat we de parameters op dezelfde manier binnen krijgen als we in de functie zelf doen. Geven we deze niet mee dan krijgen we één object met alle parameters daarin.

Het volgende dat je zou moeten opvallen is dat we de dojo/on module (die je kan gebruiken voor DOM events) tevens kan gebruiken voor Node.js events. Zo hebben we hier gebruik gemaakt van deze module om het request end event aan te roepen.

on(request, "end", function() {
	...
}

Het laatste speciale is dat we hier de Deferred aanroepen van de FileSystemUtil gaan aanroepen. De syntax daardoor is de volgende geworden:

fsUtil.exists(myUrl).then(function(exists) {
	...
}, function() {
	...
});

Waarbij dus de eerste functie aangeroepen wordt indien succesvol en de tweede functie aangeroepen wordt indien niet succesvol. Dit hebben we uitgevoerd bij zowel de exists(), readFile() als rename() functie.

Uitvoeren met Node.js

Daarmee zijn we dan ook aan het einde gekomen van deze tutorial. Zorg voor enkele HTML documenten in je htdocs map en je bent klaar om te testen.
Het uitvoeren van dit project doen we uiteraard met Node.js en daarvoor voeren we het volgende commando uit (in de project map):

node bootstrap.js

Let dus op dat je de bootstrap uitvoert met Node.js en niet rechtstreeks je app.

Het resultaat is dat je nu de logging ook in je console zal zien.

result

Nu kan je dus alle voordelen van de Dojo toolkit IN Node.js gebruiken.

Download project

Download – web-server-dojo.tar.gz (850 kB)

This entry was posted in JavaScript, Tutorials.