本文圍繞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'
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庫將中介軟體串聯起來執行,並具有可以逆回執行的能力。