介绍
如果你使用过redux或者nodejs,那么你对“中间件”这个词一定不会感到陌生,如果没用过这些也没关系,也可以通过这个来了解javascript中的事件流程。
一个例子
有一类人,非常的懒(比如说我),只有三种行为动作,sleep
,eat
,sleepFirst
,伪代码就是:
var wang = new LazyMan('王大锤');
wang.eat('苹果').eat('香蕉').sleep(5).eat('葡糖').eat('橘子').sleepFirst(2);
//等同于以下的代码
const wang = new LazyMan('王大锤');
wang.eat('苹果');
wang.eat('香蕉');
wang.sleep(5);
wang.eat('葡糖');
wang.eat('橘子');
wang.sleepFirst(2);
执行结果如下图:
但是javascript只有一个线程,也并没有像php的sleep
的那种方法。实现的思路就是eat、sleep、sleepFirst这些事件放在任务列中,通过next去依次执行方法。我还是希望在看源码前先手动实现一下试试看,其实这就是个lazyMan的实现。
下面是我的实现方式:
class lazyMan{
constructor(name) {
this.tasks = [];
const first = () => {
console.log(`my name is ${name}`);
this.next();
}
this.tasks.push(first);
setTimeout(()=>this.next(), 0);
}
next() {
const task = this.tasks.shift();
task && task();
}
eat(food) {
const eat = () => {
console.log(`eat ${food}`);
this.next();
};
this.tasks.push(eat);
return this;
}
sleep(time) {
const newTime = time * 1000;
const sleep = () => {
console.log(`sleep ${time}s!`);
setTimeout(() => {
this.next();
}, newTime);
};
this.tasks.push(sleep);
return this;
}
sleepFirst(time) {
const newTime = time * 1000;
const sleepzFirst = () => {
console.log(`sleep ${time}s first!`);
setTimeout(() => {
this.next();
}, newTime);
};
this.tasks.unshift(sleepzFirst);
return this;
}
}
const aLazy = new lazyMan('王大锤');
aLazy.eat('苹果').eat('香蕉').sleep(5).eat('葡萄').eat('橘子').sleepFirst(2)
我们上面说过
wang.eat('苹果').eat('香蕉').sleep(5).eat('葡糖').eat('橘子').sleepFirst(2);
//等同于以下的代码
wang.eat('苹果');
wang.eat('香蕉');
wang.sleep(5);
wang.eat('葡糖');
wang.eat('橘子');
wang.sleepFirst(2);
如果你使用过过node,你会发现,这种写法似乎有点熟悉的感觉,我们来看一下一个koa2(一个node的框架)项目的主文件:
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const cors = require('koa-cors2');
const routers = require('./src/routers/index')
const app = new Koa();
app.use(cors());
app.use(bodyParser());
app.use(routers.routes()).use(routers.allowedMethods())
app.listen(3000);
有没有发现结构有一点像?
koa中的中间件
废话不多说,直接看源码...
app.use就是用来注册中间件的,我们先看use的实现:
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
先解释一下里面做了什么处理,fn
就是传入的函数,首先肯定要判断是否是个函数,如果不是,抛出错误,其次是判断fn
是否是一个GeneratorFunction
,我用的是koa2,koa2中用async
、await
来替代koa1中的generator
,如果判断是生成器函数,证明使用或者书写的中间件为koa1的,koa2中提供了库koa-convert
来帮你把koa1中的中间件转换为koa2中的中间件,这里如果判断出是koa1的中间件会给你提醒,这里会主动帮你转换,就是代码中的convert
方法。如果验证没出现问题,就注册这个中间件并放到中间件数组中。
这里我们只看到了把中间件加到数组中,然后就没有做其他处理了。
我们再看koa2中listen
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
这里只是启动了个server,然后传进了一个回调函数的结果,我们看原生启动一个server大概是什么样的:
https.createServer(options, function (req, res) {
res.writeHead(200);
res.end("hello world\n");
}).listen(3000);
原生的回调函数接受两个参数,一个是request
一个是response
,我们再去看koa2中这个回调函数的代码:
callback() {
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
const handleRequest = (req, res) => {
res.statusCode = 404;
const ctx = this.createContext(req, res);
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fn(ctx).then(handleResponse).catch(onerror);
};
return handleRequest;
}
这里有一个const fn = compose(this.middleware);
,compose
这种不知道大家用的多不多,compose
是函数式编程中使用比较多的东西,这里将多个中间件组合起来。
我们去看compose的实现:
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
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, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
首先判断是否是中间件数组,这个不用多说,for...of是ES6中的新特性,这里不做说明,需要注意的是,数组和Set集合默认的迭代器是values()方法,Map默认的是entries()方法。
这里的dispatch和next一样是所有的中间件的核心,dispatch的参数i其实也就是对应中间件的下标,,在第一次调用的时候传入了参数0,如果中间件存在返回Promise
:
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
我们lazyMan链式调用时不断的shift()
取出下一个要执行的事件函数,koa2里采用的是通过数组下标的方式找到下一个中间件,这里是用Promise.resolve
包起来就达到了每一个中间件await next()
返回的结果都刚好是下一个中间件的执行。不难看出此处dispatch
是个递归调用,多个中间件会形成一个栈结构。其中i
的值总是比上一次传进来的大,正常执行index
的值永远小于i
,但只要在同一个中间件中next
执行两次以上,index
的值就会等于i
,同时会抛出错误。但如果不执行next
,中间件的处理也会终止。
整理下流程:
-
compose(this.middleware)(ctx)
默认会执行中间件数组中的第一个,也就是代码中的dispatch(0)
,第一个中间件通过await next()
返回的是第二个中间件的执行。 - 然后第二个中间件中执行
await next()
,然后返回第三个...以此类推 - 中间件全部处理结束以后,剩下的就是通过中间件中不断传递的
context
来对请求作处理了。