Root.js : A Skeletal MVC Framework for Node.js

OK, by skeletal, I mean absolutely no fat whatsoever. What you are about to witness is merely an illustration of a thought. Here’s the thought: I want to do serious server-side web development with JavaScript and Node.js. Why? Node.js is a convergence. The creation of Node.js was precipitated by several phenomena including the rise of JavaScript as the central programming language of the web, increased interest in functional programming, and advances in event-based programming such as epoll system call in Linux 2.6.

Without further ado, here is Root.js in primordial form:

var sys = require("sys");
var http = require("http");
var posix = require("posix");
 
function loadfolder (name)
{
    var data = {};
    var output = sys.exec("ls " + name).wait();
    var files = output[0].split("\n");
    for(var j in files)
    {
        if(files[j].match(/\.js$/))
        {
            var modname = files[j].replace(/\.js$/, "");
            sys.puts("Loading " + name + "/" + modname);
            data[modname] = require("./" + name + "/" + modname);
        }
    }
    return data;
}
 
var models = loadfolder("models");
var views = loadfolder("views");
var controllers = loadfolder("controllers");
 
for(var x in controllers)
{
    controllers[x].load(models,views);
}
 
var routemap = 
[
    [/^\/other(.*)$/, "other"],
    [/^\/(.*)$/, "default"]
];
 
http.createServer( function (request, response) {
    for(var i in routemap)
    {
        var route = routemap[i];
        var matches = route[0].exec(request.uri.path);
        if(matches != null)
        {
            matched = true;
            var c = controllers[route[1]];
            c.handle(matches.slice(1),request,response); 
            return;
        }
    }
}).listen(8000);
 
sys.puts("Server running at http://127.0.0.1:8000/");

This file expects a few things. It expects there will be folders named models, views, and controllers. In its current configuration the routemap will expect to find files named default.js and other.js in the controllers folder. Any controllers should export a function called load that provides a way to pass in the models and views during initialization and a function called handle that is called when there is an incoming request. Here is a simple example controller (default.js):

var models = null;
var views = null;
 
exports.load = function (m,v) { models = m; views = v; };
 
exports.handle = function (matches, request, response)
{
    models.mymodel.getData( function (data) {
        views.defaultview.show(response,data);
    });
}

This controller calls the getData function exported by the mymodel model which would be a file named mymodel.js in the models folder looking something like this:

var data = 
{
    title : "Page Title",
    message : "Hello, JS World"
};
 
exports.getData = function(callback)
{
    callback(data);
}

In this case, the model does not need to do an asynchronous call to get the data, but in case it did, the controller passes in a callback that the model can pass the data to asynchronously, in typical Node.js fashion. The last thing we need is the view (views/defaultview.js):

var posix = require("posix");
var jsontemplate = require("../json-template").jsontemplate;
var templates = {};
 
loadtemplates = function()
{
    templates['basic'] = jsontemplate.Template(
        posix.cat("./templates/basic.html").wait());
}
 
exports.show = function(response, data)
{
    response.sendHeader(200, {"Content-Type":"text/html"});
    var text = templates['basic'].expand(data);
    response.sendBody(text);
    response.finish();
}
 
loadtemplates();

Oh yes, and the template (templates/basic.html):

<!DOCTYPE html>
<head>
<title>{title}</title>
</head>
<body>
<h3>{message}</h3>
</body>

This example uses JSON Template which I briefly discussed here.

The preceding code actually works. It’s missing the controllers/other.js controller, but it’s enough to get the general idea. I could sit down with this as a starting point and create a web application. In fact, I just may do this. This framework has basically zero helper functions. I’ve noticed that the best helper functions and other abstractions of useful functionality spring forth naturally from the act of programming and active refactoring. When I notice myself repeating work, or pondering the idea of copy-pasting code from one part of an application to another, that’s generally the time I do the work of building infrastructure. If I decide to use this framework to do something useful, then it will develop naturally. Right now it is a blank canvas.

I recently used Redis to solve some problems as work. There is already a node.js library for accessing Redis here, so I’m thinking about building something with that. Redis is essentially a persistent key-value store that supports simple data structures with atomic operations such as lists and sets. The “list” solved a very specific problem for us which had previously been partially solved (in an ultimately broken way) by building a queue on top of Memcached using a locking system built on the atomic increment and decrement operations. We dropped in the Redis list and everything started working perfectly.

If I decide that I need a traditional SQL backend, I would probably use DBSlayer, a http/JSON interface for MySQL, though Ryan Dahl has promised native support for some traditional databases in future versions of Node.js. Cool.

Post a Comment

Your email is never published nor shared. Required fields are marked *

Powered by WP Hashcash