Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >koa源码解析,理解洋葱模型

koa源码解析,理解洋葱模型

原创
作者头像
老码小张
修改于 2020-07-30 06:57:51
修改于 2020-07-30 06:57:51
6430
举报
文章被收录于专栏:玩转全栈玩转全栈

之前,我一直在使用express做简单的后台server,写一些api,给自己做的前端来提供服务,觉着吧挺好用的,虽然koa也出来挺久的,但是我一直没有更换过,直到今天看到一个项目中别人是使用koa来做后端代理的,所以,我才想,是否需要了解一下koa的源码呢。其实,我并不是一个喜欢尝鲜的人,因为我总觉得新鲜的事物一般有他没有考虑到的地方,或许会有很多大大的坑等着我们。但是突然发现,koa其实已经好几年的历史了,沉淀的也差不多了,是时候了解一下,并切换到koa上来了。

了解一个框架最好的方式莫过于直接下载他的源码,然后,跑他最简单的例子,找到入口位置,一步一步的跟踪下去。

首先,koa的入口文件在lib/application.js中,这个是他的package.json文件中告诉我的,node的工程就是这点好,打开package.json文件,大概就知道入口健在在哪了,很方便跟踪源代码。

我们在package.json中可以看到scripts下面配置了这样一些命令,

代码语言:txt
AI代码解释
复制
 "scripts": {
    "test": "egg-bin test test",
    "test-cov": "egg-bin cov test",
    "lint": "eslint benchmarks lib test",
    "bench": "make -C benchmarks",
    "authors": "git log --format='%aN <%aE>' | sort -u > AUTHORS"
  },

testtest-cov分别就是做测试,和覆盖率测试用的,简单的说,test就是测试你工程目录test中的文件,挨个挨个挨个拿出来盘一遍,而test-cov是会出一个报告的,你通过率是多少,当然我试了下,100%通过,这说明koa质量确实杠杠的,可以考虑切换。

之前自己写项目,从来就没有考虑写过测试,大佬就是大佬,我们不妨随便看一个测试用例先。就说说 /test/application/context.js这个吧,这里面代码是:

代码语言:txt
AI代码解释
复制
'use strict';

const request = require('supertest');
const assert = require('assert');
const Koa = require('../..');

describe('app.context', () => {
  const app1 = new Koa();
  app1.context.msg = 'hello';
  const app2 = new Koa();

  it('should merge properties', () => {
    app1.use((ctx, next) => {
      assert.equal(ctx.msg, 'hello');
      ctx.status = 204;
    });

    return request(app1.listen())
      .get('/')
      .expect(204);
  });

  it('should not affect the original prototype', () => {
    app2.use((ctx, next) => {
      assert.equal(ctx.msg, undefined);
      ctx.status = 204;
    });

    return request(app2.listen())
      .get('/')
      .expect(204);
  });
});

他是为了说明,两个实例的context是互相不会影响的。

image-20200612173819628
image-20200612173819628

这个测试是通过的,其他就就不一一过了,因为本文毕竟是将源码分析的,其实我还是想啰嗦一句:

test目录下的测试用例都看过一遍之后,你其实对koa的特性就等于基本了解了一遍,以后遇到什么问题,其实都不用上Google或许都可以解决,直接到真的个目录搜索关键字,通过测试用例,就能发现也许是自己某些配置导致的,我也是近期才发现,原来还可以这样定位问题。

那就废话不多说了,我们还是聊聊源码吧,首先,我们看到koa官网给我们的那个极致简单的例子:

代码语言:txt
AI代码解释
复制
const Koa = require('koa');
const app = new Koa();

// response
app.use(ctx => {
  ctx.body = 'Hello Koa';
});

app.listen(3000);

可以分解为3步:

  • New 了一个koa实例
  • 给实例use了一个中间件
  • 把这个server绑定到3000端口并启动。

首先,我们看一下new Koa做了些什么:

代码语言:txt
AI代码解释
复制
constructor(options) {
    super();
    options = options || {};
    this.proxy = options.proxy || false;
    this.subdomainOffset = options.subdomainOffset || 2;
    this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
    this.maxIpsCount = options.maxIpsCount || 0;
    this.env = options.env || process.env.NODE_ENV || 'development';
    if (options.keys) this.keys = options.keys;
    this.middleware = [];
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
  }

options中哪些key就不一一介绍,这里有一个keys,表示使用签名的cookie,这样方式被篡改。

