实现koa核心代码

导语:实现简单的koa核心代码,便于理解koa原理。顺便学习koa代码的那些骚操作。

简单分析koa

创建一个 http 服务,只绑一个中间件。创建 index.js

/** index.js */
const Koa = require('koa')

const app = new Koa()

app.use(ctx => {
  ctx.body = 'Hello World'
})

app.listen(8080, function() {
  console.log('server start 8080')
})

从这段代码中可以看出

  • Koa 是一个构造函数
  • Koa 的原形上至少有 ues、listen 两个方法
  • listen 的参数与 http 一致
  • ues 接受一个方法,在用户访问的时候调用,并传入上下文 ctx (这里先不考虑异步与next,一步步实现)

我们再来看看 koa 源码的目录结构

|-- koa
    |-- .npminstall.done
    |-- History.md
    |-- LICENSE
    |-- Readme.md
    |-- package.json
    |-- lib
        |-- application.js
        |-- context.js
        |-- request.js
        |-- response.js

其中 application.js 是入口文件,打开后可以看到是一个 class。context.js、request.js、response.js 都是一个对象,用来组成上下文 ctx

启动http服务

先编写 application.js 部分代码。创建 myKoa 文件夹,我们的koa代码将会放在这个文件内。创建 myKoa/application.js

通过分析已经知道 application.js 导出一个 class,原形上至少有 listen 和 use 两个方法。listen 创建服务并监听端口号 http服务,use 用来收集中间件。实现代码如下

/** myKoa/application.js */
const http = require('http')

module.exports = class Koa {
  constructor() {
    // 存储中间件
    this.middlewares = []
  }
  // 收集中间件
  use(fn) {
    this.middlewares.push(fn)
  }
  // 处理当前请求方法
  handleRequest(req, res) { // node 传入的 req、res
    res.end('手写koa核心代码') // 为了访问页面有显示,暂时加上
  }
  // 创建服务并监听端口号
  listen(...arges) {
    const app = http.createServer(this.handleRequest.bind(this))
    app.listen(...arges)
  }
}

代码很简单。use 把中间件存入 middlewareslisten 启动服务,每次请求到来调用 handleRequest

创建 ctx (一)

context.js、request.js、response.js 都是一个对象,用来组成上下文 ctx。代码如下

/** myKoa/context.js */
const proto = {}

module.exports = proto
/** myKoa/request.js */
module.exports = {}
/** myKoa/response.js */
module.exports = {}

三者的关系是: request.js、response.js 两个文件的导出会绑定到 context.js 文件导出的对象上,分别作为 ctx.request 和 ctx.response 使用。

koa 为了每次 new Koa() 使用的 ctx 都是相互独立的,对 context.js、request.js、response.js 导出的对象做了处理。源码中使用的是 Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。一会在代码中演示用法

创建ctx之前,再看一下ctx上的几个属性,和他们直接的关系。一定要分清哪些是node自带的,哪些是koa的属性

app.use(async ctx => {
  ctx; // 这是 Context
  ctx.req; // 这是 node Request
  ctx.res; // 这是 node Response
  ctx.request; // 这是 koa Request
  ctx.response; // 这是 koa Response
  ctx.request.req; // 这是 node Request
  ctx.response.res;  // 这是 node Response
});

为什么这个设计,在文章后面将会解答

开始创建 ctx。部分代码如下

/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Koa {
  constructor() {
    // 存储中间件
    this.middlewares = []
    // 绑定 context、request、response
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
  }
  // 创建上下文 ctx
  createContext(req, res) {
    const ctx = this.context
    // koa 的 Request、Response
    ctx.request = this.request
    ctx.response = this.response
    // node 的 Request、Response
    ctx.request.req = ctx.req = req
    ctx.response.res = ctx.res = res
    return ctx
  }
  // 收集中间件
  use(fn) {/* ... */}
  // 处理当前请求方法
  handleRequest(req, res) {
    // 创建 上下文,准备传给中间件
    const ctx = this.createContext(req, res)
    
    res.end('手写koa核心代码') // 为了访问页面有显示,暂时加上
  }
  // 创建服务并监听端口号
  listen(...arges) {/* ... */}
}

此时就创建了一个基础的上下文 ctx。

创建 ctx (二)

获取上下文上的属性

