1. 第三次迭代

本节是 expross 的第三次迭代,主要的目标是继续完善路由系统,主要工作包括:

  • 完善路由其它接口,目前仅仅支持 GET 请求。
  • 完善路由系统的流程控制。

当前 expross 框架只支持 get 接口,具体的接口是由 application 对象提供的,函数内部调用了 Router.get 接口,而其内部又是对 Route.get 的封装。

HTTP 显然不仅仅只有 GET 这一个方法,还包括很多,例如:PUT、POST、DELETE 等等,每个方法都单独写一个处理函数显然是冗余的,因为函数的内容除了和函数名相关外,其它都是一成不变的。根据 JavaScript 脚本语言语言的特性,这里可以通过 HTTP 的方法列表动态生成函数内容。

想要动态生成函数,首先需要确定函数名称。函数名就是 nodejs 中 HTTP 服务器支持的方法名称,可以在官方文档中获取,具体参数是 http.METHODS。这个属性是在 v0.11.8 新增的,如果 nodejs 低于该版本,需要手动建立一个方法列表,具体可以参考 nodejs 代码。

Express 框架 HTTP 方法名的获取封装到另一个包,叫做 methods,内部给出了低版本的兼容动词列表。

function getBasicNodeMethods () {
  return [
    'get',
    'post',
    'put',
    'head',
    'delete',
    'options',
    'trace',
    'copy',
    'lock',
    'mkcol',
    'move',
    'purge',
    'propfind',
    'proppatch',
    'unlock',
    'report',
    'mkactivity',
    'checkout',
    'merge',
    'm-search',
    'notify',
    'subscribe',
    'unsubscribe',
    'patch',
    'search',
    'connect'
  ]
}

知道所支持的方法名列表数组后,剩下的只需要一个 for 循环生成所有的函数即可。

所有的动词处理函数的核心内容都在 Route 类中。

//不要忘记增加 http = require('http');
//不要忘记删除 Route.prototype.get

http.METHODS.forEach(function(method) {
    method = method.toLowerCase();
    Route.prototype[method] = function(fn) {
        var layer = new Layer('/', fn);
        layer.method = method;

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

        return this;
    };
});

接着修改 Router 类:

//不要忘记增加 http = require('http');
//不要忘记删除 Router.prototype.get

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

    	return this;
    };
});

最后修改 application.js 的内容。这里改动较大,重新定义了一个 Application 类,而不是使用字面量直接创建。

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

function Application() {
	this._router = new Router();
}


Application.prototype.listen = function(port, cb) {
	var self = this;

	var server = http.createServer(function(req, res) {
		self.handle(req, res);
	});

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


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

	var router = this._router;
	router.handle(req, res);
};

exports = module.exports = Application;

接着增加 HTTP 方法函数:

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

因为导出的是 Application 类,所以修改 expross.js 文件如下。

var Application = require('./application');

function createApplication() {
	var app = new Application();
	return app;
}

exports = module.exports = createApplication;

运行 node test/index.js,走起。

如果你仔细研究路由系统的源码,就会发现 Route 类设计的并没有想象中的那样美好,例如我们要增加两个相同路径不同方法的路由:

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

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

结果并不是想象中类似下面的结构:

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

而是如下的结构:

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

这显然不是我们想要的,Route 本身设计出来就是为了优化这种问题的,但是默认自然情况下添加路由的方式是无法实现理想中的结构。Express 给出的方案是使用下面的代码:

var router = express.Router();

router.route('/users/:user_id')
.get(function(req, res, next) {
  res.json(req.user);
})
.put(function(req, res, next) {
  // just an example of maybe updating the user
  req.user.name = req.params.name;
  // save user ... etc
  res.json(req.user);
})
.post(function(req, res, next) {
  next(new Error('not implemented'));
})
.delete(function(req, res, next) {
  next(new Error('not implemented'));
});

注意这里,如果你想要优化你的路由,你需要主动的使用 route 级联你路径相同的路由,只有这样才能达到我们想要的结果。这也是为什么在 Route 类中,添加方法的时候返回 this 的原因,目的是为了可以级联:

http.METHODS.forEach(function(method) {
    method = method.toLowerCase();
    Route.prototype[method] = function(fn) {
        var layer = new Layer('/', fn);
        layer.method = method;

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

		//这里返回 this,可以使用级联策略
        return this;
    };
});

知道了 Route 使用时的注意事项,接下来就要讨论路由的顺序问题。在路由系统中,路由的处理顺序非常重要,因为路由是按照数组的方式存储的,如果遇见两个同样的路由,同样的方法名,不同的处理函数,这时候前后声明的顺序将直接影响结果(这也是 Express 中间件存在顺序相关的原因),例如下面的例子:

app.get('/', function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('first');
});

app.get('/', function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('second');
});

上面的代码如果执行会发现永远都返回 first,但是有的时候会根据前台传来的参数动态判断是否执行接下来的路由(例如权限控制等),怎样才能跳过 first 进入 second?这就涉及到路由系统的流程控制问题。

流程控制分为主动和被动两种模式。

对于 expross 框架来说,路由绑定的处理逻辑、用户设置的路径参数这些都是不可靠的,在运行过程中很有可能会发生异常,被动流程控制就是当这些异常发生的时候,expross 框架要担负起捕获这些异常的工作,因为如果不明确异常的发生位置,会导致 JavaScript 代码无法继续运行,从而无法准确的报出故障。

主动流程控制则是处理函数内部的操作逻辑,以主动调用的方式来跳转路由内部的执行逻辑。

