koa浅析

94 阅读8分钟

KOA洋葱模型-01

前言

  • 看了很多关于koa的文章,都不太理解,所以本文从源码入手,希望能够提供给有需要的人一点点帮助

什么是koa

  • 也许你会觉得我会讲一大堆概念,附上中文官网地址:www.koajs.com.cn/#introducti…,简单来说就是就是基于node的web框架,直白点就是让你用javascript语言去编写接受请求,响应请求的逻辑代码。(自己说的大白话,如有错请各位大神指出)
  • 另外一个问题就是koa,koa2,express有什么区别,这种问题自行百度,框架无所谓好坏,只有适不适合

koa初探

  • 如果你用过其他的web框架如express,或者node提供的原生http服务,会对下面的代码很熟悉

  • const Koa = require('koa');
    const app = new Koa();
    //中间件1
    const mid1 = async (ctx, next) => {
      ctx.body = `<h3>请求 => 第一层中间件</h3>`
      await next()
      ctx.body += `<h3>响应 <= 第一层中间件</h3>`
    }
    //中间件2
    const mid2 = async (ctx, next) => {
      ctx.body += `<h3>请求 => 第二层中间件</h3>`
      await next()
      ctx.body += `<h3>响应 <= 第二层中间件</h3>`
    }
    
    app.use(mid1)
    app.use(mid2)
    
    app.listen(5000, () => {
      console.log('starting at http://localhost:5000');
    });
    
  • 上面的代码无非就是引包=> 书写两个中间件=> 把中间件加入服务处理队列=> 开启服务

  • 把本段代码粘贴到任意一个空白的js文件中,然后用node 文件路径(相对或绝对路径) ,接着在浏览器地址栏访问 http://localhost:5000,就能看到结果了(结果请自行尝试)

  • 乍一看,就这啊,我也会,但只会写这个这还远远不够,一个服务器应该能处理各种各样的请求,但是我们这个服务只能处理 访问了 http://localhost:5000这一个请求,如果你学过ajax,那你对各种请求肯定不默认。

  • 但是,本文暂不处理各种各样的请求,咱们谈论的是,为什么请求这个地址会返回相对应的数据

源码分析

  • 在node_modules找到koa这个包,打开package.json,可以发现入口文件是 ./lib/application.js

image.png

  • 打开application.js文件,发现会依赖当前目录的request.js , response.js context.js包。为了统一好调试,我们把这四个包统一复制出来,放入一个单独的文件夹中,然后我们把application.js

image.png

  • 重点关注的是application.js文件,在源码31行我们可以看到,对外暴露了一个类,这个就是我们new 的koa实例

image.png

  • 在源码第86行可以看到该类的listen方法,这就是我们上面代码listen方法源码,底层就是基于node提供的原生api开启的服务

image.png

  • 如果你写过node原生的api,应该对下面的代码很熟悉
const http = require('http');
const hostname = "localhost",
  port = 8080

//创建服务
const server = http.createServer((req,res) => {
  console.log('有人访问了我们的网站')
  res.end('hello world');
})

//监听请求 , http.createServer里面的函数和这个监听等价,只要有人请求了,那么就会触发
// server.on('request', (req,res) => {
//   console.log('有人访问了我们的网站');
//   res.end('hello world');
// })

//启动服务,协议默认是http
server.listen(port, hostname, () => {
  console.log('服务已经启动了');
})
  • 做完了对比,可以发现koa传入的是this.callback(),所以我们可以知道这个函数调用后返回了一个函数,看callback源码(application.js第150行)
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;
  }
  
  handleRequest方法:
  /*  第一行就是组装原生res,req对象成ctx上下文对象,这不是简单的组装,ctx有很多实
   *  用的属性和方法,比如请求参数,设置响应头等等,具体可以自行查阅,本文不做介绍。
   *  第二行调用的是类本身的 handleRequest方法,而不是这个局部的,所以不是递归
   */
  • 接下来看一下这个参数,第一个是ctx上下文对象,不做过多介绍,第二个fn,回头看一下fn的由来,到了本文的重头戏了,看一下compose,因自 koa-compose,这就是大名鼎鼎的koa引用库之一,代码不到40行,看一下这个包,就暴露了一个函数,先不看这个函数,先看参数,this.middleware , 可以在application.js文件中第59行看到在构造函数中初始化了一个空数组

image.png

  • 这就是存放中间件的容器,我们写的所有的中间件函数都存在在这里,前提是我们得use一下,既然提到了use,就简单看一下use方法(第129行),可以看到,首先做了一下判断,只要你写的规范,就不用在意这些,倒数第二行就是直接往我们这个数组里面添加这个函数返回this是为了链式调用,这里不深究
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;
  }
  • 我们知道了compose的参数,以及参数是怎么形成的,接下来就讨论洋葱模型的内核,koa-compose库的compose函数
