本次分析的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()后面的部分。
  下圖是官方給的一張中間件執行順序示意圖。
:-: 
  在下圖中,每一層相當于是一個中間件,在request時,處理的是next()的前半部分,在response時,處理的是其后半部分。
:-: 
  下面就是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),歡迎閱讀。

推薦一款前端監控腳本:[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年
- 2024年
- 日志
- 2020