技术分享 | Koa框架源码分析

简介:


一、Why Koa ?

koa 是由 Express 原班人马打造的,致力于成为一个更小、更富有表现力、更健壮的 Web 框架。 使用 koa 编写 web 应用,通过组合不同的 generator,可以免除重复繁琐的回调函数嵌套, 并极大地提升错误处理的效率。koa 不在内核方法中绑定任何中间件, 它仅仅提供了一个轻量优雅的函数库,使得编写 Web 应用变得得心应手。

  • async await写法,让异步更加优雅。(代码是写给人看的,顺便让机器执行而已)

  • 良好的抽象,简洁的api,简单、吊炸天的中间件机制。

  • 强大的社区,GayHub上数不胜数的中间件。

二、小试牛刀之源码文件一览

npm上download下koa之后打开koa/lib文件夹可以看到四个文件,足以证明koa框架是多么简洁。(不过koa框架依赖了很多node_modules)

1876810c715ad910d6db9cb9a900dd03167204ca

  • application.js 入口文件,也是骨架文件,创建一个服务。

  • context.js app 的 context 对象, 传入中间件的上下文对象。

  • request.js app 的请求对象,包含请求相关的一些属性。

  • response.js app 的响应对象,包含响应相关的一些属性。

下面大致总的分为三章节来分享一下koa源码。

1、Hello Koa


首先看一下原生node和koa分别是如何创建一个server。




/**

*原生Node创建server

*/

const http = require('http');

const server = http.createServer((req, res) => {

  res.statusCode = 200;

  res.setHeader('Content-Type', 'text/plain');

  res.end('Hello World\n');

});

server.listen(3000);

/**

*koa创建server

*/

const Koa = require('koa');

const app = new Koa();

app.use(ctx => {

  ctx.body = 'Hello Koa';

});

app.listen(3000);


看一下源码中koa究竟都做了什么



//1.首先创建了一个Application的类,继承了Emitter类,然后暴露出去。

module.exports = class Application extends Emitter {

  constructor() {

    super();

  }

}

//2.类下面有一个listen方法,创建http服务

 listen(...args) {

    debug('listen');

    const server = http.createServer(this.callback());

    return server.listen(...args);

 }


在Hello Koa中,引入这个类,然后实例化这个类,在使用下面的listen即搭建起了一个简单的koa服务。


然后讲一下初始化koa的时候,这个类的下面有一个use方法,也就是我们添加中间件的方法。

  use(fn) {

      //如果use里面传的不是方法,直接错误抛出去

    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');  

      //如果是星号函数,用一个库转成async await方法(去兼容koa1的中间件)

    if (isGeneratorFunction(fn)) { 

      //如果是generator函数,用convert转成async函数,然后执行。也是就是koa2能支持koa1的中间件。

      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); //转async await方法

    }

    debug('use %s', fn._name || fn.name || '-');

    this.middleware.push(fn); //koa把所有的中间件都放在一个数组里面

    return this;

  }

如代码所示,koa把所有的中间件放在一个数组里面,然后koa2也是兼容generator函数,为了兼容koa1的中间件。

callback方法Koa处理请求的方法。



  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;

  }


接下来就是看一下callback中compose如何处理了中间价和中间件的执行机制


 Koa中间件(middleware)实现探索

首先用一段简单的代码了解一下中间件执行顺序。




const Koa = require('koa')

const app = new Koa()

app.use(async function m1(ctx, next) {

  console.log('1')

  await next();

  console.log('1 end')

})

app.use(async function m2(ctx, next) {

  console.log('2')

  await next();

  console.log('2 end')

})

app.use(async function m3(ctx, next) {

  console.log('3')

  ctx.body = 'Hello World'

  console.log('3 end')

})

app.listen(3000);

// 打印的顺序是

// 1、2、3、3end、2end、1end

koa通过compose方法把中间件封装成一个promise对象,然后递归执行。使中间件数组变成洋葱圈,能递归线性处理上下文。
下面是官方的配图:
ebaba88a79703baedb8cd19128292d0c092454b2

我们主要看一下这个库:



