Das "Hallo, Welt!" in node.js ist es einen kleinen Webserver zu bauen der eben diesen Text zurück gibt. Doch was, wenn man nicht nur eine Seite zurückgeben möchte? Eine kleine Funktion hilft abhängig von der eingegebenen URL verschiedenen JavaScript-Funktionen aufzurufen.

In der Doku zu node.js geht hervor, dass das Request-Objekt die URL im Attribut url enthält. Der einfachste Weg URLs auf Funktionen abzubilden ist ein Objekt zu erstellen welches einfach die Pfade als Attributsname und die dazugehörigen Funktionen als Attributswerte verwendet. Dann können wir einfach direkt am Objekt die URL aufrufen. Das abbildende Objekt könnte so aussehen:

var url_mapping = {
  '/': printIndex,
  '/index.html': printIndex
}

Die Funktion printIndex kann so aussehen:

// Ok, not really an index, but works.
var printIndex = function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello, World!\n');
}

Die Abbildung wird dann wie folgt implementiert:

mapped = url_mappings[req.url];
if (mapped) {
  mapped(req, res);
} else {
  console.log(req.url + ' has not been mapped');
  invalidURL(req, res);
}

Der Code nutzt aus, dass wir in JavaScript dynamisch auf Werte eines Objektes zugreifen können und undefined zurückbekommen wenn der Wert im Objekt nicht gesetzt ist. Theoretisch könnte es hier ein Problem geben da JavaScript Objekte immer ein paar eigene Attribute besitzen, wie z.B. die Funktion hasOwnProperty, aber da die URL immer mit einem Schrägstrich beginnt, kann man mit keinem dieser Werte kollidieren, es sei denn irgendeine JavaScript-Bibliothek beginnt irgendwann wahnsinnigerweise Objekte mit solchen Werten anzureichern.

Mit der einfachen Funktion können wir zwar schon URLs auf Funktionen abbilden, oft möchte man aber eine ganze Klasse von URLs auf eine Funktion abbilden. Mithilfe des URL-Moduls könnte man zwar die URL noch um den Query-Teil bereinigen, aber wenn man alle URLs die mit einem bestimmten String anfangen, oder gar die einem bestimmten regulären Ausdruck entsprechen, abgebildet werden sollen kommen wir mit dem bisherigen Ansatz nicht weiter.

Um auch diesen Fall abzuhandeln können wir einen Array nutzen der Objekte enthält die jeweils ein Muster auf eine Funktion abbildet:

var pattern_mapping = [
  { pattern: '/echo/', mapped: echo },
  { pattern: /^\/regexp/, mapped: printMatch }
];

Möchten wir die richtige Funktion für eine URL finden so müssen alle Muster im Array durchprobieren. Da JavaScripts typeof-Operator leider reguläre Ausdrücke nicht eindeutig identifiziert akzeptieren wir alles welches die für unseren Test benötigte Methode aufweist:

for (i = 0; !mapped && i < pattern_mappings.length; i = i + 1) {
  pattern = pattern_mappings[i].pattern;
  console.log('Testing pattern ' + i + ': ' + pattern + ' [' + typeof(pattern) + ']');
  if( typeof(pattern) === 'string' && req.url.slice(0, pattern.length) === pattern ) {
    mapped = pattern_mappings[i].mapped;
  } else if ( pattern.test && typeof(pattern.test) === 'function' && pattern.test(req.url) ) {
    // we can only assume it's a regexp, as typeof is not clear on it,
    // but if it has a test function...
    mapped = pattern_mappings[i].mapped;
  }
}

Das ganze kann man natürlich noch beliebig aufbohren. Eine naheliegende Verbesserung wäre der aufgerufenen Funktion noch als dritten Parameter das abgebildete Muster zu übergeben. Ohne weitere Verbesserungen sieht die komplette Funktion so aus:

function(req, res, url_mappings, pattern_mappings) {
  // Forward decleration of variables as recommended by Crockford
  var i;
  var mapped;

  console.log('Request url: ' + req.url);

  // check whether the url has a direct mapping
  mapped = url_mappings[req.url];
  if (mapped) {
    mapped(req, res);
  } else {
    // url did not match any of the direct mappingss
    // check whether it matches anny of the patterns
    for (i = 0; !mapped && i < pattern_mappings.length; i = i + 1) {
      pattern = pattern_mappings[i].pattern;
      console.log('Testing pattern ' + i + ': ' + pattern + ' [' + typeof(pattern) + ']');
      if( typeof(pattern) === 'string' && req.url.slice(0, pattern.length) === pattern ) {
        mapped = pattern_mappings[i].mapped;
      } else if ( pattern.test && typeof(pattern.test) === 'function' && pattern.test(req.url) ) {
        // we can only assume it's a regexp, as typeof is not clear on it,
        // but if it has a test function...
        mapped = pattern_mappings[i].mapped;
      }
    }
    if (mapped) {
      mapped(req, res);
    } else {
      console.log(req.url + ' has not been mapped');
      invalidURL(req, res);
    }
  }
}

Zum einfacheren rumspielen mit dem Code gibt es das Beispiel als Datei: path.js