Koa 原始碼淺析
本文圍繞koa服務從啟動,到處理請求再到回覆響應這個過程對原始碼進行簡單的解析
在koa中ctx是貫穿整個請求過程的,它是這次請求原資訊的承載體,可以從ctx上獲取到request、response、cookie等,方便我們進行後續的計算處理。 ctx在實現上原本就是一個空物件,在koa服務起來時,往上掛載了很多物件和方法。當然開發者也可以自定義掛載的方法。 在 context.js
檔案中對ctx初始化了一些內建的物件和屬性,包括錯誤處理,設定cookie。
get cookies() { if (!this[COOKIES]) { this[COOKIES] = new Cookies(this.req, this.res, { keys: this.app.keys, secure: this.request.secure }); } return this[COOKIES]; }, set cookies(_cookies) { this[COOKIES] = _cookies; } 複製程式碼
相對於這種寫法,還有另外一種較為優雅的掛載方法。
// ./context.js delegate(proto, 'response') .method('attachment') .method('redirect') .method('remove') .method('vary') .method('set') .method('append') .method('flushHeaders') .access('status') .access('message') .access('body') .access('length') .access('type') .access('lastModified') .access('etag') .getter('headerSent') .getter('writable'); 複製程式碼
delegate方法的作用是將其他物件的方法或者屬性掛載在指定物件上,在這裡就是proto,也就是最初的ctx物件,屬性提供方就是第二個引數"response"。 method是代理方法,getter代理get,access代理set和get. 看delegate如何實現:
function Delegator(proto, target) { if (!(this instanceof Delegator)) return new Delegator(proto, target); this.proto = proto; this.target = target; this.methods = []; this.getters = []; this.setters = []; this.fluents = []; } Delegator.prototype.method = function(name){ var proto = this.proto; var target = this.target; this.methods.push(name); proto[name] = function(){ return this[target][name].apply(this[target], arguments); }; return this; }; 複製程式碼
delegate中的method的實現是在呼叫原屬性上指定方法時,轉而呼叫提供方的方法。這裡可以發現提供方也被收在了this上,這裡的不直接傳入一個物件而是將該物件賦值在原物件上的原因,我想應該是存放一個副本在原物件上,這樣可以通過原物件直接訪問到提供屬性的物件。
./context.js
中使用delegates為ctx賦值的過程並不完整,因為這裡的屬性提供方雖然是request和response, 但是是從 ./application.js
createContext方法中傳入,這樣delegates才算完成了工作
到這裡我們就可以看下平時用koa時常走的流程。
const Koa = require('koa'); const app = new Koa(); // response app.use(ctx => { ctx.body = 'Hello Koa'; }); app.listen(3000); 複製程式碼
基本上就是分為三步,例項化Koa,註冊中介軟體再監聽埠, 這裡正常能讓koa服務或者說一個http服務起的來的操作其實是在app.listen(...args)裡,是不是和想象中的有點差距, 看下原始碼實現。
// ./application.js ... listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); } ... 複製程式碼
在listen方法裡使用了http模組的createServer方法來啟動http服務,這裡相當於是聲明瞭一個http.Server例項,該例項也繼承於EventEmitter,是一個事件型別的伺服器,並監聽了該例項的request事件,意為當客戶端有請求發過來的時候,這個事件將會觸發,等價於如下程式碼
var http = require("http"); var server = new http.Server(); server.on("request", function(req, res){ // handle request }); server.listen(3000); 複製程式碼
這個事件有兩個引數req和res,也就是這次事件的請求和響應資訊。有點扯遠了,回到koa原始碼, 處理req和res引數的任務就交給了this.callback()的返回值來做,繼續看callback裡做了什麼
// 去除了一些不影響主流程的處理程式碼 callback() { const fn = compose(this.middleware); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; } handleRequest(ctx, fnMiddleware) { const handleResponse = () => respond(ctx); return fnMiddleware(ctx).then(handleResponse).catch(onerror); } 複製程式碼
callback返回一個函式由他來處理req和res,這個函式內部做了兩件事, 這兩件事分別在koa服務的初始化和響應時期完成,上述程式碼中compose中介軟體就是在服務初始化完成, 而當request事件觸發時,該事件會由callback返回的handleRequest方法處理,這個方法保持了對fn,也就是初始化過後中介軟體的應用, handleRequest先會初始化貫穿整個事件的ctx物件,這個時候就可以將ctx以此走入到各個中介軟體中處理了。
可以說koa到這裡主流程已經走一大半了,讓我們理一理經過簡單分析過的原始碼可以做到哪個地步(忽略錯誤處理)
- 響應http請求 √
- 生成ctx物件 √
- 運用中介軟體 √
- 返回請求 ×
如上我們已經可以做到將響應進入readly狀態,但還沒有返回響應的能力,後續會說道。在前三個過程中有兩個點需要注意,ctx和middleware,下面我們依次深入學習下這兩個關鍵點。
ctx是貫穿整個request事件的物件,它上面掛載瞭如req和res這種描述該次事件資訊的屬性,開發者也可以根據自己喜好,通過前置中介軟體掛載一些屬性上去。 ctx在koa例項createContext方法上建立並被完善,再由callback返回的handleRequest也就是響應request的處理函式消費。看下createContext原始碼
createContext(req, res) { 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; 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; context.state = {}; return context; } 複製程式碼
前三行依次聲明瞭context、request和response,分別繼承於koa例項的三個靜態屬性,這三個靜態屬性由koa自己定義,在上面有一些快捷操作方法,比如在Request靜態類上可以獲取通過query獲取查詢引數,通過URL解析url等,可以理解為request的工具庫,Response和Context同理。res和rep是node的原生物件,還記得嗎,這兩個引數是由http.Server()例項觸發request事件帶來的入參。 res是http.incomingMessage的例項而rep繼承於http.ServerResponse, 貼一張圖。

箭頭指向說明了從屬關係,有五個箭頭指向ctx表面ctx上有五個這樣的的屬性,可以很清楚看到ctx上各個屬性之間的關係。
接下來我們再來看看koa中的中介軟體,在koa中使用use方法可以註冊中介軟體.
use(fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); if (isGeneratorFunction(fn)) { deprecate('Support for generators will be removed in v3. ' + 'See the documentation for examples of how to convert old middleware ' + 'https://github.com/koajs/koa/blob/master/docs/migration.md'); fn = convert(fn); } debug('use %s', fn._name || fn.name || '-'); this.middleware.push(fn); return this; } 複製程式碼
兩件事,統一中介軟體格式,再將中介軟體推入中介軟體陣列中。 在koa2.0以後middleware都是使用async/await語法,使用generator function也是可以的,2.0以後版本內建了koa-convert,它可以根據 fn.constructor.name == 'GeneratorFunction'
36029e1b9263b94cba8a7c%2Flib%2Fapplication.js%23L105" rel="nofollow,noindex">check here .來判斷是legacyMiddleware還是modernMiddleware,並根據結果來做相應的轉換。 koa-convert的核心使用是co這個庫,它提供了一個自動的執行器,並且返回的是promise,generator function有了這兩個特性也就可以直接和async函式一起使用了。
回到koa原始碼來,callback中是這樣處理中介軟體陣列的
const fn = compose(this.middleware); 複製程式碼
這裡的compose也就是koa-compose模組,它負責將所有的中介軟體串聯起來,並保證執行順序。經典的洋蔥圈圖:

koa-compose模組的介紹只有簡單的一句話
Compose the given middleware and return middleware.
言簡意賅,就是組合中介軟體。貼上原始碼
function compose (middleware) { return function (context, next) { let index = -1 return dispatch(0) function dispatch (i) { if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } } 複製程式碼
compose先存入中介軟體陣列,從第一個開始執行依次resolve到最後一個,中介軟體函式簽名為 (ctx,next)=>{}
,在內部呼叫next就會間接喚起下一個中介軟體,也就是執行 dispatch.bind(null, i + 1)
,中介軟體執行順序如下(網上扒下來的圖)。

圖上是遇到yield,和執行next同理。 也不是所有的中介軟體都需要next,在最後一個middleware執行完畢後可以不呼叫next,因為這個時候已經走完了所有中介軟體的前置邏輯。當然這裡呼叫next也是可以的,是為了在所有前置邏輯執行完後有一個回撥。我們單獨使用koa-compose:
const compose = require("koa-compose"); let _ctx = { name: "ctx" }; const mw_a = async function (ctx, next) { console.log("this is step 1"); ctx.body = "lewis"; await next(); console.log("this is step 4"); } const mw_b = async function (ctx, next) { console.log("this is step 2"); await next(); console.log("this is step 3"); } const fn = compose([mw_a, mw_b]); fn(_ctx, async function (ctx) { console.log("Done", ctx) }); // => // // this is 1 // this is 2 // Done {name: "ctx", body: "lewis"} // this is 3 // this is 4 複製程式碼
compose返回的函式接受的引數不光是ctx,還可以接受一個函式作為走完所有中介軟體前置邏輯後的回撥。有特殊需求的開發者可以關注一下。 當然整個中介軟體執行完後會返回一個resolve狀態的promise,在這個回撥中koa用來告訴客戶端“響應已經處理完畢,請查收”,這個時候客戶端才結束等待狀態,這個過程的原始碼:
// handleRequest 的返回值 // 當中間件已經處理完畢後,交由handleResponse也就是respond方法來最後處理ctx const handleResponse = () => respond(ctx); fnMiddleware(ctx).then(handleResponse).catch(onerror); /** * 交付response */ function respond(ctx) { // allow bypassing koa if (false === ctx.respond) return; const res = ctx.res; if (!ctx.writable) return; let body = ctx.body; const code = ctx.status; // ignore body if (statuses.empty[code]) { // strip headers ctx.body = null; return res.end(); } if ('HEAD' == ctx.method) { if (!res.headersSent && isJSON(body)) { ctx.length = Buffer.byteLength(JSON.stringify(body)); } return res.end(); } // status body if (null == body) { body = ctx.message || String(code); if (!res.headersSent) { ctx.type = 'text'; ctx.length = Buffer.byteLength(body); } return res.end(body); } // responses if (Buffer.isBuffer(body)) return res.end(body); if ('string' == typeof body) return res.end(body); if (body instanceof Stream) return body.pipe(res); // body: json body = JSON.stringify(body); if (!res.headersSent) { ctx.length = Buffer.byteLength(body); } res.end(body); } 複製程式碼
以上程式碼對各種款式的status和ctx.body做了相應的處理,最關鍵的還是這一句 res.end(body)
,它呼叫了node原生response的end方法,來告訴伺服器本次的請求以回傳body來結束,也就是告訴伺服器此響應的所有報文頭及報文體已經發出,伺服器在此呼叫後認為這條資訊已經發送完畢,並且這個方法必須對每個響應呼叫一次。
總結
至此,koa整個流程已經走通,可以看到koa的關鍵點集中在ctx物件和中介軟體的運用上。 通過delegate將原生res和req的方法屬性代理至ctx上,再掛載koa內建的Request和Reponse,提供koa風格操作底層res和req的實現途徑和獲取請求資訊的工具方法。 中介軟體則是使用koa-compose庫將中介軟體串聯起來執行,並具有可以逆回執行的能力。