koa源码阅读[0]

koa源码阅读[0]

Node.js也是写了两三年的时间了,刚开始学习Node的时候,hello world就是创建一个HttpServer,后来在工作中也是经历过ExpressKoa1.xKoa2.x以及最近还在研究的结合着TypeScriptrouting-controllers(驱动依然是ExpressKoa)。
用的比较多的还是Koa版本,也是对它的洋葱模型比较感兴趣,所以最近抽出时间来阅读其源码,正好近期可能会对一个Express项目进行重构,将其重构为koa2.x版本的,所以,阅读其源码对于重构也是一种有效的帮助。

Koa是怎么来的

首先需要确定,Koa是什么。
任何一个框架的出现都是为了解决问题,而Koa则是为了更方便的构建http服务而出现的。
可以简单的理解为一个HTTP服务的中间件框架。

使用http模块创建http服务

相信大家在学习Node时,应该都写过类似这样的代码:

const http = require(‘http‘)

const serverHandler = (request, response) => {
  response.end(‘Hello World‘) // 返回数据
}

http
  .createServer(serverHandler)
  .listen(8888, _ => console.log(‘Server run as http://127.0.0.1:8888‘))

一个最简单的示例,脚本运行后访问http://127.0.0.1:8888即可看到一个Hello World的字符串。
但是这仅仅是一个简单的示例,因为我们不管访问什么地址(甚至修改请求的Method),都总是会获取到这个字符串:

> curl http://127.0.0.1:8888
> curl http://127.0.0.1:8888/sub
> curl -X POST http://127.0.0.1:8888

所以我们可能会在回调中添加逻辑,根据路径、Method来返回给用户对应的数据:

const serverHandler = (request, response) => {
  // default
  let responseData = ‘404‘

  if (request.url === ‘/‘) {
    if (request.method === ‘GET‘) {
      responseData = ‘Hello World‘
    } else if (request.method === ‘POST‘) {
      responseData = ‘Hello World With POST‘
    }
  } else if (request.url === ‘/sub‘) {
    responseData = ‘sub page‘
  }

  response.end(responseData) // 返回数据
}

类似Express的实现

但是这样的写法还会带来另一个问题,如果是一个很大的项目,存在N多的接口。
如果都写在这一个handler里边去,未免太过难以维护。
示例只是简单的针对一个变量进行赋值,但是真实的项目不会有这么简单的逻辑存在的。
所以,我们针对handler进行一次抽象,让我们能够方便的管理路径:

class App {
  constructor() {
    this.handlers = {}

    this.get = this.route.bind(this, ‘GET‘)
    this.post = this.route.bind(this, ‘POST‘)
  }

  route(method, path, handler) {
    let pathInfo = (this.handlers[path] = this.handlers[path] || {})

    // register handler
    pathInfo[method] = handler
  }

  callback() {
    return (request, response) => {
      let { url: path, method } = request

      this.handlers[path] && this.handlers[path][method]
        ? this.handlers[path][method](request, response)
        : response.end(‘404‘)
    }
  }
}

然后通过实例化一个Router对象进行注册对应的路径,最后启动服务:

const app = new App()

app.get(‘/‘, function (request, response) {
  response.end(‘Hello World‘)
})

app.post(‘/‘, function (request, response) {
  response.end(‘Hello World With POST‘)
})

app.get(‘/sub‘, function (request, response) {
  response.end(‘sub page‘)
})

http
  .createServer(app.callback())
  .listen(8888, _ => console.log(‘Server run as http://127.0.0.1:8888‘))

Express中的中间件

这样,就实现了一个代码比较整洁的HttpServer,但功能上依旧是很简陋的。
如果我们现在有一个需求,要在部分请求的前边添加一些参数的生成,比如一个请求的唯一ID。
将代码重复编写在我们的handler中肯定是不可取的。
所以我们要针对route的处理进行优化,使其支持传入多个handler

route(method, path, ...handler) {
  let pathInfo = (this.handlers[path] = this.handlers[path] || {})

  // register handler
  pathInfo[method] = handler
}

callback() {
  return (request, response) => {
    let { url: path, method } = request

    let handlers = this.handlers[path] && this.handlers[path][method]

    if (handlers) {
      let context = {}
      function next(handlers, index = 0) {
        handlers[index] &&
          handlers[index].call(context, request, response, () =>
            next(handlers, index + 1)
          )
      }

      next(handlers)
    } else {
      response.end(‘404‘)
    }
  }
}

然后针对上边的路径监听添加其他的handler:

function generatorId(request, response, next) {
  this.id = 123
  next()
}

app.get(‘/‘, generatorId, function(request, response) {
  response.end(`Hello World ${this.id}`)
})

这样在访问接口时,就可以看到Hello World 123的字样了。
这个就可以简单的认为是在Express中实现的 中间件
中间件是ExpressKoa的核心所在,一切依赖都通过中间件来进行加载。

更灵活的中间件方案-洋葱模型

上述方案的确可以让人很方便的使用一些中间件,在流程控制中调用next()来进入下一个环节,整个流程变得很清晰。
但是依然存在一些局限性。
例如如果我们需要进行一些接口的耗时统计,在Express有这么几种可以实现的方案:

function beforeRequest(request, response, next) {
  this.requestTime = new Date().valueOf()

  next()
}

// 方案1. 修改原handler处理逻辑,进行耗时的统计,然后end发送数据
app.get(‘/a‘, beforeRequest, function(request, response) {
  // 请求耗时的统计
  console.log(
    `${request.url} duration: ${new Date().valueOf() - this.requestTime}`
  )

  response.end(‘XXX‘)
})

// 方案2. 将输出数据的逻辑挪到一个后置的中间件中
function afterRequest(request, response, next) {
  // 请求耗时的统计
  console.log(
    `${request.url} duration: ${new Date().valueOf() - this.requestTime}`
  )

  response.end(this.body)
}

app.get(
  ‘/b‘,
  beforeRequest,
  function(request, response, next) {
    this.body = ‘XXX‘

    next() // 记得调用,不然中间件在这里就终止了
  },
  afterRequest
)

无论是哪一种方案,对于原有代码都是一种破坏性的修改,这是不可取的。
因为Express采用了response.end()的方式来向接口请求方返回数据,调用后即会终止后续代码的执行。
而且因为当时没有一个很好的方案去等待某个中间件中的异步函数的执行。

function a(_, _, next) {
  console.log(‘before a‘)
  let results = next()
  console.log(‘after a‘)
}

function b(_, _, next) {
  console.log(‘before b‘)
  setTimeout(_ => {
    this.body = 123456
    next()
  }, 1000)
}

function c(_, response) {
  console.log(‘before c‘)
  response.end(this.body)
}

app.get(‘/‘, a, b, c)

就像上述的示例,实际上log的输出顺序为:

before a
before b
after a
before c

这显然不符合我们的预期,所以在Express中获取next()的返回值是没有意义的。

所以就有了Koa带来的洋葱模型,在Koa1.x出现的时间,正好赶上了Node支持了新的语法,Generator函数及Promise的定义。
所以才有了co这样令人惊叹的库,而当我们的中间件使用了Promise以后,前一个中间件就可以很轻易的在后续代码执行完毕后再处理自己的事情。
但是,Generator本身的作用并不是用来帮助我们更轻松的使用Promise来做异步流程的控制。
所以,随着Node7.6版本的发出,支持了asyncawait语法,社区也推出了Koa2.x,使用async语法替换之前的co+Generator

Koa也将co从依赖中移除(2.x版本使用koa-convertGenerator函数转换为promise,在3.x版本中将直接不支持Generator
ref: remove generator supports

由于在功能、使用上Koa的两个版本之间并没有什么区别,最多就是一些语法的调整,所以会直接跳过一些Koa1.x相关的东西,直奔主题。

Koa中,可以使用如下的方式来定义中间件并使用:

async function log(ctx, next) {
  let requestTime = new Date().valueOf()
  await next()

  console.log(`${ctx.url} duration: ${new Date().valueOf() - requestTime}`)
}

router.get(‘/‘, log, ctx => {
  // do something...
})

因为一些语法糖的存在,遮盖了代码实际运行的过程,所以,我们使用Promise来还原一下上述代码:

function log() {
  return new Promise((resolve, reject) => {
    let requestTime = new Date().valueOf()
    next().then(_ => {
      console.log(`${ctx.url} duration: ${new Date().valueOf() - requestTime}`)
    }).then(resolve)
  })
}

大致代码是这样的,也就是说,调用next会给我们返回一个Promise对象,而Promise何时会resolve就是Koa内部做的处理。
可以简单的实现一下(关于上边实现的App类,仅仅需要修改callback即可):

callback() {
  return (request, response) => {
    let { url: path, method } = request

    let handlers = this.handlers[path] && this.handlers[path][method]

    if (handlers) {
      let context = { url: request.url }
      function next(handlers, index = 0) {
        return new Promise((resolve, reject) => {
          if (!handlers[index]) return resolve()

          handlers[index](context, () => next(handlers, index + 1)).then(
            resolve,
            reject
          )
        })
      }

      next(handlers).then(_ => {
        // 结束请求
        response.end(context.body || ‘404‘)
      })
    } else {
      response.end(‘404‘)
    }
  }
}

每次调用中间件时就监听then,并将当前Promiseresolvereject处理传入Promise的回调中。
也就是说,只有当第二个中间件的resolve被调用时,第一个中间件的then回调才会执行。
这样就实现了一个洋葱模型。

就像我们的log中间件执行的流程:

  1. 获取当前的时间戳requestTime
  2. 调用next()执行后续的中间件,并监听其回调
  3. 第二个中间件里边可能会调用第三个、第四个、第五个,但这都不是log所关心的,log只关心第二个中间件何时resolve,而第二个中间件的resolve则依赖他后边的中间件的resolve
  4. 等到第二个中间件resolve,这就意味着后续没有其他的中间件在执行了(全都resolve了),此时log才会继续后续代码的执行

所以就像洋葱一样一层一层的包裹,最外层是最大的,是最先执行的,也是最后执行的。(在一个完整的请求中,next之前最先执行,next之后最后执行)。

小记

最近抽时间将Koa相关的源码翻看一波,看得挺激动的,想要将它们记录下来。
应该会拆分为几段来,不一篇全写了,上次写了个装饰器的,太长,看得自己都困了。
先占几个坑:

  • 核心模块 koa与koa-compose
  • 热门中间件 koa-router与koa-views
  • 杂七杂八的轮子 koa-bodyparser/multer/better-body/static

示例代码仓库地址
源码阅读仓库地址

原文地址:https://www.cnblogs.com/jiasm/p/9351234.html

时间: 2024-08-01 03:17:59

koa源码阅读[0]的相关文章

koa源码阅读[2]-koa-router

koa源码阅读[2]-koa-router 第三篇,有关koa生态中比较重要的一个中间件:koa-router 第一篇:koa源码阅读-0第二篇:koa源码阅读-1-koa与koa-compose koa-router是什么 首先,因为koa是一个管理中间件的平台,而注册一个中间件使用use来执行.无论是什么请求,都会将所有的中间件执行一遍(如果没有中途结束的话)所以,这就会让开发者很困扰,如果我们要做路由该怎么写逻辑? app.use(ctx => { switch (ctx.url) { c

koa源码阅读[3]-koa-send与它的衍生(static)

koa源码阅读的第四篇,涉及到向接口请求方提供文件数据. 第一篇:koa源码阅读-0第二篇:koa源码阅读-1-koa与koa-compose第三篇:koa源码阅读-2-koa-router 处理静态文件是一个繁琐的事情,因为静态文件都是来自于服务器上,肯定不能放开所有权限让接口来读取.各种路径的校验,权限的匹配,都是需要考虑到的地方.而koa-send和koa-static就是帮助我们处理这些繁琐事情的中间件.koa-send是koa-static的基础,可以在NPM的界面上看到,static

AFNetworking 3.0源码阅读 - AFURLSessionManager

这次来说一下AFURLSessionManager 从头文件的英文注释可以看出AFURLSessionManager类创建并管理着NSURLSession对象,而NSURLSession又是基于NSURLSessionConfiguration的.同时该类也是AFHTTPSessionManager的父类,下一篇来讲. AFURLSessionManager实现了四个协议 1.NSURLSessionDelegate URLSession:didBecomeInvalidWithError: U

muduo2.0源码阅读记录

花了20天的时间读了陈硕先生的<Linux多线程服务端编程>一书的前8章.当然,每天阅读的时间并不算多,中间有些部分也反反复复看了几遍,最后也算是能勉强接受作者传授的知识.配合书把muduo2.0网络部分的代码和日志库代码细读了一遍,这也算是个人第一次较为深入地去读取一个开源项目源码.通过书和源码的阅读,确实是对不少东西加深了理解. 本来想按自己的理解来写源码阅读笔记的,但考虑到网上关于muduo代码的解析文章已经很多并且写的很好了,就放弃了这个想法.摘录几个自己在源码阅读过程中参考的网页:

redis 5.0.7 源码阅读——双向链表

redis中动态字符串sds相关的文件为:adlist.h与adlist.c 一.数据结构 redis里定义的双向链表,与普通双向链表大致相同 单个节点: 1 typedef struct listNode { 2 struct listNode *prev; 3 struct listNode *next; 4 void *value; 5 } listNode; 链表: 1 typedef struct list { 2 listNode *head; 3 listNode *tail; 4

redis 5.0.7 源码阅读——跳跃表skiplist

redis中并没有专门给跳跃表两个文件.在5.0.7的版本中,结构体的声明与定义.接口的声明在server.h中,接口的定义在t_zset.c中,所有开头为zsl的函数. 一.数据结构 单个节点: typedef struct zskiplistNode { //key,唯一 sds ele; //分值,可重复 double score; //后退指针 struct zskiplistNode *backward; //层 struct zskiplistLevel { //前进指针 struc

CI框架源码阅读笔记3 全局函数Common.php

从本篇开始,将深入CI框架的内部,一步步去探索这个框架的实现.结构和设计. Common.php文件定义了一系列的全局函数(一般来说,全局函数具有最高的加载优先权,因此大多数的框架中BootStrap引导文件都会最先引入全局函数,以便于之后的处理工作). 打开Common.php中,第一行代码就非常诡异: if ( ! defined('BASEPATH')) exit('No direct script access allowed'); 上一篇(CI框架源码阅读笔记2 一切的入口 index

淘宝数据库OceanBase SQL编译器部分 源码阅读--生成逻辑计划

body, td { font-family: tahoma; font-size: 10pt; } 淘宝数据库OceanBase SQL编译器部分 源码阅读--生成逻辑计划 SQL编译解析三部曲分为:构建语法树,生成逻辑计划,指定物理执行计划.第一步骤,在我的上一篇博客淘宝数据库OceanBase SQL编译器部分 源码阅读--解析SQL语法树里做了介绍,这篇博客主要研究第二步,生成逻辑计划. 一. 什么是逻辑计划?我们已经知道,语法树就是一个树状的结构组织,每个节点代表一种类型的语法含义.如

JDK部分源码阅读与理解

本文为博主原创,允许转载,但请声明原文地址:http://www.coselding.cn/article/2016/05/31/JDK部分源码阅读与理解/ 不喜欢重复造轮子,不喜欢贴各种东西.JDK代码什么的,让整篇文章很乱...JDK源码谁都有,没什么好贴的...如果你没看过JDK源码,建议打开Eclipse边看源码边看这篇文章,看过的可以把这篇文章当成是知识点备忘录... JDK容器类中有大量的空指针.数组越界.状态异常等异常处理,这些不是重点,我们关注的应该是它的一些底层的具体实现,这篇