目前 Express 通过引入 next 参数的方式来解决流程控制问题。next 是处理函数的一个参数,其本身也是一个函数,该函数有几种使用方式:

  • 执行下一个处理函数。执行 next()。
  • 报告异常。执行 next(err)。
  • 跳过当前 Route,执行 Router 的下一项。执行 next('route')。
  • 跳过整个 Router。执行 next('router')。

接下来,我们尝试实现上面这些需求。

首先修改最底层的 Layer 对象,该对象的 handle_request 函数是负责调用路由绑定的处理逻辑,这里添加 next 参数,并且增加异常捕获功能。

Layer.prototype.handle_request = function (req, res, next) {
    var fn = this.handle;

    try {
        fn(req, res, next);
    } catch (err) {
        next(err);
    }
};

接下来修改 Route.dispath 函数。因为涉及到内部的逻辑跳转,使用 for 循环不太好控制,这里使用了类似递归的方式。

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

    function next(err) {
        //跳过route
        if(err && err === 'route') {
            return done();
        }

        //跳过整个路由系统
        if(err && err === 'router') {
            return done(err);
        }

        //越界
        if(idx >= stack.length) {
            return done(err);
        }

        //不等枚举下一个
        var layer = stack[idx++];
        if(method !== layer.method) {
            return next(err);
        }

        if(err) {
            //主动报错
            return done(err);
        } else {
            layer.handle_request(req, res, next);
        }
    }

    next();
};

整个处理过程本质上还是一个 for 循环,唯一的差别就是在处理函数中用户主动调用 next 函数的处理逻辑。

如果用户通过 next 函数返回错误、route 和 router 这三种情况,目前统一抛给 Router 处理。

因为修改了 dispatch 函数,所以调用该函数的 Router.route 函数也要修改,这回直接改彻底,以后无需根据参数的个数进行调整。

Router.prototype.route = function route(path) {
    ...
    
	//使用bind方式
    var layer = new Layer(path, route.dispatch.bind(route));
    
    ...
};

接着修改 Router.handle 的代码,逻辑和 Route.dispatch 类似。


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

	function next(err) {
		var layerError = (err === 'route' ? null : err);

		//跳过路由系统
		if(layerError === 'router') {
			return done(null);
		}

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

		var layer = stack[idx++];
		
		//匹配,执行
		if(layer.match(req.url) && layer.route &&
			layer.route._handles_method(method)) {
			return layer.handle_request(req, res, next);
		} else {
			next(layerError);
		}
	}

	next();
};

修改后的函数处理过程和原来的类似,不过有一点需要注意,当发生异常的时候,会将结果返回给上一层,而不是执行原有 this.stack 第 0 层的代码逻辑。

除了上面的修改,这里移除原有的 this.stack 的初始化代码,将:

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

改为:

this.stack = [];

我们已经将错误代码传递给了 application 这一层,所以要添加处理错误的代码,修改
Application.handle 函数如下:


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

	var done = function finalhandler(err) {
		res.writeHead(404, {
			'Content-Type': 'text/plain'
		});

		if(err) {
			res.end('404: ' + err);	
		} else {
			var msg = 'Cannot ' + req.method + ' ' + req.url;
			res.end(msg);	
		}
	};

	var router = this._router;
	router.handle(req, res, done);
};

这里简单的将 done 函数处理为返回 404 页面,其实在 Express 框架中,使用的是一个单独的 npm 包,叫做 finalhandler

简单的修改一下测试用例证明一下成果。

var expross = require('../');
var app = expross();

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

.get('/', function(req, res, next) {
	next(new Error('error'));
})

.get('/', function(req, res) {
	res.send('third');
});

app.listen(3000, function() {
	console.log('Example app listening on port 3000!');
});

运行 node test/index.js,访问 http://127.0.0.1:3000/,结果显示:

404: Error: error

貌似目前一切都很顺利,不过还有一个需求目前被忽略了。当前处理函数的异常全部是由框架捕获,返回的信息只能是固定的 404 页面,对于框架使用者显然很不方便,大多数时候,我们都希望可以捕获错误,并按照一定的信息封装返回给客户端,所以 expross 需要一个返回错误给上层使用者的接口。

目前和上层对接的处理函数的声明如下:

function process_fun(req, res, next) {
  
}

如果增加一个错误处理函数,按照 nodejs 的规则,第一个参数是错误信息,定义应该如下所示:

function process_err(err, req, res, next) {
  
}

因为两个声明的第一个参数信息是不同的,如果区分传入的处理函数是处理错误的函数还是处理正常的函数,这个是 expross 框架需要搞定的关键问题。

JavaScript 中,Function.length 属性可以获取传入函数指定的参数个数,这个可以当做区分二者的关键信息。

既然确定了原理,接下来直接在 Layer 类上增加一个专门处理错误的函数,和处理正常信息的函数区分开。


//错误处理
Layer.prototype.handle_error = function (error, req, res, next) {
    var fn = this.handle;

    //如果函数参数不是标准的4个参数,返回错误信息
    if (fn.length !== 4) {
        return next(error);
    }

    try {
        fn(error, req, res, next);
    } catch (err) {
        next(err);
    }
};

接着修改 Route.dispatch 函数。

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

    function next(err) {
    
		...

        if(err) {
            //主动报错
            layer.handle_error(err, req, res, next);
        } else {
            layer.handle_request(req, res, next);
        }
    }

    next();
};

当发生错误的时候,Route 会一直向后寻找错误处理函数,如果找到则返回,否则(越界)执行 done(err),将错误抛给 Router。

本节的内容基本上完成,包括 HTTP 相关的动作接口的添加、路由系统的流程跳转以及 Route 级别的错误响应等等,对于 Router.handle 的修改,因为涉及到一些中间件的概念,完整的错误处理将推移到下一节完成。