Koa是一個新的 web 框架,由 Express 幕后的原班人馬打造, 致力于成為 web 應用和 API 開發領域中的一個更小、更富有表現力、更健壯的基石。 通過利用 async 函數, Koa 幫你丟棄回調函數,并有力地增強錯誤處理。 Koa 并沒有捆綁任何中間件,而是提供了一套優雅的方法,幫助您快速而愉快地編寫服務端應用程序。
Koa 中間件的作用
中間件的功能是可以訪問請求對象( request ),響應對象( response )和應用程序的請求-響應周期中的通過 next 對下一個中間件函數的調用。通俗來講, 利用這一特性在 next 之前對 request 進行處理, 而在 next 之后對 response 進行處理。
簡單應用程序
const Koa = require('koa');
const App = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
以上代碼是 Koa 官網上面的 簡單示例 , 接下來一起深入中間件機制的運行原理。
中間件應用 demo
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(2);
});
app.use(async (ctx, next) => {
console.log(3);
await next();
console.log(4);
});
app.use(async (ctx, next) => {
ctx.body = 'Hello, Koa';
});
app.listen(3001);
結合上面應用demo, 逐步剖析中間件運行原理。每當服務器接收一個客戶端請求時, 都會依次打印: 1, 3, 4, 2 。
中間件原理
注冊中間件函數
上面應用使用 use 進行注冊中間件函數, 看下 Koa 內部中間件的實現。
use(fn) {
// 省略部分代碼...
this.middleware.push(fn);
return this;
}
省略了部分校驗和轉換的代碼, use 函數最核心的就是 this.middleware.push(fn) 這一句。將我們注冊的中間件函數都緩存到 middleware 棧中, 并且返回了 this 自身, 方便進行鏈式調用。
上面的 demo 應用注冊了三個中間件函數,具體這些中間件函數什么時候執行以及如何執行, 繼續看。
創建 server 服務
上面 demo 引用調用 Koa 實例的 listen 方法, 開啟端口號為 3001 的服務, 看下 Koa 內部 listen 方法的實現。
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
內部使用了 Node 原生的 http 模塊, 通過 createServer 創建一個 Server 實例并監聽指定的端口號。 http.createServer(RequestListener) 接受請求偵聽器函數作為參數, RequestListener 函數接受 request 和 response 對象兩個參數。
所以, 知道 this.callback() 函數的調用返回一個函數, 并且這個函數接受 request 和 response 請求和響應對象。
callback 創建 RequestListener 請求偵聽器函數
上面說到, callback 函數的調用返回一個 RequestListener 請求偵聽器函數, 并且接受 請求對象( request )和響應對象( response )。
callback() {
// compose 為中間件運行的核心
const fn = compose(this.middleware);
// handleRequest 就是 callback 函數返回的函數
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
callback函數主要做了兩件事情:
- 使用 compose 函數對緩存中間件函數的棧做了一層校驗, 并且 返回了一個函數 。后文會詳細分析 compose 函數的實現。
- 創建一個 RequestListener 請求偵聽器函數, 并且返回出去。 如果有客戶端請求時, 就會先觸發請求偵聽器函數執行, 并且接受這次請求的 request 和 response 對象。
const ctx = this.createContext(req, res) 純碎做了一件根據請求的 request 和 response 創建了一個 ctx 上下文對象, 創建它們三者的互相引用關系等, 這不是這篇文章的重點, 可自行了解。。
然后通過 handleRequest 函數將 ctx 上下文對象和 compose 函數的結果作為參數進行處理, 那么 compose 函數主要做了什么呢?
compose
compose 是一個 koa-compose npm 包, 其內部核心代碼也就 20+ 行, 它提供了中間件 next 函數調用的核心承載, 看一下內部的代碼:
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!')
}
/**
* @param {Object} ctx
* @return {Promise}
* @api public
*/
return function fn (ctx, next) {
// 簡化了部分代碼
return dispatch(0)
function dispatch (i) {
let middlewareFn = middleware[i]
try {
return Promise.resolve(middlewareFn(ctx, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
所以, const fn = compose(this.middleware) 的調用主要做了一些對 middleware 及 middleware 棧內每一個中間件函數的校驗, 并返回 fn 函數。
下面結合 handleRequest 函數內部的處理來深入理解 fn 函數的執行過程。
handleRequest
每次客戶端有請求時, 都會調用 RequestListener 請求偵聽器函數, 并創建請求響應上下文對象后, 傳遞 上下文對象 和 fn 函數到 handleRequest 函數處理。所以每次請求都會處理一次, 每次請求都會依次觸發已注冊的中間件函數。
handleRequest(ctx, fn) {
// 省略無關代碼...
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
// 省略無關代碼...
return fn(ctx).then(handleResponse).catch(onerror);
}
fn(ctx) 接受上下文對象參數,執行的結果可以調用 .then , 不用想了吧, 八成返回一個 Promise 對象, 下面再進入到看下 fn 函數內部的實現。
內部調用了 dispatch(0) 根據下標取出 middleware 棧中的第一個中間件函數 middlewareFn :
async (ctx, next) => {
console.log(1);
await next();
console.log(2);
}
希望你對 bing 有深刻的理解。 MDN bind
然后執行第一個中間件函數, 將上下文對象( ctx ) 和 next ( dispatch.bind(null, i + 1) ) 作為參數傳遞給中間件函數。首先會執行 console.log(1) 打印 1 , 然后執行 await next() 將當前函數的 執行權 轉交給 dispatch.bind(null, i + 1) 函數執行。
相當于調用了 dispatch(1) , 則取出第二個中間件函數執行, 依次類推。
看圖輔助理解
洋蔥模型
當 dispatch(0) 出棧后則表示所有的中間件函數已依次執行完畢, 如果某個中間件執行出現錯誤, 就會拋出 Promise.reject 由外部的 onerror 函數處理, 如果沒有出現錯誤則調用 handleResponse 函數并轉交給 respond 函數處理 body 的數據格式, 這些不是本篇幅的重點。