实现一个 ctx.request.url。开始前先考虑几个问题

  • request.js 导出的是一个对象,不能接受参数
  • ctx.req.urlctx.request.urlctx.request.req.url 三者直接应该始终相等

koa 是这样做的

  • 第一步 ctx.request.req = ctx.req = req
  • 访问 ctx.request.url 转成访问 ctx.request.req.url

没错,就是 get 语法糖

/** myKoa/request.js */
module.exports = {
  get url() {
    return this.req.url
  }
}
此时的 this 指向的是 Object.create(request) 生成的对象,并不是 request.js 导出的对象

设置上下文上的属性

接下来我们实现 ctx.response.body = 'Hello World'。当设置 ctx.response.body 时实际上是把属性存到了 ctx.response._body 上,当获取 ctx.response.body 时只需要在 ctx.response._body 上取出就可以了 。代码如下

/** myKoa/response.js */
module.exports = {
  set body(v) {
    this._body = v
  },
  get body() {
    return this._body
  }
}
此时的 this 指向的是 Object.create(response) 生成的对象,并不是 response.js 导出的对象

设置 ctx 别名

koa 给我们设置了很多别名,比如 ctx.body 就是 ctx.response.body

有了之前的经验,获取/设置属性就比较容易。直接上代码

/** myKoa/context.js */
const proto = {
  get url() {
    return this.request.req.url
  },
  get body() {
    return this.response.body
  },
  set body(v) {
    this.response.body = v
  },
}
module.exports = proto

有没有感觉很简单。当然koa上下文部分没有到此结束。看 koa/lib/context.js 代码,在最下面可以看到这样的代码(从只挑选了access方法)

delegate(proto, 'response')
  .access('status')
  .access('body')

delegate(proto, 'request')
  .access('path')
  .access('url')

koa 对 get/set 做了封装。用的是 Delegator 第三方包。核心是用的 __defineGetter____defineSetter__ 两个方法。这里为了简单易懂,只是简单封装两个方法代替 Delegator 实现简单的功能。

// 获取属性。调用方法如 defineGetter('response', 'body')
function defineGetter(property, key) {
  proto.__defineGetter__(key, function() {
    return this[property][key]
  })
}
// 设置属性。调用方法如 defineSetter('response', 'body')
function defineSetter(property, key) {
  proto.__defineSetter__(key, function(v) {
    this[property][key] = v
  })
}

myKoa/context.js 文件最终修改为

/** myKoa/context.js */
const proto = {}

function defineGetter(property, key) {
  proto.__defineGetter__(key, function() {
    return this[property][key]
  })
}

function defineSetter(property, key) {
  proto.__defineSetter__(key, function(v) {
    this[property][key] = v
  })
}

// 请求
defineGetter('request', 'url')
// 响应
defineGetter('response', 'body')
defineSetter('response', 'body')

module.exports = proto

让 ctx.body 显示在页面上

这步非常简单,只需要判断 ctx.body 是否有值,并触发 req.end() 就完成了。相关代码如下

/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Koa {
  constructor() {/* ... */}
  // 创建上下文 ctx
  createContext(req, res) {/* ... */}
  // 收集中间件
  use(fn) {/* ... */}
  // 处理当前请求方法
  handleRequest(req, res) {
    const ctx = this.createContext(req, res)
    
    res.statusCode = 404 //默认 status
    if (ctx.body) {
      res.statusCode = 200
      res.end(ctx.body)
    } else {
      res.end('Not Found')
    }
  }
  // 创建服务并监听端口号
  listen(...arges) {/* ... */}
}

同理可以处理 header 等属性

实现同步中间件

中间件接受两个参数,一个上下文ctx,一个 next方法。上下文ctx已经写好了,主要是怎么实现next方法。

写一个dispatch方法,他的主要功能是:比如传入下标0,找出数组中下标为0的方法middleware,调用middleware并传入一个方法next,并且当next调用时, 查找下标加1的方法。实现如下

const middlewares = [f1, f2, f3]
function dispatch(index) {
  if (index === middlewares.length) return
  const middleware = middlewares[index]
  const next = () => dispatch(index+1)
  middleware(next)
}
dispatch(0)

此时就实现了next方法。

在koa中,是不允许一个请求中一个中间件调用两次next。比如

app.use((ctx, next) => {
  ctx.body = 'Hello World'
  next()
  next() // 报错 next() called multiple times
})

