路由给真正的请求处理程序

回到正题,现在我们的HTTP服务器和请求路由模块已经如我们的期望,可以相互交流了,就像一对亲密无间的兄弟。

当然这还远远不够,路由,顾名思义,是指我们要针对不同的URL有不同的处理方式。例如处理*/start* 的“业务逻辑”就应该和处理*/upload* 的不同。

在现在的实现下,路由过程会在路由模块中“结束”,并且路由模块并不是真正针对请求“采取行动”的模块,否则当我们的应用程序变得更为复杂时,将无法很好地扩展。

我们暂时把作为路由目标的函数称为请求处理程序。现在我们不要急着来开发路由模块,因为如果请求处理程序没有就绪的话,再怎么完善路由模块也没有多大意义。

应用程序需要新的部件,因此加入新的模块 -- 已经无需为此感到新奇了。我们来创建一个叫做requestHandlers的模块,并对于每一个请求处理程序,添加一个占位用函数,随后将这些函数作为模块的方法导出:

function start() {
  console.log("Request handler 'start' was called.");
}
function upload() {
  console.log("Request handler 'upload' was called.");
}
exports.start = start;
exports.upload = upload;

这样我们就可以把请求处理程序和路由模块连接起来,让路由“有路可寻”。

在这里我们得做个决定:是将requestHandlers模块硬编码到路由里来使用,还是再添加一点依赖注入?虽然和其他模式一样,依赖注入不应该仅仅为使用而使用,但在现在这个情况下,使用依赖注入可以让路由和请求处理程序之间的耦合更加松散,也因此能让路由的重用性更高。

这意味着我们得将请求处理程序从服务器传递到路由中,但感觉上这么做更离谱了,我们得一路把这堆请求处理程序从我们的主文件传递到服务器中,再将之从服务器传递到路由。

那么我们要怎么传递这些请求处理程序呢?别看现在我们只有2个处理程序,在一个真实的应用中,请求处理程序的数量会不断增加,我们当然不想每次有一个新的URL或请求处理程序时,都要为了在路由里完成请求到处理程序的映射而反复折腾。除此之外,在路由里有一大堆if request == x then call handler y 也使得系统丑陋不堪。

仔细想想,有一大堆东西,每个都要映射到一个字符串(就是请求的URL)上?似乎关联数组(associative array)能完美胜任。

不过结果有点令人失望,JavaScript没提供关联数组 -- 也可以说它提供了?事实上,在JavaScript中,真正能提供此类功能的是它的对象。

在这方面,http://msdn.microsoft.com/en-us/magazine/cc163419.aspx有一个不错的介绍,我在此摘录一段:

在C++或C#中,当我们谈到对象,指的是类或者结构体的实例。对象根据他们实例化的模板(就是所谓的类),会拥有不同的属性和方法。但在JavaScript里对象不是这个概念。在JavaScript中,对象就是一个键/值对的集合 -- 你可以把JavaScript的对象想象成一个键为字符串类型的字典。

但如果JavaScript的对象仅仅是键/值对的集合,它又怎么会拥有方法呢?好吧,这里的值可以是字符串、数字或者……函数!

好了,最后再回到代码上来。现在我们已经确定将一系列请求处理程序通过一个对象来传递,并且需要使用松耦合的方式将这个对象注入到route() 函数中。

我们先将这个对象引入到主文件index.js 中:

var server = require("./server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");
var handle = {}
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers.upload;
server.start(router.route, handle);

虽然handle 并不仅仅是一个“东西”(一些请求处理程序的集合),我还是建议以一个动词作为其命名,这样做可以让我们在路由中使用更流畅的表达式,稍后会有说明。

正如所见,将不同的URL映射到相同的请求处理程序上是很容易的:只要在对象中添加一个键为*"/"* 的属性,对应requestHandlers.start 即可,这样我们就可以干净简洁地配置*/start* 和*/* 的请求都交由start 这一处理程序处理。

在完成了对象的定义后,我们把它作为额外的参数传递给服务器,为此将server.js 修改如下:

var http = require("http");
var url = require("url");
function start(route, handle) {
  function onRequest(request, response) {
    var pathname = url.parse(request.url).pathname;
    console.log("Request for " + pathname + " received.");
    route(handle, pathname);
    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write("Hello World");
    response.end();
  }
  http.createServer(onRequest).listen(8888);
  console.log("Server has started.");
}
exports.start = start;

这样我们就在start() 函数里添加了handle 参数,并且把handle对象作为第一个参数传递给了route() 回调函数。

然后我们相应地在route.js 文件中修改route() 函数:

function route(handle, pathname) {
  console.log("About to route a request for " + pathname);
  if (typeof handle[pathname] === 'function') {
    handle[pathname]();
  } else {
    console.log("No request handler found for " + pathname);
  }
}
exports.route = route;

通过以上代码,我们首先检查给定的路径对应的请求处理程序是否存在,如果存在的话直接调用相应的函数。我们可以用从关联数组中获取元素一样的方式从传递的对象中获取请求处理函数,因此就有了简洁流畅的形如handlepathname; 的表达式,这个感觉就像在前方中提到的那样:“嗨,请帮我处理了这个路径”。

有了这些,我们就把服务器、路由和请求处理程序在一起了。现在我们启动应用程序并在浏览器中访问http://localhost:8888/start ,以下日志可以说明系统调用了正确的请求处理程序:

Server has started.
Request for /start received.
About to route a request for /start
Request handler 'start' was called.

并且在浏览器中打开http://localhost:8888/ 可以看到这个请求同样被start 请求处理程序处理了:

Request for / received.
About to route a request for /
Request handler 'start' was called.