本次分析的KOA版本是[2.13.1](https://github.com/koajs/koa),它非常轻量,诸如路由、模板等功能默认都不提供,需要自己引入相关的中间件。
  源码的目录结构比较简单,主要分为3部分,\_\_tests\_\_,lib和docs,从名称中就可以了解到。
  \_\_tests\_\_是单元测试,lib是核心代码,docs是文档。在lib目录中只有4个文件。
~~~
├── __tests__ ------------------------ 单元测试
├── docs ----------------------------- 文档
├── lib ------------------------------ 源码目录
│ ├── application.js --------------- 运行
│ ├── context.js ------------------- 上下文
│ ├── request.js ------------------- 请求
│ ├── response.js ------------------ 响应
~~~
  阅读源码除了能学到不经常使用的概念之外,还能学到各种软件开发思路,见识到各种类型的第三方库,对于提升自己的日常编码很有帮助。
## 一、package.json
  在package.json文件中,可以看到KOA的入口是 application.js。
~~~
"main": "lib/application.js",
~~~
  在devDependencies中,依赖的是ESLint和单元测试库。
  在dependencies中,好几个都是与通信有关的库,还有几个工具库,具体包括:
* [accepts](https://www.npmjs.com/package/accepts):为给定的 req 创建一个新的 Accepts 对象。
* [cache-content-type](https://www.npmjs.com/package/cache-content-type):与 mime-types 的 contentType 方法相同,但缓存了结果。
* [content-disposition](https://www.npmjs.com/package/content-disposition):创建和解析 HTTP Content-Disposition 头。
* [content-type](https://www.npmjs.com/package/content-type):根据 RFC 7231 创建和解析 HTTP Content-Type 头。
* [cookies](https://www.npmjs.com/package/cookies):一个用于获取和设置 HTTP(S) cookie 的 node.js 模块。
* [debug](https://www.npmjs.com/package/debug):一个模仿 Node.js 核心调试技术的小型 JavaScript 调试实用程序。
* [delegates](https://www.npmjs.com/package/delegates):创建一个委托实例,让一个对象可以直接访问其属性对象的属性和方法(在下一篇中会详细说明)。
* [destroy](https://www.npmjs.com/package/destroy):销毁一个流,确保流被销毁,处理不同的 API 和 Node.js 错误。
* [encodeurl](https://www.npmjs.com/package/encodeurl):将 URL 编码为百分比编码形式,不包括已编码的序列。
* [escape-html](https://www.npmjs.com/package/escape-html):将特殊字符转换成HTML实体。例如 foo & bar =》foo & bar。
* [fresh](https://www.npmjs.com/package/fresh):HTTP 响应新鲜度测试。
* [http-assert](https://www.npmjs.com/package/http-assert):状态码断言,像 Koa 中的 ctx.throw() 一样,但是有一个守卫。
* [http-errors](https://www.npmjs.com/package/http-errors):为 Express、Koa、Connect 等创建 HTTP 错误。
* [koa-compose](https://www.npmjs.com/package/koa-compose):组合给定的中间件,KOA的插件。
* [on-finished](https://www.npmjs.com/package/on-finished):当 HTTP 请求关闭、完成或出错时执行回调。
* [only](https://www.npmjs.com/package/only):指定属性白名单,然后只返回这几个属性。
* [parseurl](https://www.npmjs.com/package/parseurl):解析给定请求对象的 URL(req.url 属性)并返回结果,结果与 url.parse 相同。在 req.url 不变的同一个 req 上多次调用此函数将返回一个缓存的解析对象。
* [statuses](https://www.npmjs.com/package/statuses):返回已知 HTTP 状态代码的状态消息字符串。
* [type-is](https://www.npmjs.com/package/type-is):检查请求的内容类型是否是 content-type 中的一种类型。
* [vary](https://www.npmjs.com/package/vary):将给定的头字段添加到 res 的 Vary 响应头中。
## 二、application.js
  application.js是KOA的入口文件,在此文件中,会引入lib目录的另外3个文件,以及多个依赖库。
~~~
const debug = require('debug')('koa:application')
const onFinished = require('on-finished')
const response = require('./response')
const compose = require('koa-compose')
const context = require('./context')
const request = require('./request')
const statuses = require('statuses')
const Emitter = require('events')
const util = require('util')
const Stream = require('stream')
const http = require('http')
const only = require('only')
const { HttpError } = require('http-errors')
~~~
  在下面的代码中,去掉了大部分的方法体,只留下了方法名和注释。其中Application继承自Emitter,这样就能监听和触发自定义事件了。
~~~
/**
* 继承自 Emitter.prototype
*/
module.exports = class Application extends Emitter {
constructor (options) { }
/**
* 简写:
* http.createServer(app.callback()).listen(...)
*/
listen (...args) { }
/**
* JSON格式化
*/
toJSON () { return only(this, ['subdomainOffset', 'proxy', 'env']) }
/**
* Inspect implementation.
*/
inspect () { return this.toJSON() }
/**
* 使用给定的中间件 fn
*/
use (fn) { }
/**
* 请求处理程序回调,用于本机 http 服务器
*/
callback () { }
/**
* 在回调中处理请求
*/
handleRequest (ctx, fnMiddleware) { }
/**
* 初始化一个新的上下文
*/
createContext (req, res) { }
/**
* 默认错误处理程序
*/
onerror (err) { }
/**
* 帮助 TS 用户遵守 CommonJS、ESM、bundler mismatch
* @see https://github.com/koajs/koa/issues/1513
*/
static get default () { return Application }
}
/**
* 响应助手
*/
function respond (ctx) { }
/**
* 使库的消费者可以使用 HttpError,这样消费者就不会直接依赖于 `http-errors`
*/
module.exports.HttpError = HttpError
~~~
  在看过源码后,再来阅读一段简单的demo,在初始化KOA实例后,调用了Application的 use() 和 listen() 两个方法。
~~~
const Koa = require("koa");
const app = new Koa();
app.use(async (ctx, next) => {
ctx.body = "hello,KOA";
});
app.listen(3000);
~~~
**1)构造函数**
  在构造函数中,会声明各种参数,包括代理信息、环境变量等。
  其中[Object.create()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create)用于创建一个新对象,带着指定的原型对象和属性。
  因为在同一个应用中可能会有多个KOA实例,所以为了防止相互污染,通过Object.create()的拷贝将他们不再引用同一个地址。
~~~
constructor (options) {
super()
options = options || {} // 参数
this.proxy = options.proxy || false// 是否代码模式
this.subdomainOffset = options.subdomainOffset || 2
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For' // 代理 IP 头,默认为 X-Forwarded-For
this.maxIpsCount = options.maxIpsCount || 0 // 从代理 IP 标头读取的最大 IP,默认为 0(表示无穷大)
this.env = options.env || process.env.NODE_ENV || 'development' // 环境变量
if (options.keys) this.keys = options.keys
this.middleware = []
this.context = Object.create(context) // 创建一个新的context
this.request = Object.create(request)// 创建一个新的request
this.response = Object.create(response)// 创建一个新的response
// util.inspect.custom support for node 6+
/* istanbul ignore else */
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect
}
}
~~~
**2)use()**
  在KOA实例中,会维护一个中间件数组(middleware),在添加fn之前,会利用[typeof](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/typeof)判断其是否是函数类型。
~~~
use (fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
this.middleware.push(fn)
return this
}
~~~
  KOA的中间件采用的是著名的洋葱模型,后面会细说。
**3)listen()**
  listen()内部直接调用[http.createServer()](https://nodejs.org/dist/latest-v18.x/docs/api/http.html#httpcreateserveroptions-requestlistener)创建一个server,监听指定端口,并且每个请求都会回调当前实例的callback()方法。
~~~
listen (...args) {
const server = http.createServer(this.callback())
return server.listen(...args)
}
~~~
  在callback()方法中,会调用洋葱模型的compose()函数,监听error事件(回调error()函数),最后处理请求调用handleRequest()方法。
~~~
callback () {
// 包装所有的中间件,返回一个可执行函数,compose()是洋葱模型的实现
const fn = compose(this.middleware)
// 若未指定error事件,那么创建error事件监听器
if (!this.listenerCount('error')) {
this.on('error', this.onerror)
}
const handleRequest = (req, res) => {
// 为ctx包装Node原生的req和res,并且每个请求都是单独的ctx
const ctx = this.createContext(req, res)
// 实例的handleRequest(),并不是递归
return this.handleRequest(ctx, fn)
}
return handleRequest
}
~~~
**4)compose()**
  中间件通常用于完成一些全局的特定功能,例如权限验证、错误处理、日志添加等。
  下面是一个简单的中间件示例,用于处理500响应。
~~~
export default () => async (ctx, next) => {
try {
await next();
} catch (error) {
ctx.status = 500;
ctx.body = { error: String(error), stack: error.stack };
}
};
~~~
  compose()引用自koa-compose库,在该库中,中间件会被next()函数分成两部分,先执行next()之前的部分,在请求处理完毕后,再执行next()后面的部分。
  下图是官方给的一张中间件执行顺序示意图。
:-: ![](https://img.kancloud.cn/b3/53/b353e7e458d7542b366c1139b01b1a07_1169x1000.gif =800x)
  在下图中,每一层相当于是一个中间件,在request时,处理的是next()的前半部分,在response时,处理的是其后半部分。
:-: ![](https://img.kancloud.cn/d0/88/d088e7f855ba2ebeb2056dbd6f75a530_683x619.jpeg =400x)
  下面就是koa-compose库的所有代码,已加注释,为了便于理解,我已经将可执行的代码放到[codepen](https://codepen.io/strick/pen/XWVQmgQ)中,在线调试。
~~~
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!')
}
/**
* 返回一个函数
* context就是ctx
* next()函数就是下一个中间件函数
*/
return function (context, next) {
// 上一个中间件的索引
let index = -1
// 启动dispatch()函数,初始值是0
return dispatch(0)
function dispatch (i) {
// 以免在一个中间件内,调用多次next()
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// fn就是中间件函数
let fn = middleware[i]
// 中间件都已执行过一次,fn是undefined
if (i === middleware.length) fn = next
// 终止递归
if (!fn) return Promise.resolve()
try {
// fn是中间件,dispatch()就是下一个中间件的next()函数
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
~~~
  函数分为几步:
* 第一步是检查中间件数组和中间件的类型。
* 第二步是返回一个函数,参数是 ctx 和 next(),其中 next() 就是下一个中间件函数。
* 第三步是调用 dispatch(0) 启动中间件的运行,并且在一个中间件中,不允许多次调用 next() 函数。
* 第四步是递归地依次为每一个要执行的中间件传递参数,其第二个参数是下一个 dispatch() 函数。
  递归过程中的 dispatch() 其实就是中间件中的 next() 函数。
  Promise.resolve(fn(context, dispatch.bind(null, i + 1))) 会先运行一次中间件,然后遇到 next(),就去运行下一个中间件,递归终止后,再回溯处理中间件余下的逻辑。
**5)createContext()**
  每次HTTP请求都生成一个新的context,与其他请求中的context之间相互隔离。
~~~
createContext (req, res) {
// 每次HTTP请求都生成一个新的context
const context = Object.create(this.context)
const request = context.request = Object.create(this.request)
const response = context.response = Object.create(this.response)
context.app = request.app = response.app = this
// 挂载Node原生的req和res
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.originalUrl = request.originalUrl = req.url
// 可自定义的状态,例如koa-jwt库就使用了该属性
context.state = {}
return context
}
~~~
  context具备高内聚的特征,因为它能访问KOA提供的所有数据和方法。
  并且还预留了一个state属性,可用于传递自定义的状态值。
**6)handleRequest()**
  在 handleRequest() 函数中,会运行中间件函数,以及处理响应的不同情况。
~~~
/**
* 在回调中处理请求
* @param {*} ctx 上下文
* @param {*} fnMiddleware 可执行的中间件函数
* @returns
*/
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)
}
~~~
  respond()函数内容比较多,包括为格式化JSON格式的body,流类型的body调用pipe(),为HEAD请求加 Content-Length 头等。
  官方也提供了属性,来绕开上述这些处理。
~~~
function respond (ctx) {
// 允许绕过KOA的处理
if (ctx.respond === false) return
if (!ctx.writable) return
const res = ctx.res
let body = ctx.body
const code = ctx.status
// code不是已知的状态码
if (statuses.empty[code]) {
// strip headers
ctx.body = null
return res.end()
}
// HEAD请求
if (ctx.method === 'HEAD') {
// 加Content-Lengthh头
if (!res.headersSent && !ctx.response.has('Content-Length')) {
const { length } = ctx.response
if (Number.isInteger(length)) ctx.length = length
}
return res.end()
}
// status body
if (body == null) {
if (ctx.response._explicitNullBody) {
ctx.response.remove('Content-Type')
ctx.response.remove('Transfer-Encoding')
ctx.length = 0
return res.end()
}
if (ctx.req.httpVersionMajor >= 2) {
body = String(code)
} else {
body = ctx.message || String(code)
}
if (!res.headersSent) {
ctx.type = 'text'
ctx.length = Buffer.byteLength(body)
}
return res.end(body)
}
// 对body的三种类型采用不同的处理
if (Buffer.isBuffer(body)) return res.end(body)
if (typeof body === 'string') return res.end(body)
if (body instanceof Stream) return body.pipe(res)
// JSON格式的body
body = JSON.stringify(body)
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body)
}
res.end(body)
}
~~~
参考资料:
[koa源码解析](https://zhuanlan.zhihu.com/p/104621640)
[高质量 - Koa 源码解析](https://segmentfault.com/a/1190000021109975)
[超级详细的koa源码解析](https://juejin.cn/post/6855129007508488206)
*****
> 原文出处:
[博客园-Node.js躬行记](https://www.cnblogs.com/strick/category/1688575.html)
[知乎专栏-Node.js躬行记](https://zhuanlan.zhihu.com/pwnode)
已建立一个微信前端交流群,如要进群,请先加微信号freedom20180706或扫描下面的二维码,请求中需注明“看云加群”,在通过请求后就会把你拉进来。还搜集整理了一套[面试资料](https://github.com/pwstrick/daily),欢迎阅读。
![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200)
推荐一款前端监控脚本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不仅能监控前端的错误、通信、打印等行为,还能计算各类性能参数,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、扩展运算符和剩余参数
- 3、解构
- 4、模板字面量
- 5、对象字面量的扩展
- 6、Symbol
- 7、代码模块化
- 8、数字
- 9、字符串
- 10、正则表达式
- 11、对象
- 12、数组
- 13、类型化数组
- 14、函数
- 15、箭头函数和尾调用优化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、类
- 21、类的继承
- 22、Promise
- 23、Promise的静态方法和应用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基础实践
- 3、WebRTC视频通话
- 4、Web音视频基础
- CSS进阶
- 1、CSS基础拾遗
- 2、伪类和伪元素
- 3、CSS属性拾遗
- 4、浮动形状
- 5、渐变
- 6、滤镜
- 7、合成
- 8、裁剪和遮罩
- 9、网格布局
- 10、CSS方法论
- 11、管理后台响应式改造
- React
- 1、函数式编程
- 2、JSX
- 3、组件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表单
- 8、样式
- 9、组件通信
- 10、高阶组件
- 11、Redux基础
- 12、Redux中间件
- 13、React Router
- 14、测试框架
- 15、React Hooks
- 16、React源码分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基础
- 4、webpack进阶
- 5、Git
- 6、Fiddler
- 7、自制脚手架
- 8、VSCode插件研发
- 9、WebView中的页面调试方法
- Vue.js
- 1、数据绑定
- 2、指令
- 3、样式和表单
- 4、组件
- 5、组件通信
- 6、内容分发
- 7、渲染函数和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、数据类型
- 2、接口
- 3、类
- 4、泛型
- 5、类型兼容性
- 6、高级类型
- 7、命名空间
- 8、装饰器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系统和网络
- 3、命令行工具
- 4、自建前端监控系统
- 5、定时任务的调试
- 6、自制短链系统
- 7、定时任务的进化史
- 8、通用接口
- 9、微前端实践
- 10、接口日志查询
- 11、E2E测试
- 12、BFF
- 13、MySQL归档
- 14、压力测试
- 15、活动规则引擎
- 16、活动配置化
- 17、UmiJS版本升级
- 18、半吊子的可视化搭建系统
- 19、KOA源码分析(上)
- 20、KOA源码分析(下)
- 21、花10分钟入门Node.js
- 22、Node环境升级日志
- 23、Worker threads
- 24、低代码
- 25、Web自动化测试
- 26、接口拦截和页面回放实验
- 27、接口管理
- 28、Cypress自动化测试实践
- 29、基于Electron的开播助手
- Node.js精进
- 1、模块化
- 2、异步编程
- 3、流
- 4、事件触发器
- 5、HTTP
- 6、文件
- 7、日志
- 8、错误处理
- 9、性能监控(上)
- 10、性能监控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 监控系统
- 1、SDK
- 2、存储和分析
- 3、性能监控
- 4、内存泄漏
- 5、小程序
- 6、较长的白屏时间
- 7、页面奔溃
- 8、shin-monitor源码分析
- 前端性能精进
- 1、优化方法论之测量
- 2、优化方法论之分析
- 3、浏览器之图像
- 4、浏览器之呈现
- 5、浏览器之JavaScript
- 6、网络
- 7、构建
- 前端体验优化
- 1、概述
- 2、基建
- 3、后端
- 4、数据
- 5、后台
- Web优化
- 1、CSS优化
- 2、JavaScript优化
- 3、图像和网络
- 4、用户体验和工具
- 5、网站优化
- 6、优化闭环实践
- 数据结构与算法
- 1、链表
- 2、栈、队列、散列表和位运算
- 3、二叉树
- 4、二分查找
- 5、回溯算法
- 6、贪心算法
- 7、分治算法
- 8、动态规划
- 程序员之路
- 大学
- 2011年
- 2012年
- 2013年
- 2014年
- 项目反思
- 前端基础学习分享
- 2015年
- 再一次项目反思
- 然并卵
- PC网站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端学习之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 日志
- 2020