koa 用了一个小技巧。记录每次调用的中间件下标,当发现调用的中间件下标没有加1(中间件下标 <= 上一次中间件下标)时,就报错。修改代码如下

const middlewares = [f1, f2, f3] // 比如中间件中有三个方法
let i = -1 
function dispatch(index) {
  if (index <= i) throw new Error('next() called multiple times')
  if (index === middlewares.length) return
  i = index
  const middleware = middlewares[index]
  const next = () => dispatch(index+1)
  middleware(next)
}
dispatch(0)

中间件代码基本完成,传入 ctx 、加入 myKoa/application.js 文件。

/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Koa {
  constructor() {/* ... */}
  // 创建上下文 ctx
  createContext(req, res) {/* ... */}
  // 收集中间件
  use(fn) {/* ... */}
  // 处理中间件
  compose(ctx) {
    const middlewares = this.middlewares
    let i = -1 
    function dispatch(index) {
      if (index <= i) throw new Error('next() called multiple times')
      if (index === middlewares.length) return
      i = index
      const middleware = middlewares[index]
      const next = () => dispatch(index+1)
      middleware(ctx, next)
    }
    dispatch(0)
  }
  // 处理当前请求方法
  handleRequest(req, res) {
    const ctx = this.createContext(req, res)
    this.compose(ctx)

    res.statusCode = 404 //默认 status
    if (ctx.body) {
      res.statusCode = 200
      res.end(ctx.body)
    } else {
      res.end('Not Found')
    }
  }
  // 创建服务并监听端口号
  listen(...arges) {/* ... */}
}

到此就实现了同步中间件

实现异步中间件

koa 中使用异步中间件的写法如下

app.use(async (ctx, next) => {
  ctx.body = 'Hello World'
  await next()
})

app.use(async (ctx, next) => {
  await new Promise((res, rej) => setTimeout(res,1000))
  console.log('ctx.body:', ctx.body)
})

上述代码接受请求后大约1s 控制台打印 ctx.body: Hello World。可以看出,koa是基于 async/await 的。期望每次 next() 后返回的是一个 Promise

同时考虑到中间件变为异步执行,那么handleRequest应该等待中间件执行完再执行相关代码。那么compose也应该返回Promise

可以通过async快速完成 普通函数 =》Promise 的转化。

修改compose代码和handleRequest代码

/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Koa {
  constructor() {/* ... */}
  // 创建上下文 ctx
  createContext(req, res) {/* ... */}
  // 收集中间件
  use(fn) {/* ... */}
  // 处理中间件
  compose(ctx) {
    const middlewares = this.middlewares
    let i = -1 
    async function dispatch(index) {
      if (index <= i) throw new Error('next() called multiple times')
      if (index === middlewares.length) return
      i = index
      const middleware = middlewares[index]
      const next = () => dispatch(index+1)
      return middleware(ctx, next)
    }
    return dispatch(0)
  }
  // 处理当前请求方法
  handleRequest(req, res) {
    const ctx = this.createContext(req, res)
    const p = this.compose(ctx)
    p.then(() => {
      res.statusCode = 404 //默认 status
      if (ctx.body) {
        res.statusCode = 200
        res.end(ctx.body)
      } else {
        res.end('Not Found')
      }
    }).catch((err) => {
      console.log(err)
    })
  }
  // 创建服务并监听端口号
  listen(...arges) {/* ... */}
}

代码展示

application.js

/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Koa {
  constructor() {
    // 存储中间件
    this.middlewares = []
    // 绑定 context、request、response
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
  }
  // 创建上下文 ctx
  createContext(req, res) {
    const ctx = this.context
    // koa 的 Request、Response
    ctx.request = this.request
    ctx.response = this.response
    // node 的 Request、Response
    ctx.request.req = ctx.req = req
    ctx.response.res = ctx.res = res
    return ctx
  }
  // 收集中间件
  use(fn) {
    this.middlewares.push(fn)
  }
  // 处理中间件
  compose(ctx) {
    const middlewares = this.middlewares
    let i = -1 
    async function dispatch(index) {
      if (index <= i) throw new Error('multi called next()')
      if (index === middlewares.length) return
      i = index
      const middleware = middlewares[index]
      const next = () => dispatch(index+1)
      return middleware(ctx, next)
    }
    return dispatch(0)
  }
  // 处理当前请求方法
  handleRequest(req, res) {
    const ctx = this.createContext(req, res)
    const p = this.compose(ctx)
    p.then(() => {
      res.statusCode = 404 //默认 status
      if (ctx.body) {
        res.statusCode = 200
        res.end(ctx.body)
      } else {
        res.end('Not Found')
      }
    }).catch((err) => {
      console.log(err)
    })
  }
  // 创建服务并监听端口号
  listen(...arges) {
    const app = http.createServer(this.handleRequest.bind(this))
    app.listen(...arges)
  }
}

