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 官網上面的 簡單示例 , 接下來一起深入中介軟體機制的執行原理。
中介軟體應用 democonst 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 函式主要做了什麼呢?
composecompose 是一個 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 的資料格式, 這些不是本篇幅的重點。