然后,这里初始化了一个中间件的数组,用来存储一会用use注册的中间件,等会我们来看这里,先打一个记号。

然后,对context,request,response,但是这里使用的是Ojbect.create,可以了解一下,既:

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。 (请打开浏览器控制台以查看运行结果。)

那,这就意味着this.context的原型其实就是我们import进来的那个context,同理,this.request,this.response也是如此。这种做法明显就比较省内存,同时将context,request,response独立出来,做到了解耦,复用,感觉完美至极。

好吧,koa实例实际上就这么初始化了,其实,我们记得,主要是绑了context,request,response给这个实例,然后做了一个装中间件的数组容器

那么,接下来,我们看看中间件是如何注册的。

代码语言:txt
AI代码解释
复制
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;
  }

还是贴源码比价过瘾一点,中间件就是通过这个函数注册的,这里他已经不建议注册那种迭代器函数了,至于神马是迭代器函数,可以参考这里

这里是注册了,那么,哪里执行的中间件呢?

代码语言:txt
AI代码解释
复制
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);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

我们可以看到是在callback函数中,而callback又是在启动httpServer时注册的回调函数,这就意味着来一个请求就会触发这个回调,进而会调用到我们的callback,然而。这里有一个问题:

那就是我们注册了一堆的中间件,他是以怎么样的方式来执行呢?

可以看到中间件数组被compose了一下,这个compose是干啥的呢,一开始我没有看出个所以然,不过,看了这篇文章之后,我大概就明白了。然来Koa.js 的中间件通过这个工具函数组合后,按 app.use() 的顺序同步执行,也就是形成了 洋葱圈 式的调用。如图所示

image-20200612231322626
image-20200612231322626

部分比较重要的代码看下面,所有源码都在这

代码语言:txt
AI代码解释
复制
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)
      }
    }
  }
}

可以看到,中间件需要实现next方法,否则,你会将这个链路断开,到时其他注册的走不了。

至于怎么理解洋葱圈式的调用,可以参考我们的测试用例