context.js

/** myKoa/context.js */
const proto = {}

function defineGetter(property, key) {
  proto.__defineGetter__(key, function() {
    return this[property][key]
  })
}

function defineSetter(property, key) {
  proto.__defineSetter__(key, function(v) {
    this[property][key] = v
  })
}

// 请求
defineGetter('request', 'url')
// 响应
defineGetter('response', 'body')
defineSetter('response', 'body')

module.exports = proto

request.js

/** myKoa/request.js */
module.exports = {
  get url() {
    return this.req.url
  }
}

response.js

/** myKoa/response.js */
module.exports = {
  set body(v) {
    this._body = v
  },
  get body() {
    return this._body
  }
}
Image placeholder
odayou
未设置
  62人点赞

没有讨论,发表一下自己的看法吧

推荐文章
史上规模最大的中文知识图谱以及估值两个亿的 AI 核心代码

——大声告诉我,怎样才能可以让你变得更强?——充钱——???——都什么玩意?还有啥子咧?——充更多钱执迷不悟,无可救药了。所以,正确答案应该是什么呢?答:是知识。反正,说这些就是为了切入「知识」这个话

学习 nodejs+mongodb+koa2 写接口(二) koa2教程入门

一.hellokoa安装koa2#初始化package.json npminit #安装koa2 npminstallkoahelloworld代码constKoa=require('koa') c

采用 PHP-quickorm/Captcha,用最快的速度在 PHP 语言下实现验证码功能

要调用起这个库,门槛十分低,但是建议满足以下几个条件: PHP5+ PHPGD扩展 Composer(非必须) 安装方法 首先我们花30秒来引入一下这个库,主要有以下两种方式。 其一、使用Comp

中信银行信用卡业务数据库实现国产替换,湖北银行新核心系统项目正式验收,阿里云与MongoDB达成战略合作

中信银行信用卡业务数据库实现国产替换10月31日,由IT168旗下ChinaUnix社区主办的第十一届中国系统架构师大会(SACC2019)在北京召开。会上,中信银行软件开发中心/技术平台开发处副处长

Java 与 Kotlin 系列文章 (一):性能问题

随着对Kotlin越来越深入的了解,我发现市面上关于Kotlin方面,比较深入的资料几乎是0,所以我决定,将Kotlin各个方面的研究作为我的研究生课题,而性能问题往往是程序员最佳关注的内容,所以第

集成 think-ORM 的 symfony bundle thinkorm-bundle

thinkorm-bundleSymfonyThinkOrmBundle关于thinkorm-bundle允许在你symfony使用thinkorm.所安装$composerrequireccwwwo

使用 ES6 写 Koa Web 项目

完整代码:传送门我们node.js只是实现了部分ES6的语法,所以为了让我们ES6的代码能100%在node.js下执行,必须使用我们的babel把ES6代码编译成nodejs可执行的代码。node环

资源混淆是如何影响到Kotlin协程的

导言随着kotlin的使用,协程也慢慢在我们工程中被开始被使用起来,但在我们工程中却遇到了一个问题,经过资源混淆处理之后的apk包,协程却不如期工作。那么两者到底有什么关联呢,资源混淆又是如何影响到协

Koa源码阅读

查看Koa,version@2.7源码,总共只有四个文件application.js、context.js、request.js、response.js;分别对应Koa应用入口、上下文环境、请求对象和

koala如何压缩css?

koala如何压缩css?koala是一个前端预处理器语言图形编译工具,支持Less、Sass、Compass、CoffeeScript,帮助web开发者更高效地使用它们进行开发。跨平台运行,完美兼容

Kotlin如何安全访问lateinit变量

