1. 第四次迭代

本节是 expross 的第四次迭代,主要的目标是建立中间件机制并继续完善路由系统的功能。

在 Express 中,中间件其实是一个介于 web 请求来临后到调用处理函数前整个流程体系中间调用的组件。其本质是一个函数,内部可以访问修改请求和响应对象,并调整接下来的处理流程。

Express 官方给出的解释如下:

Express 是一个自身功能极简,完全是由路由和中间件构成一个的 web 开发框架:从本质上来说,一个 Express 应用就是在调用各种中间件。

中间件(Middleware) 是一个函数,它可以访问请求对象(request object (req)), 响应对象(response object (res)), 和 web 应用中处于请求-响应循环流程中的中间件,一般被命名为 next 的变量。

中间件的功能包括:

  • 执行任何代码。
  • 修改请求和响应对象。
  • 终结请求-响应循环。
  • 调用堆栈中的下一个中间件。

如果当前中间件没有终结请求-响应循环,则必须调用 next() 方法将控制权交给下一个中间件,否则请求就会挂起。

Express 应用可使用如下几种中间件:

使用可选则挂载路径,可在应用级别或路由级别装载中间件。另外,你还可以同时装载一系列中间件函数,从而在一个挂载点上创建一个子中间件栈。

官方给出的定义其实已经足够清晰,一个中间件的样式其实就是上一节所提到的处理函数,只不过并没有正式命名。所以对于代码来说 Router 类中的 this.stack 属性内部的每个 handle 都是一个中间件,根据使用接口不同区别了应用级中间件和路由级中间件,而四个参数的处理函数就是错误处理中间件。

接下来就给 expross 框架添加中间件的功能。

首先是应用级中间件,其使用方法是 Application 类上的两种方式:Application.useApplication.METHOD (HTTP 的各种请求方法),后者其实在前面的小节里已经实现了,前者则是需要新增的。

在前面的小节里的代码已经说明 Application.METHOD 内部其实是 Router.METHOD 的代理,Application.use 同样如此。

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

	router.use(path, fn);

	return this;
};

因为 Application.use 支持可选路径,所以需要增加处理路径的重载代码。

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

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

	router.use(path, fn);

	return this;
};

其实 Express 框架支持的参数不仅仅这两种,但是为了便于理解剔除了一些旁枝末节,便于框架的理解。

接下来实现 Router.use 函数:

Router.prototype.use = function(fn) {
	var path = '/';

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

	var layer = new Layer(path, fn);
	layer.route = undefined;

	this.stack.push(layer);

	return this;
};

内部代码和 Application.use 差不多,只不过最后不再是调用 Router.use,而是直接创建一个 Layer 对象,将其放到 this.stack 数组中。

在这里段代码里可以看到普通路由和中间件的区别。普通路由放到 Route 中,且 Router.route 属性指向 Route 对象,Router.handle 属性指向 Route.dispatch 函数;中间件的 Router.route 属性为 undefinedRouter.handle 指向中间件处理函数,被放到 Router.stack 数组中。

对于路由级中间件,首先按照要求导出 Router 类,便于使用。

exports.Router = Router;

上面的代码添加到 expross.js 文件中,这样就可以按照下面的使用方式创建一个单独的路由系统。

var app = express();
var router = express.Router();

router.use(function (req, res, next) {
  console.log('Time:', Date.now());
});

现在问题来了,如果像上面的代码一样创建一个新的路由系统是无法让路由系统内部的逻辑生效的,因为这个路由系统没法添加到现有的系统中。

一种办法是增加一个专门添加新路由系统的接口,这是完全是可行的,但是我更欣赏 Express 框架的办法,这可能是 Router 叫做路由级中间件的原因。Express 将 Router 定义成一个特殊的中间件,而不是一个单独的类。

这样将单独创建的路由系统添加到现有的应用中的代码非常简单通用:

var router = express.Router();

// 将路由挂载至应用
app.use('/', router);

这确实是一个好方法,现在就来将 expross 修改成类似的样子。

首先调整一下构造函数,使用 Object.setPrototypeOf 方法直接继承现有原型对象。

var proto = function() {
	function router(req, res, next) {
		router.handle(req, res, next);
	}

	Object.setPrototypeOf(router, proto);

	router.stack = [];
	return router;
};

然后将现有的 Router 方法转移到 proto 对象上。

proto.handle = function(req, res, done) {...};
proto.route = function route(path) {...};
proto.use = function(fn) { ... };

http.METHODS.forEach(function(method) {
    method = method.toLowerCase();
    proto[method] = function(path, fn) {
    	var route = this.route(path);
    	route[method].call(route, fn);

    	return this;
    };
});

module.exports = proto;

结果并不理想,原有的应用程序还有两个地方需要修改。首先是逻辑处理问题。原有的 Router.handle 函数中并没有处理中间件的情况,需要进一步修改。

