简介
个人理解的中间件:中间件的本质就是一个函数,在收到请求和返回相应的过程中做一些我们想做的事情。 express 官方:
- 执行任何代码。
- 修改请求和响应对象。
- 终结请求-响应循环。
- 调用堆栈中的下一个中间件。
express 中间件
中间件比较出彩的应用在 Koa,Redux,Express 中,我的偶像阿宝哥有写一篇从 Koa 更好的理解中间件和洋葱模型的文章,所以这次我打算主要从 Express 来理解中间件。 首先我们下载 express 源码 ,我们可以通过命令行下载
$ npm install express --save
express 中间件的类别
- 应用级中间件
- 路由级中间件
- 错误处理中间件
- 内置中间件
- 第三方中间件
express 中间件的初使用
我们来看一个简单的使用示例,这个例子监听了3000端口,我们通过 use 方法,给它增加了3个中间件。
var express = require("express");
var app = express();
app.listen(3000, function () {
console.log("listen 3000...");
});
function middlewareA(req, res, next) {
console.log("middlewareA before next()");
next();
console.log("middlewareA after next()");
}
function middlewareB(req, res, next) {
console.log("middlewareB before next()");
next();
console.log("middlewareB after next()");
}
function middlewareC(req, res, next) {
console.log("middlewareC before next()");
next();
console.log("middlewareC after next()");
}
app.use(middlewareA);
app.use(middlewareB);
app.use(middlewareC);
运行结果 我们发现执行完一个中间件,走到 next() 后,会跳过之后语句,执行下一个中间件,当所有执行完毕后,又会反向的执行 next() 之后的语句。 通过上图,我们可以很清晰的看到中间件是什么,可以应用在哪些场景。通过中间件,我们可以精确的控制某个执行操作的前置和后置操作,例如 axios 发送请求前的处理,接收请求后的处理。 那么 express 是如何做到的这种任务执行方式的呢?
express 中间件的实现原理
首先我们先输出这样的一个概念:Layer (中间件)实例是 path 和 handle 互相映射的实体,每一个 **Layer **便是一个中间件。 然后我们看一下 express 中间件的初始化 通过上图我们发现,中间件中间可能嵌套中间件,面对这种情况,express 就在 Layer 中做了处理,我们分两种情况挂载中间件。
- 通过使用
app.use
,router.use
来挂载。- app.use 经过处理以后,最终也是调用
router.use
。
- app.use 经过处理以后,最终也是调用
- 使用
app.[Http Method]
、app.route
、router.all
、router.[Http Method]
、router.route
来挂载的。app.all
、app.[Http Method]
、app.route
、router.all
、router.[Http Method]
经过一系列处理之后最终也是调用router.route
的。
所以我们主要研究 router.use
和 router.route
router.use
我们在 router/index.js 中看见定义
proto.use = function use(fn) {
var offset = 0;
var path = '/';
if (typeof fn !== 'function') {
......
}
if (typeof arg !== 'function') {
......
}
}
var callbacks = flatten(slice.call(arguments, offset));
if (callbacks.length === 0) {
throw new TypeError('Router.use() requires a middleware function')
}
for (var i = 0; i < callbacks.length; i++) {
var fn = callbacks[i];
if (typeof fn !== 'function') {
......
}
debug('use %o %s', path, fn.name || '<anonymous>')
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
layer.route = undefined;
this.stack.push(layer);
}
return this;
};
小结:没有深层嵌套的中间件,会使用 router.use 进行注册。
router.route
proto.route = function route(path) {
var route = new Route(path);
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer);
return route;
};
小结:没有深层嵌套的中间件,会使用 router.route进行注册。 我们发现不管是哪种方法,最后都会往 stack数组中添加我们的中间件。 当事件触发后 app.handle 会对 stack 数组进行处理
app.handle = function(req, res, callback) {
var stack = this.stack;
var idx = 0;
function next(err) {
if (idx >= stack.length) {
callback('err')
return;
}
var mid;
while(idx < stack.length) {
mid = stack[idx++];
mid(req, res, next);
}
}
next()
}
通过尾递归调用,next 方法不断取出 stack 中的中间件进行调用,并且将自身传递给中间件作为第三个参数,每个中间件的约定固定格式为 (req, res, next) =>{}
,这样每个中间件函数只要调用了 next 方法,即可调用下一个中间件,关于尾调用有兴趣的可以阅读下阮老师的尾调用优化。
关于next()后是如何触发,还没搞清楚。
整体流程就是中间件注册后添加进 stack 数组中,当触发时,进行递归调用, 一个触发一个
koa 里的中间件
koa 相比 express 而言,代码更加高级精炼,代码基于 ES6 实现,支持 generator(async, await)
没有内置路由实现和任何内置中间件, context 的设计也很是巧妙。
中间件的处理
- 构造函数
constructor
中维护全局中间件数组this.middleware
和全局的this.context
实例(源码中还有request,response对象和一些其他辅助属性)。 express
不同,因为没有router
的实现,所有this.middleware
中就是普通的”中间件“函数而非复杂的layer
实例
koa 中的任务编排
koa中任务执行的顺序是通过把前置执行和后置执行,放在 await next() 的前后完成任务编排,应用了 async await 的执行顺序的原理。
// 统计请求处理时长的中间件
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
koa 中的任务调度
通过之前了解,我们知道中间件统一放在 middleware 数组中保存,为了完成任务,我们就要将他一个一个拿出来执行,相关的调度算法在 koa-compose 下的compose 函数中。
function compose(middleware) {
// 省略部分代码
return function (context, next) {
// last called middleware #
let index = -1;
return dispatch(0);
function dispatch(i) {
if (i <= index)
return Promise.reject(new Error("next() called multiple times"));
index = i;
let fn = middleware[i];
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}
看起来和 express 的 next 有点像,但是因为支持异步,所以使用的是 promise 形式的,中间件async(ctx, next) =>{}
每一个执行都是返回一个 promise ,第二个参数的值dispatch.bind(null, i+1)
,所以中间件会一个一个向下执行,当最后一个执行完毕,resolve掉,然后开始执行 await 队列中的方法,最终使最外层的 promise resolve掉。
小结:
这里和 express 最大的不同点就是 koa 的响应处理是在中间件执行完,返回 promise resolve后。
我们看一下 koa 的 application 中的部分代码
// application.js 中部分代码
constructor() {
super()
this.middleware = []
this.context = Object.create(context)
}
use(fn) {
this.middleware.push(fn)
}
listen(...args) {
debug('listen')
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback() {
// 这里即中间件处理代码
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
// ctx 是koa的精髓之一, req, res上的很多方法代理到了ctx上, 基于 ctx 很多问题处理更加方便
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
ctx.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
我们发现,监听触发以后,会执行 callback ,这里会处理中间件代码,处理完毕以后会执行 handleRequest,通过 handleResponse
最后对响应做处理,”中间件“会设置ctx.body
, handleResponse
也会主要处理 ctx.body
,所以 koa
的”洋葱圈“模型才会成立,await next()
后的代码也会影响到最后的响应。
洋葱模型
小结:我们通过express 和 koa 理解了如何实现一个洋葱模型的任务编排和调度。