资讯专栏INFORMATION COLUMN

浅析koa的洋葱模型实现

dabai / 1543人阅读

摘要:前言被认为是第二代,它最大的特点就是独特的中间件流程控制,是一个典型的洋葱模型。这段代码就很巧妙的实现了两点将一路传下去给中间件将中的下一个中间件作为未来的返回值这两点也是洋葱模型实现的核心。

前言

koa被认为是第二代node web framework,它最大的特点就是独特的中间件流程控制,是一个典型的洋葱模型。koa和koa2中间件的思路是一样的,但是实现方式有所区别,koa2在node7.6之后更是可以直接用async/await来替代generator使用中间件,本文以最后一种情况举例。

洋葱模型

下面两张图是网上找的,很清晰的表明了一个请求是如何经过中间件最后生成响应的,这种模式中开发和使用中间件都是非常方便的

来看一个koa2的demo:

const Koa = require("koa");

const app = new Koa();
const PORT = 3000;

// #1
app.use(async (ctx, next)=>{
    console.log(1)
    await next();
    console.log(1)
});
// #2
app.use(async (ctx, next) => {
    console.log(2)
    await next();
    console.log(2)
})

app.use(async (ctx, next) => {
    console.log(3)
})

app.listen(PORT);
console.log(`http://localhost:${PORT}`);

访问http://localhost:3000,控制台打印:

1
2
3
2
1

怎么样,是不是有一点点感觉了。当程序运行到await next()的时候就会暂停当前程序,进入下一个中间件,处理完之后才会仔回过头来继续处理。也就是说,当一个请求进入,#1会被第一个和最后一个经过,#2则是被第二和倒数第二个经过,依次类推。

实现

koa的实现有几个最重要的点

context的保存和传递

中间件的管理和next的实现

翻看源码我们发现
app.listen使用了this.callback()来生成node的httpServer的回调函数

listen(...args) {
    debug("listen");
    const server = http.createServer(this.callback());
    return server.listen(...args);
}

那就再来看this. callback

callback() {
    const fn = compose(this.middleware);
    
    if (!this.listeners("error").length) this.on("error", this.onerror);
    
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
    
    return handleRequest;
}

这里用compose处理了一下this.middleware,创建了ctx并赋值为createContext的返回值,最后返回了handleRequest

this.middleware看起来应该是中间件的集合,查了下代码,果不其然:

this.middleware = [];
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;
}

抛开兼容和判断,这段代码只做了一件事:

use(fn) {
    this.middleware.push(fn);
    return this;
}

原来当我们app.use的时候,只是把方法存在了一个数组里。
那么compose 又是什么呢。跟踪源码可以看到compose来自koa-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, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

比较关键的就是这个dispatch函数了,它将遍历整个middleware,然后将contextdispatch(i + 1)传给middleware中的方法。

return Promise.resolve(fn(context, function next () {
      return dispatch(i + 1)
}))

这段代码就很巧妙的实现了两点:

1. 将`context`一路传下去给中间件

2. 将`middleware`中的下一个中间件`fn`作为未来`next`的返回值

这两点也是洋葱模型实现的核心。
再往下看代码实际上就没有太多花样了。
createContexthandleRequest 做的事实际上是把ctx和中间件进行绑定,也就是第一次调用compose 返回值的地方。

  createContext(req, res) {
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    context.app = request.app = response.app = this;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.cookies = new Cookies(req, res, {
      keys: this.keys,
      secure: request.secure
    });
    request.ip = request.ips[0] || req.socket.remoteAddress || "";
    context.accept = request.accept = accepts(req);
    context.state = {};
    return context;
  }

  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);
  }



文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93712.html

相关文章

  • koa2 总体流程原理浅析(二) 之 中间件原理

    摘要:任何一层报错,都能用捕获总结是一个非常轻量级的框架,只实现了中间件处理流程和对对象的封装。其他的功能都由外部中间件提供。 koa 的中间件机制巧妙的运用了闭包和 async await 的特点,形成了一个洋葱式的流程,和 JS 的事件流 (捕获 -> target -> 冒泡) 相似 handleRequest(ctx, fnMiddleware) { const res ...

    zhoutk 评论0 收藏0
  • 浅析Redux源码

    摘要:用法源码由在年创建的科技术语。我们除去源码校验函数部分,从最终返回的大的来看。这个返回值无法被识别。洋葱模型我们来看源码源码每个都以作为参数进行注入,返回一个新的链。改变原始组数,是一种副作用。 @(Redux)[|用法|源码] Redux 由Dan Abramov在2015年创建的科技术语。是受2014年Facebook的Flux架构以及函数式编程语言Elm启发。很快,Redux因其...

    lifesimple 评论0 收藏0
  • KOA2框架原理解析和实现

    摘要:实现的四大模块上文简述了源码的大体框架结构,接下来我们来实现一个的框架,笔者认为理解和实现一个框架需要实现四个大模块,分别是封装创建类构造函数构造对象中间件机制和剥洋葱模型的实现错误捕获和错误处理下面我们就逐一分析和实现。 什么是koa框架?        koa是一个基于node实现的一个新的web框架,它是由express框架的原班人马打造的。它的特点是优雅、简洁、表达力强、自由度...

    tracymac7 评论0 收藏0
  • KOA2框架原理解析和实现

    摘要:实现的四大模块上文简述了源码的大体框架结构,接下来我们来实现一个的框架,笔者认为理解和实现一个框架需要实现四个大模块,分别是封装创建类构造函数构造对象中间件机制和剥洋葱模型的实现错误捕获和错误处理下面我们就逐一分析和实现。 什么是koa框架?        koa是一个基于node实现的一个新的web框架,它是由express框架的原班人马打造的。它的特点是优雅、简洁、表达力强、自由度...

    liangzai_cool 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<