koa原理淺析
選取的版本為koa2
原文連結
koa的原始碼由四個檔案組成
application.js koa的骨架
context.js ctx的原型
request.js request的原型
response.js response的原型
基本用法
const Koa = require(`koa`);
const app = new Koa();
app.use(async ctx => {
ctx.body = `Hello World`;
});
app.listen(3000);
初始伺服器
利用http模組建立伺服器
const app = http.createServer((req, res) => {
...
})
app.listen(3000)
事實上koa把這些包在了其listen方法中
listen(...args) {
debug(`listen`);
const server = http.createServer(this.callback());
return server.listen(...args);
}
顯然this.callback()返回的是一個形如下面的函式
(req, res) => {}
上下文ctx
callback方法如下
callback() {
const fn = compose(this.middleware);
if (!this.listeners(`error`).length) this.on(`error`, this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
ctx在koa中事實上是一個包裝了request和response的物件,從createContext中可以看到起繼承自context
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.cookies = new Cookies(req, res, {
keys: this.keys,
secure: request.secure
});
request.ip = request.ips[0] || req.socket.remoteAddress || ``;
context.accept = request.accept = accepts(req);
context.state = {};
return context;
}
可以看到ctx.request繼承自request,ctx.response繼承自response,檢視response和request可以看到裡面大都是set和get方法(獲取query,設定header)等等。並且ctx代理了ctx.request和ctx.response的方法,在原始碼中可以看到
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`);
/**
* Request delegation.
*/
delegate(proto, `request`)
.method(`acceptsLanguages`)
.method(`acceptsEncodings`)
.method(`acceptsCharsets`)
.method(`accepts`)
.method(`get`)
.method(`is`)
.access(`querystring`)
.access(`idempotent`)
.access(`socket`)
.access(`search`)
.access(`method`)
.access(`query`)
.access(`path`)
.access(`url`)
.getter(`origin`)
.getter(`href`)
.getter(`subdomains`)
.getter(`protocol`)
.getter(`host`)
.getter(`hostname`)
.getter(`URL`)
.getter(`header`)
.getter(`headers`)
.getter(`secure`)
.getter(`stale`)
.getter(`fresh`)
.getter(`ips`)
.getter(`ip`);
所以我們可以直接這麼寫
ctx.url
等價於
ctx.request.url
中介軟體
我們再看一下callback函式,觀察發現compose模組十分的神奇,我暫且把它稱為是一個迭代器,它實現了中介軟體的順序執行
const fn = compose(this.middleware);
列印fn如下
function (context, next) {
// last called middleware #
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, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
最初接觸koa的時候我疑惑為什麼我寫了
ctx.body = `hello world`
並沒有ctx.response.end()之類的方法,事實上koa已經幫我們做了處理,在handleRequest方法中
const handleResponse = () => respond(ctx);
// fnMiddleware即為上面compose之後的fn
fnMiddleware(ctx).then(handleResponse).catch(onerror)
fnMiddleware返回的是一個promise,在中介軟體邏輯完成後在respond函式中最終去處理ctx.body
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);
}
錯誤處理
- (非首部)中介軟體層處理(我瞎起的)
對於每個中介軟體可能發生的錯誤,可以直接在該中介軟體捕獲
app.use((ctx, next) => {
try {
...
} catch(err) {
...
}
})
- (首部)中介軟體層處理
事實上,我們只要在第一個中介軟體新增try… catch… ,整個中介軟體組的錯誤都是可以捕獲的到的。
- (應用級別)頂層處理
app.on(`error`, (err) = {})
在上面中介軟體執行時看到,koa會自動幫我們捕獲錯誤並處理,如下
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
// 捕獲錯誤
return Promise.reject(err)
}
// 在ctx.onerror中處理
const onerror = err => ctx.onerror(err);
fnMiddleware(ctx).then(handleResponse).catch(onerror)
我們看ctx.onerror發現它事實上是出發app監聽的error事件
onerror(err) {
// delegate
this.app.emit(`error`, err, this);
假如我們沒有定義error回撥怎麼辦呢,koa也為我們定義了預設的錯誤處理函式
callback方法做了判斷
callback() {
...
if (!this.listeners(`error`).length) this.on(`error`, this.onerror);
...
}
全文完