1. 第五次迭代

本节是 expross 的第五次迭代,主要的目标是封装 request 和 response 两个对象,方便使用。

其实 nodejs 已经给我们提供这两个默认的对象,之所以要封装是因为丰富一下二者的接口,方便框架使用者,目前框架在 response 对象上已经有一个接口:

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

如果需要继续封装,也要类似的结构在代码上添加显然会给人一种很乱的感觉,因为 request 和 response 的原始版本是 nodejs 提供给框架的,框架获取到的是两个对象,并不是类,要想在二者之上提供另一组接口的办法有很多,归根结底就是将新的接口加到该对象上或者加到该对象的原型链上,目前的代码选择了前者,Express 的代码选择了后者。

首先建立两个文件:request.jsresponse.js,二者分别导出 reqres 对象。

//request.js
var http = require('http');

var req = Object.create(http.IncomingMessage.prototype);

module.exports = req;


//response.js
var http = require('http');

var res = Object.create(http.ServerResponse.prototype);

module.exports = res;

二者文件的代码都是创建一个对象,分别指向 nodejs 提供的 request 和 response 两个对象的原型,以后 expross 自定的接口可以统一挂载到这两个对象上。

接着修改 Application.handle 函数,因为这个函数里面有新鲜出炉的 request 和 response。思路很简单,就是将二者的原型指向我们自建的 reqres。因为 reqres 对象的原型和 request 和 response 的原型相同,所以并不影响原有 nodejs 的接口。

var request = require('./request');
var response = require('./response');

...

Application.prototype.handle = function(req, res) {

	Object.setPrototypeOf(req, request);
	Object.setPrototypeOf(res, response);


	...
};

这里将原有的 res.send 转移到了 response.js 文件中。

res.send = function(body) {
	this.writeHead(200, {
		'Content-Type': 'text/plain'
	});
	this.end(body);
};

注意函数中不再是 res.writeHeadres.end,而是 this.writeHeadthis.end

在整个路由系统中,Router.stack 每一项其实都是一个中间件,每个中间件都有可能用到 reqres 这两个对象,所以代码中修改 nodejs 原生提供的 request 和 response 对象的代码放到了 Application.handle 中,这样做并没有问题,但是有一种更好的方式,expross 框架将这部分代码封装成了一个内部中间件。

为了确保框架中每个中间件接收这两个参数的正确性,需要将该内部中间放到 Router.stack 的首项。这里将原有 Application 的构造函数中的代码去掉,不再是直接创建 Router() 路由系统,而是用一种惰性加载的方式,动态创建。

去除原有 Application 构造函数的代码。

function Application() {}

添加惰性初始化函数。

var middleware = require('./middleware/init');

Application.prototype.lazyrouter = function() {
	if(!this._router) {
		this._router = new Router();

		this._router.use(middleware.init);
	}
};

因为是惰性初始化,所以在使用 this._router 对象前,一定要先调用该函数动态创建 this._router 对象。类似如下代码:

//获取router
this.lazyrouter();
router = this._router;

当前代码一共有两处需要添加,一个添加中间的 Application.prototype.use 函数:

Application.prototype.use = function(fn) {
	var path = '/',
		router;

	//获取router
	this.lazyrouter();
	router = this._router;

	//路径挂载
	if(typeof fn !== 'function') {
		path = fn;
		fn = arguments[1];
	}

	router.use(path, fn);

	return this;
};

另一处是添加普通路由的函数:

http.METHODS.forEach(function(method) {
    method = method.toLowerCase();
    Application.prototype[method] = function(path, fn) {
		this.lazyrouter();
		
    	this._router[method].apply(this._router, arguments);
    	return this;
    };
});

接下来创建一个叫 middleware 文件夹,专门放内部中间件的文件,再创建一个 init.js 文件,放置 Application.handle 中用来初始化 resreq 的代码。

var request = require('../request');
var response = require('../response');

//不要忘记移除 Application.handle 中的两行代码!!!

exports.init = function expressInit(req, res, next) {
	//request文件可能用到res对象
	req.res = res;

	//response文件可能用到req对象
	res.req = req;

	//修改原始req和res原型
	Object.setPrototypeOf(req, request);
	Object.setPrototypeOf(res, response);

	//继续
	next();
};

修改原有的 Applicaton.handle 函数。

Application.prototype.handle = function(req, res) {

	...

	// 这里无需调用lazyrouter,因为listen前一定调用了 .use 或者 .METHODS 方法。
	// 如果二者都没有调用,没有必要创建路由系统。this._router 为 undefined。
	var router = this._router;
	if(router) {
		router.handle(req, res, done);
	} else {
		done();
	}
};

运行 node test/index.js 走起……

Express 框架中,request 和 response 两个对象有很多非常好用的函数,不过大部分和框架结构无关,并且这些函数内部过于专研细节,对框架本身的理解没有多少帮助。不过接下来有一个方面需要仔细研究一下,那就是前后端参数的传递,Express 如何获取并分类这些参数的,这一点还是需要略微了解。

默认情况下,一共有三种参数获取方式。

  • req.query 代表查询字符串。
  • req.params 代表路径变量。
  • req.body 代表表单提交变量。

req.query 是最常用的方式,例如:

// GET /search?q=tobi+ferret
req.query.q
// => "tobi ferret"

// GET /shoes?order=desc&shoe[color]=blue&shoe[type]=converse
req.query.order
// => "desc"

req.query.shoe.color
// => "blue"

req.query.shoe.type
// => "converse"

后台获取这些参数最简单的方式就是通过 nodejs 自带的 querystring 模块分析 URL。Express 使用的是另一个 npm 包:qs。并且将其封装为另一个内部中间件,专门负责解析查询字符串,默认加载。

req.params 是另一种从URL获取参数的方式,例如:

//路由规则  /user/:name
// GET /user/tj
req.params.name
// => "tj"

这是一种 Express 框架规定的参数获取方式,对于批量处理逻辑非常实用。在 expross 中并没有实现,因为路径匹配问题过于细节化,如果对此感兴趣可以研究研究 path-to-regexp 模块,Express 也是在其上的封装。

req.body 是获取表单数据的方式,但是默认情况下框架是不会去解析这种数据,直接使用只会返回 undefined。如果想要支持需要添加其他中间件,例如 body-parsermulter

本小节主要介绍了 request 和 response 两个对象,并且讲解了一下现有 expross 框架中获取参数的方式,整体上并没有太深入的仿制,主要是这方面内容涉及的细节过多,过于复杂,本质上是一些工具函数的使用方式,知道了就是知道了,感觉没啥太大的帮助,除非重头再造一次轮子。