其实关于这个话题之前已经提到过了, 也写过一篇关于express和koa对比的文章, 但是现在回过头看, 其实还是挺多错误的地方, 比如关于express和koa中间件原理的部分陷入了一个陷阱, 当时也研究了挺久但是没怎么理解. 关于这部分其实就是对于设计模式的欠缺了. 关于中间件模式我们不说那么多概念或者实现了, 针对代码说话.
柿子当然挑软的捏, express的代码量不算大, 但是有个更加简单的connect, 我们就从connect入手吧.
花了点时间画了个示意图, 但是之前没怎么画过代码流程图, 意思一下而已:
代码分析
首先我们看看connect是怎么使用的:
const connect = require('connect')
const app = connect()
app.use('/', function (req, res, next) {
console.log('全局中间件')
next()
console.log('执行完了')
})
app.use('/bar', function (req, res) {
console.log('第二个中间件')
res.end('end')
})
app.listen(8001)
跟express类似, 新建实例, 匹配路由, 很简洁也很有效. 上面代码执行访问后我们发现其实next后还是会回来执行下面的代码的, 似乎跟koa的中间件有点类似, 号称洋葱型中间件嘛. 结论是否定的, 反正这里不是与koa进行对比.
梳理一下代码结构吧:
var proto = {}
var createServer = function () {}
proto.use = function () {}
proto.handle = function () {}
proto.listen = function () {}
主要就是上面这几个函数, 其他辅助函数我们砍掉. 可以看到我们用connect主要就是在proto这块, 让我们根据代码来看我们启动一个connect服务器到底发生了哪些事情.
首先我们是新建一个connect实例:
var app = connect()
毫无疑问调用的是createServer
, 因为这个模块最终导出的就是它嘛, createServer
部分的代码也很简单:
function createServer() {
function app(req, res, next){ app.handle(req, res, next); }
merge(app, proto); // 继承了proto
merge(app, EventEmitter.prototype); // 继承了EventEmitter
app.route = '/';
app.stack = []; // 暂存路由和方法的地方
return app;
}
上面有用的部分我已经标出来了, 可以看出来其实我们那些常用的connect方法都来自proto, 那么我们下面主要工作就围绕着proto来.
app.use
当我们想设置某个路由的时候就是调用app.use
, 但是可能大家并不太清楚他具体做了什么事情, 比如下面的代码:
app.use('/bar', function (req, res) {
res.end('end')
})
上面已经讲了, 有个stack数组是专门用来存放路由和他的方法的, 很容易的就能想到: app.use
就是将我们想的路由和方法推进去等待执行, 实际上也是这样的:
proto.use = function use(route, fn) {
var handle = fn;
var path = route;
// default route to '/'
if (typeof route !== 'string') {
handle = route;
path = '/';
}
// wrap sub-apps
if (typeof handle.handle === 'function') {
var server = handle;
server.route = path;
handle = function (req, res, next) {
server.handle(req, res, next);
};
}
// wrap vanilla http.Servers
if (handle instanceof http.Server) {
handle = handle.listeners('request')[0];
}
// strip trailing slash
if (path[path.length - 1] === '/') {
path = path.slice(0, -1);
}
// add the middleware
debug('use %s %s', path || '/', handle.name || 'anonymous');
this.stack.push({ route: path, handle: handle });
return this;
};
看上去蛮复杂的, 我们简化一下, 不考虑各种异常以及兼容, 默认只能app.use(route, handle)
调用:
// 很好嘛 把if都给去掉了就是简化2333
proto.use = function use(route, fn) {
var handle = fn;
var path = route;
this.stack.push({ route: path, handle: handle });
return this;
};
简化后是不是顺眼多了, 其实就是维护数组, 当然这样肯定有问题的, 重复路由什么的就不管了.
中间件的实现
那use实现后其实我们就有点数了, 中间件现在都在stack里, 那我们执行中间件就是针对具体路由来遍历这个stack嘛, 对的, 就是遍历stack, 但是connect的中间件事顺序执行的, 如果一个个排下来就是所有中间件都会执行一遍, 可能的情况就是比如一个异常处理的中间件, 我只要在出现异常的时候才需要调用这个中间件.这时候next
就上场了, 首先来看看proto.handle实现的几十行代码吧:
proto.handle = function handle(req, res, out) {
var index = 0;
var protohost = getProtohost(req.url) || '';
var removed = '';
var slashAdded = false;
var stack = this.stack;
// final function handler
var done = out || finalhandler(req, res, {
env: env,
onerror: logerror
});
// store the original URL
req.originalUrl = req.originalUrl || req.url;
function next(err) {
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
if (removed.length !== 0) {
req.url = protohost + removed + req.url.substr(protohost.length);
removed = '';
}
// next callback
var layer = stack[index++];
// all done
if (!layer) {
defer(done, err);
return;
}
// route data
var path = parseUrl(req).pathname || '/';
var route = layer.route;
// skip this layer if the route doesn't match
if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
return next(err);
}
// skip if route match does not border "/", ".", or end
var c = path.length > route.length && path[route.length];
if (c && c !== '/' && c !== '.') {
return next(err);
}
// trim off the part of the url that matches the route
if (route.length !== 0 && route !== '/') {
removed = route;
req.url = protohost + req.url.substr(protohost.length + removed.length);
// ensure leading slash
if (!protohost && req.url[0] !== '/') {
req.url = '/' + req.url;
slashAdded = true;
}
}
// call the layer handle
call(layer.handle, route, err, req, res, next);
}
next();
};
还是挺长的, 需要简化, 同理我们把if都给去掉简化代码:
proto.handle = function handle(req, res, out) {
var index = 0;
var protohost = getProtohost(req.url) || '';
var removed = '';
var slashAdded = false;
var stack = this.stack;
// final function handler
var done = out || finalhandler(req, res, {
env: env,
onerror: logerror
});
// store the original URL
req.originalUrl = req.originalUrl || req.url;
function next(err) {
// next callback
var layer = stack[index++];
// all done 这个不能去
if (!layer) {
defer(done, err);
return;
}
// route data
var path = parseUrl(req).pathname || '/';
var route = layer.route;
// skip this layer if the route doesn't match
if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
return next(err);
}
// call the layer handle
call(layer.handle, route, err, req, res, next);
}
next();
};
简化后我们可以看到, 其实next是个递归, 只要符合条件它会不停地调用自身, 也就是说只要你在中间件里调用了next它会遍历stack寻找中间件如果找到了就执行, 如果没找到就defer(done), 注意proto.handle定义了一个index, 这是寻找中间件的一个索引, next一直需要用到. 这里无关紧要的函数就不提了, 比如getProtohost
, 比如call
.
app.listen
app.listen
其实也很简单了, 无法是新建一个http.Server
而已, 代码如下:
proto.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
结束
说到这里差不多快结束了, 我们其实可以知道, connect/express的中间件模型是这样的:
http.createServer(function (req, res) {
m1 (req, res) {
m2 (req, res) {
m3 (req, res) {}
}
}
})
当我们调用next的时候才会继续寻找中间件并调用. 这样写出来我自己好像也清楚了很多(逃
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。