proto.handle = function(req, res, done) {
	
	//...
	
	function next(err) {
		
		//...
		
		//注意这里,layer.route属性
		if(layer.match(req.url) && layer.route &&
			layer.route._handles_method(method)) {
			layer.handle_request(req, res, next);
		} else {
			next(layerError);
		}
	}

	next();
};

改为:

proto.handle = function(req, res, done) {

	//...

	function next(err) {
		
		//...
		
		//匹配,执行
		if(layer.match(req.url)) {
			if(!layer.route) {
				//处理中间件
				layer.handle_request(req, res, next);	
			} else if(layer.route._handles_method(method)) {
				//处理路由
				layer.handle_request(req, res, next);
			}	
		} else {
			next(layerError);
		}
	}

	next();
};

其次是路径匹配的问题。原有的单一路径被拆分成为不同中间的路径组合,这里判断需要多步进行,因为每个中间件只是匹配自己的路径是否通过,不过相对而言目前涉及的匹配都是全等匹配,还没有涉及到类似 Express 框架中的正则匹配,算是非常简单了。

想要实现匹配逻辑就要清楚的知道哪段路径和哪个处理函数匹配,这里定义三个变量:

  • req.originalUrl 原始请求路径。
  • req.url 当前路径。
  • req.baseUrl 父路径。

主要修改 proto.handle 函数,该函数主要负责提取当前路径段,便于和事先传入的路径进行匹配。

这段代码主要处理两种情况:

第一种,存在路由中间件的情况。如:

router.use('/1', function(req, res, next) {
	res.send('first user');
});

router.use('/2', function(req, res, next) {
	res.send('second user');
});

app.use('/users', router);

这种情况下,Router.handle 顺序匹配到中间的时候,会递归调用 Router.handle,所以需要保存当前的路径快照,具体路径相关信息放到 req.urlreq.originalUrlreq.baseUrl 这三个参数中。

第二种,非路由中间件的情况。如:

app.get('/', function(req, res, next) {
	res.send('home');
});

app.get('/books', function(req, res, next) {
	res.send('books');
});

这种情况下,Router.handle 内部主要是按照栈中的次序匹配路径即可。

改好了处理函数,还需要修改一下 Layer.match 这个匹配函数。目前创建 Layer 可能会有三种情况:

  • 不含有路径的中间件。path 属性默认为 /
  • 含有路径的中间件。
  • 普通路由。如果 path 属性为 *,表示任意路径。

修改原有 Layer 构造函数,增加一个 fast_star 标记用来判断 path 是否为 *

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

    //是否为*
    this.fast_star = (path === '*' ? true : false);
    if(!this.fast_star) {
        this.path = path;
    }
}

接着修改 Layer.match 匹配函数:

Layer.prototype.match = function(path) {

  //如果为*,匹配
  if(this.fast_star) {
    this.path = '';
    return true;
  }

  //如果是普通路由,从后匹配
  if(this.route && this.path === path.slice(-this.path.length)) {
    return true;
  }

  if (!this.route) {
    //不带路径的中间件
    if (this.path === '/') {
      this.path = '';
      return true;
    }

    //带路径中间件
    if(this.path === path.slice(0, this.path.length)) {
      return true;
    }
  }
  
  return false;
};

代码中一共判断四种情况,根据 this.route 区分中间件和普通路由,然后分开判断。

Express 除了普通的中间件外还要一种错误中间件,专门用来处理错误信息。该中间件的声明和上一小节最后介绍的错误处理函数是一样的,同样是四个参数分别是:errreqresnext

目前 Router.handle 中,当遇见错误信息的时候,会直接通过回调函数返回错误信息,显示错误页面。

if(idx >= stack.length || layerError) {
    return done(layerError);
}

这里需要修改策略,如果增加 layerError 判断,则会导致流程直接终止。所以需要移除 layerError 条件,将错误判断后移:


proto.handle = function(req, res, done) {
	
	//...

	function next(err) {

		//...

		//没有找到
		if(idx >= stack.length) {
			return done(layerError);
		}

		//...

		//匹配,执行
		if(layer.match(path)) {

			//处理中间件
			if(!layer.route) {

				//...

				//判断是否发生错误,主动调用人工设置的错误函数
				if(layerError)
					layer.handle_error(layerError, req, res, next);
				else
					layer.handle_request(req, res, next);
					
			} else if(layer.route._handles_method(method)) {
				//处理路由
				layer.handle_request(req, res, next);
			}	
		} else {
			next(layerError);
		}
	}

	next();
};

到此为止,expross 的错误处理部分算是基本完成了。

路由系统和中间件两个大的概念算是全部讲解完毕,当然还有很多细节没有完善,在剩下的文字里如果有必要会继续完善。

下一节主要的内容是介绍前后端交互的两个重要成员:request 和 response。Express 在 nodejs 的基础之上进行了丰富的扩展,所以很有必要仿制一下。