简介如果你用过redux或者nodejs,那么你一定对“中间件”这个词不陌生。了解JavaScript中的事件流。举个例子,有一类人很懒(比如我),只有三种行为,sleep,eat,sleepFirst,伪代码是:varwang=newLazyMan('WangSledgehammer');wang.eat('Apple').eat('banana').sleep(5).eat('glucose').eat('orange').sleepFirst(2);//等价于以下代码constwang=newLazyMan('王大锤');wang.eat('苹果');wang.eat('香蕉');wang.sleep(5);wang.eat('葡萄糖');wang.eat('橘子');wang.sleepFirst(2);执行结果如下图:不管怎样,先sleep2S再做介绍,eat,sleep5S唤醒,eat,但是javascript只有一个线程,没有sleep这样的方法在php中实现思路是将eat、sleep、sleepFirst事件放在任务列表中,通过next依次执行方法。还是希望在看源码之前尝试手动实现一下。其实这就是lazyMan的实现。下面是我的实现方式:classlazyMan{constructor(name){this.tasks=[];constfirst=()=>{console.log(`我的名字是${name}`);这个.下一个();}this.tasks.push(first);setTimeout(()=>this.next(),0);}next(){consttask=this.tasks.shift();任务&&任务();}eat(food){consteat=()=>{console.log(`eat${food}`);这个.下一个();};this.tasks.push(吃);归还这个;}睡眠(时间){constnewTime=时间*1000;constsleep=()=>{console.log(`sleep${time}s!`);setTimeout(()=>{this.next();},newTime);};this.tasks.push(睡眠);归还这个;}sleepFirst(time){constnewTime=time*1000;constsleepzFirst=()=>{console.log(`先睡${time}s!`);设置Timeout(()=>{this.next();},newTime);};this.tasks.unshift(sleepzFirst);归还这个;}}constaLazy=newlazyMan('KingSledgehammer');aLazy.eat('Apple').eat('banana').sleep(5).eat('grape').eat('orange').sleepFirst(2)我们上面说的wang.eat('apple').eat('Banana').sleep(5).eat('glucose').eat('orange').sleepFirst(2);//就是相当于下面代码wang.eat('apple');wang.eat('Banana');wang.sleep(5);wang.eat('glucose');wang.eat('orange');wang睡眠第一(2);如果你用过node,你会发现这种写法似乎有点眼熟。让我们看一下koa2(一个节点框架)项目的主文件:constKoa=require('koa');constbodyParser=require('koa-bodyparser');constcors=require('koa-cors2');constrouters=require('./src/routers/index')constapp=newKoa();app.use(cors());app.use(bodyParser());app.use(routers.routes()).use(routers.allowedMethods())app.listen(3000);有没有发现结构有点相似?koa中的中间件废话不多说,直接看源码...app.use是用来注册中间件的,先看use的实现:use(fn){if(typeoffn!=='function')thrownewTypeError('中间件必须是一个函数!');如果(isGeneratorFunction(fn)){deprecate('对生成器的支持将在v3中删除。'+'请参阅文档以获取有关如何转换旧中间件的示例'+'https://github.com/koajs/koa/blob/master/docs/migration.md');fn=转换(fn);}debug('使用%s',fn._name||fn.name||'-');这个.middleware.push(fn);归还这个;}先解释一下里面做了什么。fn是传入函数。首先要判断是不是函数。如果不是,则会抛出错误。其次判断fn是否为GeneratorFunction。我用的是koa2。koa2中async,await代替koa1中的generator。如果判断为生成器函数,则证明使用或编写的中间件是koa1。koa2中提供了库koa-convert来帮助你将koa1中的中间件转换为koa2中的中间件,如果判断是koa1的中间件,它会提醒你,它会主动帮你转换,也就是代码中convert方法验证没有问题,注册这个middleware并将其放入middleware数组中。这里我们只看到中间件被添加到数组中,然后没有做其他处理。让我们看看listenlisten(...args){debug('listen');constserver=http.createServer(this.callback());返回server.listen(...args);}这里只是开始创建一个服务器,然后传入一个回调函数的结果。让我们看看本地启动服务器是什么样子的:https.createServer(options,function(req,res){res.writeHead(200);res.end("helloworld\n");}).listen(3000);原来的回调函数接受两个参数,一个是request,一个是response。我们看一下koa2中这个回调函数的代码:callback(){constfn=compose(this.middleware);if(!this.listeners('error').length)this.on('error',this.onerror);consthandleRequest=(req,res)=>{res.statusCode=404;constctx=this.createContext(req,res);constonerror=err=>ctx.onerror(err);consthandleResponse=()=>respond(ctx);onFinished(res,onerror);返回fn(ctx).then(handleResponse).catch(onerror);};返回句柄请求;这是一个constfn=compose(this.middleware);为了使用更多的东西,这里组合了多个中间件。我们来看一下compose的实现:!=='function')thrownewTypeError('中间件必须由函数组成!')}/***@param{Object}context*@return{Promise}*@apipublic*/returnfunction(context,next){//最后调用的中间件#letindex=-1returndispatch(0)functiondispatch(i){if(i<=index)returnPromise.reject(newError('next()被多次调用'))index=iletfn=middleware[i]if(i===middleware.length)fn=nextif(!fn)returnPromise.resolve()try{returnPromise.resolve(fn(context,functionnext(){returndispatch(i+1)}))}catch(err){returnPromise.reject(err)}}}}首先判断是否是数组的中间件。不用说,for...of是ES6中的一个新特性。这里不做解释。需要注意的是,数组和Set集合的默认迭代器是values()方法。嘛p的默认值是entries()方法。这里dispatch和next是所有中间件的核心。dispatch的参数i其实就是对应中间件的下标。第一次调用时传入参数0,如果中间件存在,则返回Promise:returnPromise.resolve(fn(context,functionnext(){returndispatch(i+1)}))当我们lazyMan链调用时,我们不断的shift()取出下一个要执行的事件函数,koa2使用数组下标来寻找下一个中间件。这里用Promise.resolve包裹起来,实现每个中间件awaitnext()返回的结果就是下一个中间件的执行。不难看出这里的dispatch是一个递归调用,多个中间件会形成一个栈结构。i的值总是大于上次传入的值,index的值在正常执行时总是小于i,但是只要next在同一个中间件中执行了两次以上,index的值就会等于i,同时会抛出错误。但是如果不执行next,中间件的处理也会终止。整理流程:compose(this.middleware)(ctx)默认会执行middleware数组中的第一个,即代码中的dispatch(0),第一个中间件返回第二个通过awaitnext()执行的一个中间件。然后在第二个中间件执行awaitnext(),然后返回到第三个……以此类推。所有的中间件都处理完了,剩下的就是通过中间件不断传递的context来处理请求了。