Kotlin设计之初就是不允许非null变量在声明期间不进行初始化的,为了解决这个问题,Kotlinlateinit允许我们先声明一个变量,然后在程序执行周期的将来某个时候将其初始化,让编译检查时不会

学习 nodejs+mongodb+koa2 写接口(一) 环境布置

一.环境准备最近在学用Nodejs写后端接口,了解到koa2是Nodejs的一个框架。可以快速开发后端接口,同时也能更快熟悉Nodejs以下是所需的环境node  v7.6+,可以用nvm或者n安装指

阿里云小蜜对话机器人背后的核心算法

0.对话系统简介 对话系统的一般架构如图: 图1:对话系统一般架构 这是我们所熟知的对话系统框架,这里面主要有:NLU自然语言理解,DM对话管理,NLG自然语言生成3个主要模块,DM里面有dialo

ThinkPHP6 核心分析(一):Http 类的实例化

从入口文件出发 当访问一个ThinkPHP搭建的站点,框架最先是从入口文件开始的,然后才是应用初始化、路由解析、控制器调用和响应输出等操作。入口文件主要代码如下: //引入自动加载器,实现类的自动加载

PHP 核心特性 - 匿名函数

提出 在匿名函数出现之前,所有的函数都需要先命名才能使用 functionincrement($value) { return$value+1; } array_map('increment',[1

PHP 核心特性 - 错误处理

错误与异常 错误,可以理解程序本身的错误,例如语法错误。而异常则更偏向于程序运行不符合预期或者不符合正常流程;对于PHP语言而言,处理错误和处理异常使用的机制完全不同,因此很容易让人产生困惑。 例如,

spring 核心概述

用spring,注解是个绕不开的话题。spring-boot中常用@RestController与@Controller来分置api与web开发。yii与其思路一致,以不同命名空间下的控制器yii\

PHP 核心技术 --​​​​​​​​面向对象

继承和多态 类的组合与继承 假设我们有两个类,一个person,另外一个是family;在family类中我们创建person类中的对象,并且我们把这个对象视为family类的一个属性,并调用它的方法

iOS 核心动画高级技巧 - 1

1.图层树图层的树状结构 巨妖有图层,洋葱也有图层,你有吗?我们都有图层--史莱克 CoreAnimation其实是一个令人误解的命名。你可能认为它只是用来做动画的,但实际上它是从一个叫做LayerK

GoldenDB ,一个已经全面支撑银行核心系统的国产数据库

摘要:沿用、并存还是替代,一直是银行核心系统数据库转型重点思考的问题。四大行目前主要采用的是沿用与并存的数据库产品战略,在确保稳定的大前提下对新兴数据库技术进行探索研究和实践。相对而言,股份制银行在这

核心业务“瘦身”进行时!手把手带你搭建海量数据实时处理架构

01背景 在线交易服务平台目的是减轻核心系统计算压力和核心性能负荷压力,通过该平台可以将核心系统的交易数据实时捕获、实时计算加工、计算结果保存于SequoiaDB中。并能实时的为用户提供在线交易查询服

揭秘华新水泥核心业务上云的背后故事

武汉地处九省通衢之地,“敢为人先,追求卓越”的武汉精神,引领着武汉在科技“攻尖”与产业“攻坚”方面硕果连连。近日,“武汉·选择不凡华为云城市峰会2019”成功举办,华为云与湖北政企客户及伙伴共同探讨“

数据科学领域的核心技能和新兴技能分别有哪些?

近年来随着大数据的迅速发展,各种各样的数据分析技能也逐渐大热,为了找到数据科学领域目前最常用的技能和未来最流行的应用趋势,我们进行了一项调查。我们确定了数据科学技能的两个主要类别:一个是大多数受访者拥

前有堵截,后有追兵,核心技术如何突围?

摘要:真正的强大不是完美,而是能正视自己的不足,认清差距,这样才能有更强的动力砥砺前行。最近,一系列事件包括贸易战、美国公司禁售、技术封锁、修改授权协议等,美国对中国技术堵截之心昭然若揭。另一方面,中

DTCC 精彩继续,核心讲解点亮技术盛宴

“数据风云,十年变迁”,DTCC2019中国数据库大会目睹了中国这十年数据库技术的演进史,也见证了中国一代DBA的成长之路。日前,DTCC2019已进行到第二天,让我们一起去看看会有哪些业内专家分享他