function compose (middleware) {

  return function (context, next) {

    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, dispatch.bind(null, i + 1)));

       /**

          dispatch.bind(null, i + 1) 利用了js的bind继承

          其实可以写成 

          function() {

            dispatch(i + 1)

          }

         一个等待被执行的函数,然后用next()去执行下一个中间件。

       */

      } catch (err) {

        return Promise.reject(err)

      }

    }

  }

}

//通过调用第一个函数,层层递归调用,然后达到洋葱模型

//大白话讲一下就是,compose方法把所有的中间的执行的时候,把第一个中间件封装成了一个psomise对象执行,然后同时在中间件里面写next()方法去递归执行下一个中间件。


6bd5f59af54cb15522c6bee41f7febb569a807b9
这里的 Promise.resolve(fn(..)) 帮助我们异步执行的中间件函数,这里的next函数就解释了为什么Koa的中间件调用是递归执行的,它递归调用了 dispatch 函数来遍历数组中的,同时,所有的中间件函数享有同一个 ctx。

然后我根据compose处理后的中间件,大致中间件执行中理想的样子如图:



(async function(){

    // middleware1

    console.log('1');

    await  (async function(){

        // middleware2

        console.log('2');

        await ( async function(){

            // middleware3

            ...

            console.log('3');

            console.log('3 end');

            // ...middleware3

        })

        console.log('2 end')

        // ...middleware2

    })

    console.log('1 end');

    // ...middleware1

})()

组装成promise的中间件然后赋值到fn上去执行。

fn(ctx).then(handleResponse).catch(onerror)。 来看看这一句,fn 之前说过了,是所有的中间件函数的 “集合”, 用这一个中间件来表示整个处理过程。 同时 fn 也是一个 async 函数,执行结果返回一个 promise 对象, 同时 handleResponse 作为其 resolved 函数,onerror 是 rejected 函数。 也就是说koa中捕捉错误不是用try catch 而且是用promise中的catch。

3、 context的来源


context 使用node原生的 http 的监听回调函数中的 req res 来进行进一步的封装,意味着对于每一个 http 请求,koa都会创建一个 context 并共享给所有的全局中间件使用,当所有的中间件执行完过后,会将最后要返回的所有数据统一再交还给 res 进行返回,所以我们在每一个中间件中才能够从 ctx 中取得自己所需要的 req 中的数据进行处理,最后 ctx 再把要返回的 body 给原生的 res 进行返回。

每一个请求都有唯一一个 context 对象,所有的关于请求和返回的东西都统一放在里面 createContext 方法将 req res 进一步封装。



const context = Object.create(this.context); 

// 创建一个对象,使之拥有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的原则,context 必须是作为一个临时对象而存在,所有的东西都必须封进一个对象中,因此 app req res 三个属性就此诞生。

三、讲到最后

本文主要围绕“koa2的骨架”、“剖析koa中间件”、“ctx的来源”这三个模块来阐述了koa的原理和实现。并根据思路流程渐进梳理讲解了一些细节思路和比较关键的内容点,以及通过展示部分关键代码讲述了koa中间件的执行原理。文中肯定会有一些不够严谨的思考和错误,欢迎大家指正,有兴趣欢迎一起探讨和改进~

最后,感谢您的阅读!


原文发布时间为:2018年06月24日
原文作者: 掘金
本文来源:  掘金  如需转载请联系原作者



相关文章
|
ARouter 索引
|
8月前
|
前端开发 JavaScript 算法
前端面试题总结框架
前端面试题总结框架
Yii2的社区支持是什么?底层原理是什么?
Yii2的社区支持是什么?底层原理是什么?
|
存储 中间件 PHP
Laravel 中间件实现原理
Laravel 中间件实现原理
94 0
Laravel 中间件实现原理
|
中间件 Java Spring
手写了个小中间件,开源啦
手写了个小中间件,开源啦
215 0
手写了个小中间件,开源啦
|
前端开发 JavaScript 中间件
koa框架学习记录(9)
一个前端学习koa的简单记录
|
前端开发 中间件
koa框架学习记录(4)
一个前端学习koa的简单记录
|
JSON JavaScript 前端开发
koa框架学习记录(3)
一个前端学习koa的简单记录
|
前端开发
koa框架学习记录(5)
一个前端学习koa的简单记录
|
JavaScript 前端开发
koa框架学习记录(1)
一个前端学习koa的简单记录