
作者:會吃魚的貓咪
https://juejin.cn/post/6966432934756794405
koa的基礎結構
首先,讓我們認識一下koa架構的定位——koa是一個精簡的node架構:
- 它基于node原生req和res,封裝自定義的request和response對象,并基于它們封裝成一個統一的context對象。
- 它基于async/await(generator)的洋蔥模型實作了中間件機制。
koa架構的核心目錄如下:
── lib
├── application.js
├── context.js
├── request.js
└── response.js
// 每個檔案的具體功能
── lib
├── new Koa() || ctx.app
├── ctx
├── ctx.req || ctx.request
└── ctx.res || ctx.response
複制代碼
複制
undefined
koa源碼基礎骨架
application.js
application.js是koa的主入口,也是核心部分,主要幹了以下幾件事情:
- 完成了koa執行個體初始化的工作,啟動伺服器
- 實作了洋蔥模型的中間件機制
- 封裝了高内聚的context對象
- 實作了異步函數的統一錯誤處理機制
context.js
context.js主要幹了兩件事情:
- 完成了錯誤事件處理
- 代理了response對象和request對象的部分屬性和方法
request.js
request對象基于node原生req封裝了一系列便利屬性和方法,供處理請求時調用。是以當你通路ctx.request.xxx的時候,實際上是在通路request對象上的setter和getter。
response.js
response對象基于node原生res封裝了一系列便利屬性和方法,供處理請求時調用。是以當你通路ctx.response.xxx的時候,實際上是在通路response對象上的setter和getter。
4個檔案的代碼結構如下:
undefined
undefined
koa工作流
Koa整個流程可以分成三步:
- 初始化階段
new初始化一個執行個體,包括建立中間件數組、建立context/request/response對象,再使用use(fn)添加中間件到middleware數組,最後使用listen 合成中間件fnMiddleware,按照洋蔥模型依次執行中間件,傳回一個callback函數給http.createServer,開啟伺服器,等待http請求。結構圖如下圖所示:
undefined
- 請求階段
每次請求,createContext生成一個新的ctx,傳給fnMiddleware,觸發中間件的整個流程。3. 響應階段 整個中間件完成後,調用respond方法,對請求做最後的處理,傳回響應給用戶端。
koa中間件機制與實作
koa中間件機制是采用koa-compose實作的,compose函數接收middleware數組作為參數,middleware中每個對象都是async函數,傳回一個以context和next作為入參的函數,我們跟源碼一樣,稱其為fnMiddleware在外部調用this.handleRequest的最後一行,運作了中間件:
fnMiddleware(ctx).then(handleResponse).catch(onerror);
以下是
koa-compose
庫中的核心函數:
我們不禁會問:中間件中的
next
到底是什麼呢?為什麼執行
next
就進入到了下一個中間件了呢?中間件所構成的執行棧如下圖所示,其中
next
就是一個含有
dispatch
方法的函數。在第1個中間件執行
next
時,相當于在執行
dispatch(2)
,就進入到了下一個中間件的處理流程。因為
dispatch
傳回的都是
Promise
對象,是以在第n個中間件
await next()
時,就進入到了第n+1個中間件,而當第n+1個中間件執行完成後,可以傳回第n個中間件。但是在某個中間件中,我們沒有寫
next()
,就不會再執行它後面所有的中間件。運作機制如下圖所示:
undefined
koa-convert解析
在koa2中引入了koa-convert庫,在使用use函數時,會使用到convert方法(隻展示核心的代碼):
const convert = require('koa-convert');
module.exports = class Application extends Emitter {
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed';
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
}
複制代碼
複制
koa2架構針對koa1版本作了相容處理,中間件函數如果是
generator
函數的話,會使用
koa-convert
進行轉換為“類async函數”。首先我們必須了解
generator
和
async
的差別:
async
函數會自動執行,而
generator
每次都要調用next函數才能執行,是以我們需要尋找到一個合适的方法,讓
next()
函數能夠一直持續下去即可,這時可以将
generator
中
yield
的
value
指定成為一個
Promise
對象。下面看看
koa-convert
中的核心代碼:
const co = require('co')
const compose = require('koa-compose')
module.exports = convert
function convert (mw) {
if (typeof mw !== 'function') {
throw new TypeError('middleware must be a function')
}
if (mw.constructor.name !== 'GeneratorFunction') {
return mw
}
const converted = function (ctx, next) {
return co.call(ctx, mw.call(ctx, createGenerator(next)))
}
converted._name = mw._name || mw.name
return converted
}
複制代碼
複制
首先針對傳入的參數mw作校驗,如果不是函數則抛異常,如果不是
generator
函數則直接傳回,如果是
generator
函數則使用
co
函數進行處理。co的核心代碼如下:
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1);
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
複制代碼
複制
由以上代碼可以看出,co中作了這樣的處理:
- 把一個
封裝在一個generator
對象中Promise
- 這個
對象再次把它的Promise
也封裝出gen.next()
對象,相當于這個子Promise
對象完成的時候也重複調用Promise
gen.next()
- 當所有疊代完成時,對父
對象進行Promise
resolve
以上工作完成後,就形成了一個類async函數。
異步函數的統一錯誤處理機制
在koa架構中,有兩種錯誤的處理機制,分别為:
- 中間件捕獲
- 架構捕獲
undefined
中間件捕獲是針對中間件做了錯誤處理響應,如
fnMiddleware(ctx).then(handleResponse).catch(onerror)
,在中間件運作出錯時,會觸發onerror監聽函數。架構捕獲是在
context.js
中作了相應的處理
this.app.emit('error', err, this)
,這裡的
this.app
是對
application
的引用,當
context.js
調用
onerror
時,實際上是觸發
application
執行個體的
error
事件 ,因為
Application
類是繼承自
EventEmitter
類的,是以具備了處理異步事件的能力,可以使用
EventEmitter
類中對于異步函數的錯誤處理方法。
koa為什麼能實作異步函數的統一錯誤處理?因為async函數傳回的是一個Promise對象,如果async函數内部抛出了異常,則會導緻Promise對象變為reject狀态,異常會被catch的回調函數(onerror)捕獲到。如果await後面的Promise對象變為reject狀态,reject的參數也可以被catch的回調函數(onerror)捕獲到。
委托模式在koa中的應用
delegates庫由知名的 TJ 所寫,可以幫我們友善快捷地使用設計模式當中的委托模式,即外層暴露的對象将請求委托給内部的其他對象進行處理。
delegates 基本用法就是将内部對象的變量或者函數綁定在暴露在外層的變量上,直接通過 delegates 方法進行如下委托,基本的委托方式包含:
- getter:外部對象可以直接通路内部對象的值
- setter:外部對象可以直接修改内部對象的值
- access:包含 getter 與 setter 的功能
- method:外部對象可以直接調用内部對象的函數
delegates 原理就是__defineGetter__和__defineSetter__。在application.createContext函數中,被建立的context對象會挂載基于request.js實作的request對象和基于response.js實作的response對象。下面2個delegate的作用是讓context對象代理request和response的部分屬性和方法:
undefined
做了以上的處理之後,
context.request
的許多屬性都被委托在
context上
了,
context.response
的許多方法都被委托在
context
上了,是以我們不僅可以使用
this.ctx.request.xx
、
this.ctx.response.xx
取到對應的屬性,還可以通過
this.ctx.xx
取到
this.ctx.request
或
this.ctx.response
下挂載的
xx
方法。
我們在源碼中可以看到,response.js和request.js使用的是get set代理,而context.js使用的是delegate代理,為什麼呢?因為delegate方法比較單一,隻代理屬性;但是使用set和get方法還可以加入一些額外的邏輯處理。在context.js中,隻需要代理屬性即可,使用delegate方法完全可以實作此效果,而在response.js和request.js中是需要處理其他邏輯的,如以下對query作的格式化操作:
get query() {
const str = this.querystring;
const c = this._querycache = this._querycache || {};
return c[str] || (c[str] = qs.parse(str));
}
複制代碼
複制
到這裡,相信你對koa2的原理實作有了更深的了解吧?