提到 Node.js 开发,不得不提目前炙手可热的2大框架 Express 和 Koa。
Express 是一个保持最小规模的灵活的 Node.js Web 应用程序开发框架,为 Web 和移动应用程序提供一组强大的功能。目前使用人数众多。
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。
相信对这两大框架有一些了解的人都或多或少的会了解其中间件机制,Express 为线型模型,而 Koa 则为洋葱型模型。这个系列的博客主要讲解 Express 和 Koa 的中间件机制,本篇将主要讲解 Express 的中间件机制。
Express 中间件
connect 曾经是 express 3.x 之前的核心,而 express 4.x 已经把 connect 移除,在 express 中自己实现了 connect 的接口,所以我们本篇中的源码解释将直接使用 connect 源码。
示例
下面将使用 Express 实现一个简单的 demo 来进行中间件机制的讲解
var express = require('express');
var app = express();
app.use(function (req, res, next) {
console.log('第一个中间件start');
setTimeout(() => {
next();
}, 1000)
console.log('第一个中间件end');
});
app.use(function (req, res, next) {
console.log('第二个中间件start');
setTimeout(() => {
next();
}, 1000)
console.log('第二个中间件end');
});
app.use('/foo', function (req, res, next) {
console.log('接口逻辑start');
next();
console.log('接口逻辑end');
});
app.listen(4000);
此时的输出比较符合我们对 Express 线性的理解,其输出为
第一个中间件start
第一个中间件end
第二个中间件start
第二个中间件end
接口逻辑start
接口逻辑end
但是,如果我们取消掉中间件内部的异步处理直接调用 next()
var express = require('express');
var app = express();
app.use(function (req, res, next) {
console.log('第一个中间件start');
next()
console.log('第一个中间件end');
});
app.use(function (req, res, next) {
console.log('第二个中间件start');
next()
console.log('第二个中间件end');
});
app.use('/foo', function (req, res, next) {
console.log('接口逻辑start');
next();
console.log('接口逻辑end');
});
app.listen(4000);
输出结果为
第一个中间件start
第二个中间件start
接口逻辑start
接口逻辑end
第二个中间件end
第一个中间件end
这种结果不是和 Koa 的输出很相似吗?是的,但是它和剥洋葱模型还是不一样的,其实这种输出的结果是由于代码的同步运行导致的,并不是说 Express 不是线性的模型。
当我们的中间件内没有进行异步操作时,其实我们的代码最后是以下面这种方式运行的
app.use(function middleware1(req, res, next) {
console.log('第一个中间件start')
// next()
(function (req, res, next) {
console.log('第二个中间件start')
// next()
(function (req, res, next) {
console.log('接口逻辑start')
// next()
(function handler(req, res, next) {
// do something
})()
console.log('接口逻辑end')
})()
console.log('第二个中间件end')
})()
console.log('第一个中间件end')
})
导致这种运行方式的其实就是 connect 的实现方式,接下来我们进行其源码的解析
connect 源码解析
connect的源码仅有200多行,但是这里讲解只选择其中部分核心代码,并非完整代码,查看全部代码请移步 github
中间件的挂载主要依赖 proto.use 和 proto.handle,这里我们删掉部分 if 判断以使我们更专注于其内部原理的实现
proto.use = function use(route, fn) {
var handle = fn;
var path = route;
// 这里是对直接填入回调函数的进行容错处理
// default route to '/'
if (typeof route !== 'string') {
handle = route;
path = '/';
}
.
.
.
this.stack.push({ route: path, handle: handle });
return this;
};
proto.use 主要将我们需要挂载的中间件存储在其自身 stack 属性上,同时进行部分兼容处理,这一块比较容易理解。其中间件机制的核心为 proto.handle 内部 next 方法的实现。
proto.handle = function handle(req, res, out) {
var index = 0;
var stack = this.stack;
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();
};
在删除到部分非核心代码后,可以清晰的看到,proto.handle 的核心就是 next 方法的实现和递归调用,对存在于 stack 中的中间件取出、执行。
这里便可以解释上文中异步和非异步过程中所输出的结果的差异了。
- 当有异步代码时,将会直接跳过继续执行,此时的 next 方法并未执行,需要等待当前队列中的事件全部执行完毕,所以此时我们输出的数据是线性的。
- 当 next 方法直接执行时,本质上所有的代码都已经为同步,所以层层嵌套,最外层的肯定会在最后,输出了类似剥洋葱模型的结果。
总结
connect 的实现其基本原理是维护一个 stack 数组,将所需要挂载的中间件处理后全部 push 到数组内,之后在数组内循环执行 next 方法,直至所有中间件挂载完毕,当然这过程中会做一些异常、兼容等的处理。