1. 第二次迭代

本节是 expross 的第二次迭代,主要的目的是构建一个初步的路由系统。根据上一节的改动,目前的路由是用一个 router 数组进行管理,对于 router 的操作有两个,分别是在添加路由的 application.get 函数和处理路由的 application.listen 函数。

按照面向对象的封装法则,接下来将路由系统的数据和路由系统的操作封装到一起定义一个 Router 类负责整个路由系统的主要工作。创建 router/index.js 文件:

var Router = function() {
	this.stack = [{
		path: '*',
		method: '*',
		handle: function(req, res) {
			res.writeHead(200, {
				'Content-Type': 'text/plain'
			});
			res.end('404');
		}
	}];
};


Router.prototype.get = function(path, fn) {
	this.stack.push({
		path: path,
		method: 'GET',
		handle: fn
	});
};


Router.prototype.handle = function(req, res) {
	for(var i=1,len=this.stack.length; i<len; i++) {
		if((req.url === this.stack[i].path || this.stack[i].path === '*') &&
			(req.method === this.stack[i].method || this.stack[i].method === '*')) {
			return this.stack[i].handle && this.stack[i].handle(req, res);
		}
	}

	return this.stack[0].handle && this.stack[0].handle(req, res);
};

exports = module.exports = Router;

修改原有的 application.js 文件的内容。

var http = require('http');
var Router = require('./router');


exports = module.exports = {
	_router: new Router(),

	get: function(path, fn) {
		return this._router.get(path, fn);
	},

	listen: function(port, cb) {
		var self = this;

		var server = http.createServer(function(req, res) {
			if(!res.send) {
				res.send = function(body) {
					res.writeHead(200, {
						'Content-Type': 'text/plain'
					});
					res.end(body);
				};
			}

			return self._router.handle(req, res);
		});

		return server.listen.apply(server, arguments);
	}
};

这样以后路由方面的操作只和 Router 本身有关,与 application 分离,使代码更加清晰。在进一步构建路由系统之前,我们需要再次分析一下路由的特性。

在当前的 expross 中,router 数组的每一项代表一项路由信息,包括路径、方法和处理函数三个部分。其中,前两者的关系是一对多的关系,如果用现在的方法存储路由信息,路由匹配的效率会逐步下降,特别是遵守类似 RESTful 风格的路由:

GET books/1
PUT books/1
DELETE books/1

上面三个路由分别代表读、改、删三种操作,但是它们对应的路径信息是一样的,如果用 router 数组管理,显然会产生一些冗余。随之而来的想法就是能否将这样一组路由汇聚到一起来提升匹配效率?这可能就是 Route 类诞生的原因之一(个人猜测)。

--------------
| Application  |                                 ---------------------------------------------------------
|     |        |        ----- -----------        |     0     |     1     |     2     |     3     |  ...  |
|     |-router | ----> |     | Layer     |       ---------------------------------------------------------
 --------------        |  0  |   |-path  |       | Layer     | Layer     | Layer     | Layer     |       |
  application          |     |   |-route | ----> |  |- method|  |- method|  |- method|  |- method|  ...  |
                       |-----|-----------|       |  |- handle|  |- handle|  |- handle|  |- handle|       |
                       |     | Layer     |       ---------------------------------------------------------
                       |  1  |   |-path  |                                  route
                       |     |   |-route |       
                       |-----|-----------|       
                       |     | Layer     |
                       |  2  |   |-path  |
                       |     |   |-route |
                       |-----|-----------|
                       | ... |   ...     |
                        ----- ----------- 
                             router

这张图代表了我们这一节代码的最终结果。这里从右向左分别介绍一下:

Route 是我们最新增加的东西,它用于存储一组路径相同的路由。Router 类是对上一节 router 数组的封装,其中数组的每一项被封装为一个 Layer 对象。Route 类内部同样存储着一个数组,而数组的每一项同样也是一个 Layer 对象,Layer 类被设计的很抽象,但是它是 Route 和 Router 的基础。

所以这里先实现 Layer 类。Layer 类其实代表着一个可执行层。它可以是一个专属路由、也可以是一个等待被执行的函数,这个函数在 Express 中被称为中间件。想要实现路由的基本功能,Layer 类必须包含几个成员:

  • path,用于记录路由匹配的路径,主要用在 Router 类中。
  • method,用于记录路由匹配的方法,主要用在 Route 类中。
  • handle,用于记录真正需要执行的代码。
  • route,用于记录指向的 Route 对象。

在 Express 中,如果 Layer 类中的 route 成员为空,则 Layer 代表着是一种中间件,其中 handle 记录着中间件的入口函数;如果 Layer 类中的 route 成员非空,则 route 指向需要匹配的路由,它是一个 Route 对象,handle 则记录着 Route 类的路由处理函数。这显然是一种几次迭代后精心设计的结果,值得学习感悟。

下面是 Layer 类的实现:

