Koa和Express有关洋葱模型对比及源码解析

express和koa都是基于nodejs的比较主流的两种web框架,express内置了很多中间件,而相对来说koa则更加轻量。

但对于异步处理,express用的是回调函数,koa1采用generator+yield,koa2采用异步终极解决方案async/await;通常我们说的koa就是指Koa2。

洋葱模型

在这里插入图片描述洋葱模型的意思就是:每一层洋葱皮都相当于一个中间件,进入时穿过多少层,出来时就还得原路返回穿过这些层,koa中间件的执行顺序就符合这个洋葱模型。

ps.中间件就相当于处理请求的一个函数。通常用来对请求做一些通用的http请求的细节处理。

例如Koa中间件的执行顺序:

  • 处理请求的顺序:第1层中间件-第2层-…-第n层-第n-1层-…-第2层-第1层

同步代码

express:

const express = require("express")
const app = express()

app.use((req, res, next) => {
  console.log("第一层中间件start")
  next()
  console.log("第一层中间件end")
  res.send("hello world")
})

app.use((req, res, next) => {
  console.log("第二层中间件start")
  next();
  console.log("第二层中间件end")
})

app.use((req, res, next) => {
  console.log("第三层中间件start");
  next()
  console.log("第三层中间件end");
})

app.listen(3000)

输出结果:

第一层中间件start
第二层中间件start
第三层中间件start
第三层中间件end
第二层中间件end
第一层中间件end

Koa:

const Koa = require("koa")
const app = new Koa()

app.use((ctx, next) => {
  console.log("第一层中间件start")
  next()
  console.log("第一层中间件end")
  ctx.body = "hello world"
})

app.use((ctx, next) => {
  console.log("第二层中间件start")
  next()
  console.log("第二层中间件end")
})

app.use((ctx, next) => {
  console.log("第三层中间件start")
  next()
  console.log("第三层中间件end")
})

app.listen(3000)

输出结果:

第一层中间件start
第二层中间件start
第三层中间件start
第三层中间件end
第二层中间件end
第一层中间件end

小结

可以看出两个框架在同步代码的情况下,得到的结果是一致的,都符合洋葱模型的执行顺序。

异步代码

Express:

const express = require("express")
const app = express()

app.use((req, res, next) => {
  console.log("第一层中间件start")
  next()
  console.log("第一层中间件end")
  res.send("hello world")
})

app.use((req, res, next) => {
  console.log("第二层中间件start")
  next();
  console.log("第二层中间件end")
})

app.use(async(req, res, next) => {
  console.log("第三层中间件start");
  await new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log("异步");
      resolve()
    }, 1000);
  })
  console.log("第三层中间件end");
})

app.listen(3000)

输出结果:

第一层中间件start
第二层中间件start
第三层中间件start
第二层中间件end
第一层中间件end
异步
第三层中间件end

Koa:

const Koa = require("koa")
const app = new Koa()

app.use(async (ctx, next) => {
  console.log("第一层中间件start")
  await next()
  console.log("第一层中间件end")
  ctx.body = "hello world"
})

app.use(async(ctx, next) => {
  console.log("第二层中间件start")
  await next()
  console.log("第二层中间件end")
})

app.use(async(ctx, next) => {
  console.log("第三层中间件start")
  await new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log("异步");
      resolve()
    }, 1000);
  })
  console.log("第三层中间件end")
})

app.listen(3000)

输出结果:

第一层中间件start
第二层中间件start
第三层中间件start
异步
第三层中间件end
第二层中间件end
第一层中间件end

小结

可以看到koa对于异步代码仍然是严格遵循洋葱模型,但是express则没有。

koa的中间件模式是洋葱模型,而express的中间件模式是直线型,这种区别的核心所在就是因为它们的中间件执行机制不同,next函数的实现原理不一样,koa的next()会返回一个promise实例,而express的next()返回void;express递归回调中不会去等待中间件中的异步函数执行完毕,而koa则存在await中间件异步函数。

洋葱模型源码解析

express的next方法实现:

  var idx = 0;//当前中间件索引
  function next(err) {
  ...//此处只保留核心的代码,其余都省略
    // find next matching layer
    var layer;
    var match;
    var route;

    while (match !== true && idx < stack.length) {
      layer = stack[idx++];
      match = matchLayer(layer, path);
      route = layer.route;

      if (typeof match !== 'boolean') {
        // hold on to layerError
        layerError = layerError || match;
      }

      if (match !== true) {
        continue;
      }
      ...//省略路由部分
    }
    // no match
    if (match !== true) {
      return done(layerError);
    }
    ...//省略
    // this should be done for the layer
    self.process_params(layer, paramcalled, req, res, function (err) {
      if (err) {
        next(layerError || err)
      } else if (route) {
        layer.handle_request(req, res, next)
      } else {
        trim_prefix(layer, layerError, layerPath, path)
      }

      sync = 0
    });
  }

其实就是维护了一个存放中间件的栈stack,每次循环都会按顺序从栈中取出一个layer,这个layer包含了路由信息和中间件信息,然后调用matchLayer函数将这个layer和请求的路径做匹配,匹配成功就会执行layer.handle_request方法,递归调用中间件函数,如果失败就进入下一次循环。

Koa2:

constructor(options) {
    this.middleware = [];
	...//省略
}
listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
use(fn) {
   ...//省略
    this.middleware.push(fn);
    return this;
  }
 callback() {
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      if (!this.ctxStorage) {
        return this.handleRequest(ctx, fn);
      }
      return this.ctxStorage.run(ctx, async() => {
        return await this.handleRequest(ctx, fn);//注意这里的await
      });
    };
    return handleRequest;
 }
   handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

listen方法就是我们new出来的koa实例app后调用的app.listen方法,从以上代码可知该listen方法借用了this.callback方法来生成回调函数,作为创建http web服务器对象的参数。

use方法就是维护一个中间件的数组middleware,做一个push方法去新加中间件。

再看callback函数,可以看出这里创建了上下文,然后从this.handleRequest(ctx, fn)和const fn = compose(this.middleware)可以看出真正去调用中间件函数的方法是compose。

this.handleRequest会执行compose函数返回的Promise函数并返回结果。

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]
      // 当 fn 为空的时候,就会开始执行 next() 后面部分的代码
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
      		//执行下一个中间件 fn第一个参数是上下文,第二个是next函数
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

i=0时,执行的是第一个中间件,之后也是按顺序一个接一个地调用,但当到最后一个中间件时,执行next的时候,进入dispatch函数时,fn会为undefined,这个时候就会进入到if(!fn)的判断条件中,执行promise.resolve。然后就会执行该中间件的next()语句之后的代码,然后反向回去。

参考链接:
https://zhuanlan.zhihu.com/p/417163957
https://juejin.cn/post/7044481311888654350

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值