代码语言:txt
AI代码解释
复制
describe('app.use(fn)', () => {
  it('should compose middleware', async() => {
    const app = new Koa();
    const calls = [];

    app.use((ctx, next) => {
      calls.push(1);
      return next().then(() => {
        calls.push(6);
      });
    });

    app.use((ctx, next) => {
      calls.push(2);
      return next().then(() => {
        calls.push(5);
      });
    });

    app.use((ctx, next) => {
      calls.push(3);
      return next().then(() => {
        calls.push(4);
      });
    });

    const server = app.listen();

    await request(server)
      .get('/')
      .expect(404);

    assert.deepEqual(calls, [1, 2, 3, 4, 5, 6]);
  });

  it('should compose mixed middleware', async() => {
    process.once('deprecation', () => {}); // silence deprecation message
    const app = new Koa();
    const calls = [];

    app.use((ctx, next) => {
      calls.push(1);
      return next().then(() => {
        calls.push(6);
      });
    });

    app.use(function * (next){
      calls.push(2);
      yield next;
      calls.push(5);
    });

    app.use((ctx, next) => {
      calls.push(3);
      return next().then(() => {
        calls.push(4);
      });
    });

    const server = app.listen();

    await request(server)
      .get('/')
      .expect(404);

    assert.deepEqual(calls, [1, 2, 3, 4, 5, 6]);
  });

所以,我们知道,上面的测试用例输出的是1,2,3,4,5,6了。

最后,绑定3000端口,启动起来就不用怎么解释了,这个是node原生代码,理解起来并无难度。

那么,这就玩了么,有我不是进场用express做静态代理吗?同样的道理,koa也可以,那么使用的中间件就是这个啦

我们看下他的源码关键部分:

代码语言:txt
AI代码解释
复制
if (!opts.defer) {
    return async function serve (ctx, next) {
      let done = false

      if (ctx.method === 'HEAD' || ctx.method === 'GET') {
        try {
          done = await send(ctx, ctx.path, opts)
        } catch (err) {
          if (err.status !== 404) {
            throw err
          }
        }
      }

      if (!done) {
        await next()
      }
    }
  }

  return async function serve (ctx, next) {
    await next()

    if (ctx.method !== 'HEAD' && ctx.method !== 'GET') return
    // response is already handled
    if (ctx.body != null || ctx.status !== 404) return // eslint-disable-line

    try {
      await send(ctx, ctx.path, opts)
    } catch (err) {
      if (err.status !== 404) {
        throw err
      }
    }
  }

总共也没有多少行源码,这里的逻辑是指,静态代理是否需要推迟执行,如果不推迟执行,那就在next执行之前就执行,如果defer执行,那么先让给其他中间件先处理,处理完回来之后,我在处理。

其实,还有一个中间件,甚至是非常重要的一个,那就是路由中间件,那么他实现的大概原理是啥呢?

代码太多,就看看关键部分

代码语言:txt
AI代码解释
复制
Router.prototype.routes = Router.prototype.middleware = function () {
  const router = this;

  let dispatch = function dispatch(ctx, next) {
    debug('%s %s', ctx.method, ctx.path);

    const path = router.opts.routerPath || ctx.routerPath || ctx.path;
    const matched = router.match(path, ctx.method);
    let layerChain;

    if (ctx.matched) {
      ctx.matched.push.apply(ctx.matched, matched.path);
    } else {
      ctx.matched = matched.path;
    }

    ctx.router = router;

    if (!matched.route) return next();

    const matchedLayers = matched.pathAndMethod
    const mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
    ctx._matchedRoute = mostSpecificLayer.path;
    if (mostSpecificLayer.name) {
      ctx._matchedRouteName = mostSpecificLayer.name;
    }

    layerChain = matchedLayers.reduce(function(memo, layer) {
      memo.push(function(ctx, next) {
        ctx.captures = layer.captures(path, ctx.captures);
        ctx.params = layer.params(path, ctx.captures, ctx.params);
        ctx.routerName = layer.name;
        return next();
      });
      return memo.concat(layer.stack);
    }, []);

    return compose(layerChain)(ctx, next);
  };

  dispatch.router = this;

  return dispatch;
};

可以看到最终return的那个方法dispatch ,参数签名包括ctx,next两者,这其实和我们之前看到的中间件定义的方式是一致的。

其实就是去匹配method和path,如果找到就处理,否则直接调用next,交给其他中间件处理,注意,路由本身是中间件。

总结,这里其实可以看到,koa框架本身非常简洁,核心上来看,就是处理了context,request,response,然后所有的事情都交给了中间件处理,这就极大的提升了灵活性,把这部分开放出来交给开发者,可以玩出无限多的可能。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
koa源码阅读[2]-koa-router
首先,因为koa是一个管理中间件的平台,而注册一个中间件使用use来执行。 无论是什么请求,都会将所有的中间件执行一遍(如果没有中途结束的话) 所以,这就会让开发者很困扰,如果我们要做路由该怎么写逻辑?
贾顺名
2019/12/09
1K0
koa-router源码解读
上一篇文章阅读了koa的源码,了解了koa整个运行流程、洋葱模型的实现方式,以及ctx的构建流程,让我整体有了对koa大概的了解。
LamHo
2022/09/26
5510
koa-router源码解读
koa实践及其手撸
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。
一粒小麦
2019/07/18
1.2K0
koa实践及其手撸
手写@koa\u002Frouter源码
上一篇文章我们讲了Koa的基本架构,可以看到Koa的基本架构只有中间件内核,并没有其他功能,路由功能也没有。要实现路由功能我们必须引入第三方中间件,本文要讲的路由中间件是@koa/router,这个中间件是挂在Koa官方名下的,他跟另一个中间件koa-router名字很像。其实@koa/router是fork的koa-router,因为koa-router的作者很多年没维护了,所以Koa官方将它fork到了自己名下进行维护。这篇文章我们还是老套路,先写一个@koa/router的简单例子,然后自己手写@koa/router源码来替换他。
蒋鹏飞
2022/11/28
8540
如何更好地理解中间件和洋葱模型
相信用过 Koa、Redux 或 Express 的小伙伴对中间件都不会陌生,特别是在学习 Koa 的过程中,还会接触到 “洋葱模型”。
前端森林
2020/11/04
9030
如何更好地理解中间件和洋葱模型
实现简单的 Koa
koa 为了能够简化 API,引入上下文 context,将原始请求对象 req 和 响应对象 res 封装并挂载到 context 上,并且在 context 上设置 getter 和 setter,从而简化操作
Cellinlab
2023/05/17
2930
实现简单的 Koa
源码共读-Koa
Koa是基于 Node.js 平台的下一代 web 开发框架,它的源码可以看这里,本章通过源码来简绍一下Koa是怎么实现的。
kai666666
2024/07/11
1040
源码共读-Koa
Koa 源码剖析
跟 Express 相比,Koa 的源码异常简洁,Express 因为把路由相关的代码嵌入到了主要逻辑中,因此读 Express 的源码可能长时间不得要领,而直接读 Koa 的源码几乎没有什么障碍。
用户8921923
2022/10/24
1K0
Koa 源码剖析
Koa源码阅读
Koa 在众多NodeJs框架中,以短小精悍而著称,核心代码只有大约570行,非常适合源码阅读。
小刀c
2022/08/16
5520
Koa源码阅读
手写Koa.js源码
Express的源码还是比较复杂的,自带了路由处理和静态资源支持等等功能,功能比较全面。与之相比,本文要讲的Koa就简洁多了,Koa虽然是Express的原班人马写的,但是设计思路却不一样。Express更多是偏向All in one的思想,各种功能都集成在一起,而Koa本身的库只有一个中间件内核,其他像路由处理和静态资源这些功能都没有,全部需要引入第三方中间件库才能实现。下面这张图可以直观的看到Express和koa在功能上的区别,此图来自于官方文档:
蒋鹏飞
2020/11/11
1.3K0
手写Koa.js源码
【Node】深入浅出 Koa 的洋葱模型
本文将讲解 koa 的洋葱模型,我们为什么要使用洋葱模型,以及它的原理实现。掌握洋葱模型对于理解 koa 至关重要,希望本文对你有所帮助~
GopalFeng
2022/08/01
8190
【Node】深入浅出 Koa 的洋葱模型
学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理
感兴趣的读者可以点击阅读。 其他源码计划中的有:express、vue-rotuer、redux、 react-redux 等源码,不知何时能写完(哭泣),欢迎持续关注我(若川)。
若川
2020/03/19
1.1K0
Node进阶——之事无巨细手写Koa源码
对比发现,相对原生,Koa多了两个实例上的use、listen方法,和use回调中的ctx、next两个参数。这四个不同,几乎就是Koa的全部了,也是这四个不同让Koa如此强大。
ConardLi
2020/05/25
5000
Node进阶——之事无巨细手写Koa源码
Node学习笔记 - Koa源码阅读
最近经过一些反思,发现现在很多时候用node的框架,都缺乏对于node框架的源码理解和实现原理,所以会在接下来的一段时间里进行学习node的框架实现原理,从中去更加深入理解node当中的一些技巧以及一些细节上的问题。
LamHo
2022/09/26
6690
Node学习笔记 - Koa源码阅读
koa 源码解析
本次的文章是之前 koa 专题的延续,计划书写两篇文章,本篇从零实现一个简单版的 koa 框架(里面可能涉及一点 node 知识,不会太讲,大家如果遇到不了解的可以自行百度查看,也可以看官网文档了解使用)。包括上下文 ctx 组合 req, res 的实现,中间件机制的实现。第二篇写下 bodyparser、 router 中间件的简单实现,理解其原理。
测不准
2021/07/14
5250
你需要掌握的 Koa 洋葱模型和中间件
Koa 是一个 nodejs 框架,经常用于写 web 后端服务。它是 Express 框架的原班人马开发的新一代 web 框架,使用了 async / await 来优雅处理无处不在的异步逻辑。
前端西瓜哥
2022/12/21
6540
你需要掌握的 Koa 洋葱模型和中间件
Koa源码学习
koa是一个非常流行的Node.js http框架。本文我们来学习下它的使用和相关源码
ACK
2023/10/19
3240
Koa源码学习
从源码分析express/koa/redux/axios等中间件的实现方式
在前端比较熟悉的框架如express、koa、redux和axios中,都提供了中间件或拦截器的功能,本文将从源码出发,分析这几个框架中对应中间件的实现原理。
周陆军博客
2023/05/14
2K0
【nodejs】手写简易版 koa 及常用中间件
首先我们要实现 koa 的 use 和 listen 方法,我们这样使用 MyKoa。
一尾流莺
2022/12/10
7780
【nodejs】手写简易版 koa 及常用中间件
Koa源码分析
Koa 是一个类似于 Express 的Web开发框架,创始人也都是TJ。Koa 的主要特点是,使用了 ES6 的 Generator 函数,进行了架构的重新设计。Koa 的原理和内部结构很像 Express,但是语法和内部结构进行了升级。 创建Koa应用 创建一个 koa 非常简单: var koa = require(‘koa’); var app = koa(); app.listen(3000); 或者可以酱紫: var koa = require(‘koa’); var http = requir
xiangzhihong
2018/01/26
9630
相关推荐
koa源码阅读[2]-koa-router
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档