function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }
  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, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
  • 简要看一下这个函数:首先判断是不是数组=》不是=》报异常;然后遍历数组,每一个元素都必须是函数,不是=》报异常,最后就是返回了一个匿名函数。
  • 然后回到上面的callback方法,我们知道了fn是一个匿名函数,传递给了this.handleRequest方法(别忘了callback返回了handleRequest 这个局部方法,在有人请求的时候就会触发这个函数,上文已经提到了)
  • 接下来如果有人请求了,handleRequest触发, this.handleRequest触发,看一下this.handleRequest方法application.js第176行)
  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);
  }
  • 最后一行就是我们compose函数返回的那个匿名函数的调用了,现在可以分析该函数了。下面的代码就是callback里面的fn,就是 this.handleRequest方法第第二个参数fnMiddleware(具体可以看上面的callback方法)
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, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
  • 中间件数组我们分为空数组和传递两个中间件讨论,方便理解

传递空数组

  • 先说结论,如果是空数组的话,页面上看到的是404.
  • this.handleRequest调用=> fnMiddleware调用(就上面这个函数),该函数里面创建了dispatch方法,返回了dispatch(0)的调用,通过代码分析很容易的得到把0传进去的时候, let fn = middleware[i] (fn是undefined, 所以直接返回成功的promise ,值是undefined),=>if (!fn) return Promise.resolve(),执行完了,又回到了this.handleRequest方法,看一下喽
  • 根据promise的链式调用,会执行then回调,所以又执行了respond方法,先别急着看respond方法,上面还有两行,设置状态码为404,虽然不是给上下文对象设置的,但是在上下文对象中可以拿到,然后看respond方法(application第252行),这里就不深究了,如果需要看的话我下次再讲
function respond(ctx) {
  // 省略代码...
  if (null == body) {
    if (ctx.response._explicitNullBody) {
      ctx.response.remove('Content-Type');
      ctx.response.remove('Transfer-Encoding');
      return res.end();
    }
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      //这就是返回的body内容,404,至于为什么上面的分支都不会走,因为我是一步一步调试到这里的,如果细看的话还有很多其他需要说的,这里就不深究了
      body = ctx.message || String(code);
    }
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    //返回body,页面上就看到了404
    return res.end(body);
  }
  //省略代码...
}

传递多个中间件(本文以两个为例)

  • 再把这个函数拿来
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, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
}
  • 同理,调用上面的函数,首先取出第一个中间件赋值给fn => let fn = middleware[i],然后进入try...catch,返回了fn的调用返回值作为成功的promise值,注意,这个fn就是我们压入数组的第一个中间件,fn执行传入了两个参数,一个是上下文对象,一个是新函数,bind就改变了 dispatch函数的this指向,没有执行dispatch函数,然后数组索引加1,这就是我们在中间件执行的next方法。
  • 所以就应该听过一句话,如果不在中间件调用next方法,是不会形成中间件执行链的,就是在这里截断了。
  • 好了,现在我们按照正常的逻辑走,第一个中间件执行了 ,body添加了内容,然后执行了next方法,这个next执行,相当于执行了dispatch(1)(注意这时候第一个中间件还没执行完,因为next在这里会阻塞,等待next执行完才会执行剩余代码)
  • dispatch(1)执行,就能取出第二个中间件,fn执行,所以第二个中间件执行了,第二个中间件又调用了next方法,这时候就相当于执行了 dispatch(2),但是这时候let fn = middleware[2] ,返回值是undefined,因为只有两个中间件,后面有一一个判断,if (i === middleware.length) fn = next,成立,所以执行,需要注意的是,这个next这个局部函数的外部变量

image.png

  • 回头看我们一开始是怎么调用这个匿名函数的,是不是在this.handleRequest里面的fnMiddleware函数,他只传了一个参数ctx,所以这个next就是undefined,所以下一条件语句执行if (!fn) return Promise.resolve(),返回值为undefined的成功promise
  • 所以,这时候我们在第二个中间件里next方法就此完成,await的结果是undefined,执行下面的代码,执行完了,如果没有返回值,默认也是undefined,这时候第一个中间件的next方法执行完,执行剩余代码
  • 至此,所有中间件代码执行完毕,整理数据返回,所以能够在页面看到如下结果

image.png

结语

本文基于自己的理解,写的第一篇文章,如有错误之处,还请大神们不吝赐教!