导语:实现简单的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、resres.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; // 这是 Contextctx.req; // 这是 node Requestctx.res; // 这是 node Responsectx.request; // 这是 koa Requestctx.response; // 这是 koa Responsectx.request.req; // 这是 node Requestctx.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、responsethis.context = Object.create(context)this.request = Object.create(request)this.response = Object.create(response)}// 创建上下文 ctxcreateContext(req, res) {const ctx = this.context// koa 的 Request、Responsectx.request = this.requestctx.response = this.response// node 的 Request、Responsectx.request.req = ctx.req = reqctx.response.res = ctx.res = resreturn 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() {/* ... */}// 创建上下文 ctxcreateContext(req, res) {/* ... */}// 收集中间件use(fn) {/* ... */}// 处理当前请求方法handleRequest(req, res) {const ctx = this.createContext(req, res)res.statusCode = 404 //默认 statusif (ctx.body) {res.statusCode = 200res.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) returnconst 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) returni = indexconst 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() {/* ... */}// 创建上下文 ctxcreateContext(req, res) {/* ... */}// 收集中间件use(fn) {/* ... */}// 处理中间件compose(ctx) {const middlewares = this.middlewareslet i = -1 function dispatch(index) {if (index <= i) throw new Error('next() called multiple times')if (index === middlewares.length) returni = indexconst 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 //默认 statusif (ctx.body) {res.statusCode = 200res.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() {/* ... */}// 创建上下文 ctxcreateContext(req, res) {/* ... */}// 收集中间件use(fn) {/* ... */}// 处理中间件compose(ctx) {const middlewares = this.middlewareslet i = -1 async function dispatch(index) {if (index <= i) throw new Error('next() called multiple times')if (index === middlewares.length) returni = indexconst 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 //默认 statusif (ctx.body) {res.statusCode = 200res.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、responsethis.context = Object.create(context)this.request = Object.create(request)this.response = Object.create(response)}// 创建上下文 ctxcreateContext(req, res) {const ctx = this.context// koa 的 Request、Responsectx.request = this.requestctx.response = this.response// node 的 Request、Responsectx.request.req = ctx.req = reqctx.response.res = ctx.res = resreturn ctx}// 收集中间件use(fn) {this.middlewares.push(fn)}// 处理中间件compose(ctx) {const middlewares = this.middlewareslet i = -1 async function dispatch(index) {if (index <= i) throw new Error('multi called next()')if (index === middlewares.length) returni = indexconst 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 //默认 statusif (ctx.body) {res.statusCode = 200res.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}
}
复制代码

一步步实现koa核心代码相关推荐

  1. node之koa核心代码

    我们先实现这个小案例 app.use(async (ctx,next)=>{console.log(ctx.request.req.url)console.log(ctx.req.url)con ...

  2. 利用WxJava实现PC网站集成微信登录功能,核心代码竟然不超过10行

    最近网站PC端集成微信扫码登录,踩了不少坑,在此记录下实现过程和注意事项. 本文目录 一.微信开放平台操作步骤1.创建"网站应用"2.获取AppID和AppSecret二.开发指南 ...

  3. 融资 2000 万美元后,他竟将核心代码全开源,这……能行吗?

    立即报名:https://t.csdnimg.cn/KqnS 有这么一位"任性"的技术创业者: 2017 年,50 岁开始第三次创业,踏足自己从未深入涉及过的物联网大数据平台,敲下 ...

  4. 太牛了!30 年开源老兵,10 年躬耕 OpenStack,开源 1000 万行核心代码!

    受访者 | Jonathan Bryce 记者 | 伍杏玲 出品 | CSDN(ID:CSDNnews) 万物互联时代下,我们的一切都在依赖计算基础设施,科学.金融.政府.教育.通信和医疗保健依赖现代 ...

  5. asp.net的cms 核心代码篇

    好像开源有点多余,核心代码就下面这些. 1 using System;2 using System.Collections;3 using System.Collections.Generic;4 u ...

  6. 构建dubbo分布式平台-maven构建ant-framework核心代码annotation

    今天重点讲解的是ant-framework核心代码的编写过程. 其中ant-framework是ant分布式框架的基础核心框架,其中包括CRUD,MVC等一系列基类和模板.另外定义了spring,my ...

  7. ML之LGBMRegressor(Competition):2018年全国大学生计算机技能应用大赛《住房月租金预测大数据赛》——设计思路以及核心代码—191017再次更新

    ML之LGBMRegressor(Competition):2018年全国大学生计算机技能应用大赛<住房月租金预测大数据赛>--设计思路以及核心代码-191017再次更新 目录 竞赛相关信 ...

  8. 淘宝店铺图片数据迁移核心代码

    核心代码 using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sys ...

  9. OceanBase首次阐述战略:继续坚持自研开放之路 开源300万行核心代码

    简介:在数据库OceanBase3.0峰会上,蚂蚁集团自主研发的分布式数据库OceanBase首次从技术.商业和生态三个维度对未来发展战略进行了系统性阐述.同时,OceanBase宣布正式开源,并成立 ...

最新文章

  1. html编辑器kindeditor我的使用方法 (转载)
  2. exe一机一码加密工具_文件夹加密软件有哪些作用?
  3. java主线程控制子线程_CountDownLatch控制主线程等子线程执行完--Java多线程
  4. 不是linux内核的国产系统,国产操作系统都是山寨Windows的?为何用Linux内核?
  5. MySQL-库的操作
  6. mysql 异地备份工具_异地备份简单实现(mysql)
  7. 华为智能计算发布FusionServer Pro智能服务器
  8. Sublime Text 全程指南
  9. leetcode885.SpiralMatrixIII
  10. 国际认可不断增加,国产数据库发展与应用前景如何?
  11. [转载] Java8新特性-003-Java8接口中的default修饰符
  12. 限制编辑的PDF文档没有密码如何破解?
  13. 单片机 STM32 HAL GSM通讯 SIM800L
  14. 国内如何申请到Twitter API
  15. 中国历史上水平最高的十首诗词,你都读过吗?
  16. 计算机专业就业崩溃,计算机专业就业“遇冷说”引发争议
  17. 如何在Node.js应用程序中使用RethinkDB
  18. 三极管3种基础接法比较
  19. 独家送书福利(6本)【隐秘而伟大】这群神秘的程序员,干了票大的
  20. 树莓派 + Home Assistant + HomeKit 从零开始打造个人智能家居系统 篇三:进阶配置 Home Assistant

热门文章

  1. shell脚本----for循环-转 Syntax error: Bad for loop variable
  2. PAT 乙级 1051. 复数乘法 (15) Java版
  3. Linux系统性能检测
  4. Perl 字符串截取函数substr
  5. Linux 安装 informix
  6. 24.23%!汉能高效硅异质结薄膜电池效率再次刷新中国纪录
  7. AndroidJava保留小数位数的几种写法
  8. linux page buffer cache深入理解
  9. java 存储过程简单例子
  10. 本页不但包含安全的内容,也包含不安全的内容