function Layer(path, fn) {
    this.handle = fn;
    this.name = fn.name || '<anonymous>';
    this.path = path;
}


//简单处理
Layer.prototype.handle_request = function (req, res) {
  var fn = this.handle;

  if(fn) {
      fn(req, res);
  }
};


//简单匹配
Layer.prototype.match = function (path) {
    if(path === this.path || path === '*') {
        return true;
    }
    
    return false;
};

exports = module.exports = Layer;

创建好 Layer 类,后我们更新一下 Router 类的实现,使用 Layer 替代 this.stack 的每一项:

var Layer = require('./layer.js');

var Router = function() {
	this.stack = [new Layer('*', function(req, res) {
		res.writeHead(200, {
			'Content-Type': 'text/plain'
		});
		res.end('404');		
	})];
};


Router.prototype.handle = function(req, res) {
	var self = this;

	for(var i=1,len=self.stack.length; i<len; i++) {
		if(self.stack[i].match(req.url)) {
			return self.stack[i].handle_request(req, res);
		}
	}

	return self.stack[0].handle_request(req, res);
};


Router.prototype.get = function(path, fn) {
	this.stack.push(new Layer(path, fn));
};

exports = module.exports = Router;

现在如果测试运行,则会导致无法区分路径相同但方法不同的路由,例如:

app.put('/', function(req, res) {
	res.send('put Hello World!');
});

app.get('/', function(req, res) {
	res.send('get Hello World!');
});

程序无法分清 PUT 和 GET 的区别。继续完善前文提到的 Route 类,它包含用于区分相同路径,不同请求方法的路由:

var Layer = require('./layer.js');

var Route = function(path) {
    this.path = path;
    this.stack = [];

    this.methods = {};
};

Route.prototype._handles_method = function(method) {
    var name = method.toLowerCase();
    return Boolean(this.methods[name]);
};

Route.prototype.get = function(fn) {
    var layer = new Layer('/', fn);
    layer.method = 'get';

    this.methods['get'] = true;
    this.stack.push(layer);

    return this;
};

Route.prototype.dispatch = function(req, res) {
    var self = this,
        method = req.method.toLowerCase();

    for(var i=0,len=self.stack.length; i<len; i++) {
        if(method === self.stack[i].method) {
            return self.stack[i].handle_request(req, res);
        }
    }
};

exports = module.exports = Route;

既然有了 Route 类,接下来修改原有的 Router 类,将 route 集成其中。

var Layer = require('./layer.js'),
	Route = require('./route.js');


var Router = function() {
	this.stack = [new Layer('*', function(req, res) {
		res.writeHead(200, {
			'Content-Type': 'text/plain'
		});
		res.end('404');		
	})];
};

Router.prototype.route = function route(path) {
    var route = new Route(path);

    var layer = new Layer(path, function(req, res) {
        route.dispatch(req, res);
    });

    layer.route = route;

    this.stack.push(layer);
    
    return route;
};

Router.prototype.handle = function(req, res) {
	var self = this,
	    method = req.method;

	for(var i=0,len=self.stack.length; i<len; i++) {
	    if(self.stack[i].match(req.url) && 
	        self.stack[i].route && self.stack[i].route._handles_method(method)) {
	        return self.stack[i].handle_request(req, res);
	    }
	}

	return self.stack[0].handle_request(req, res);
};

Router.prototype.get = function(path, fn) {
    var route = this.route(path);
    route.get(fn);

    return this;
};

exports = module.exports = Router;

运行 node test/index.js,一切看起来和原来一样。

这节内容主要是创建一个完整的路由系统,并在原始代码的基础上引入了 Layer 和 Route 两个概念,并修改了大量的代码,在结束本节前总结一下目前的信息。

首先,当前程序的目录结构如下:

expross
  |
  |-- lib
  |    | 
  |    |-- expross.js
  |    |-- application.js
  |    |-- router
  |          |
  |          |-- index.js
  |          |-- layer.js
  |          |-- route.js
  |
  |-- test
  |    |
  |    |-- index.js
  |
  |-- index.js

接着,总结一下当前 expross 各个部分的工作。

application 代表一个应用程序,expross 是一个工厂类负责创建 application 对象。Router 代表路由组件,负责应用程序的整个路由系统。组件内部由一个 Layer 数组构成,每个 Layer 对象代表一组路径相同的路由信息,具体信息存储在 Route 内部,每个 Route 内部也是一个 Layer 数组,但是 Route 内部的 Layer 和 Router 内部的 Layer 是存在一定的差异性。

  • Router 内部的 Layer,主要包含 pathroutehandle 属性。
  • Route 内部的 Layer,主要包含 methodhandle 属性。

如果一个请求来临,会先从头至尾的扫描 router 内部的每一层,而处理每层的时候会先对比 URI,相同则扫描 route 的每一项,匹配成功则返回具体的信息。如果所有路由全部扫描完毕,没有任何匹配则返回未找到。