我們從以下幾個方面來學習Koa
- 創建應用程序函數
- 擴展res和req
- 中間件實現原理
創建應用程序函數
Koa 是依賴 node 原生的 http 模塊來實現 http server 的能力,原生 http 模塊可以通過幾行代碼就啓動一個監聽在 8080 端口的http服務,createServer 的第一個參數是一個回調函數,這個回調函數有兩個參數,一個是請求對象,一個是響應對象,可以根據請求對象的內容來決定響應數據的內容;
const http = require("http");
const server = http.createServer((req, res) => {
// 每一次請求處理的方法
console.log(req.url);
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello NodeJS");
});
server.listen(8080);
在 Koa 中,createServer 回調函數中的 req 和 res 會被保存到 ctx 對象上,伴隨整個處理請求的生命週期,Koa 源碼中的 request.js 和 response.js 就是對這兩個對象添加了大量便捷獲取數據和設置數據的方法,如獲取請求的方法、請求的路徑、設置返回數據體、設置返回狀態碼等操作。
而Koa在封裝創建應用程序的方法中主要執行了以下流程:
- 組織中間件(監聽請求之前)
- 生成context上下文對象
- 執行中間件
- 執行默認響應方法或者異常處理方法
// application.js
// 這個方法是封裝了http模塊提供的http.createServer和listen方法
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback() {
//組織中間件,在監聽請求之前完成的
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
//創建context上下文對象
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
// 默認狀態碼為404
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// 執行中間件
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
// 創建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.state = {};
return context;
}
擴展res和req
NodeJS中原生的res和req是http.IncomingMessage和http.ServerResponse的實例,Koa中則是自定義request和response對象,保持對原生的res和req引用,然後通過getter和setter方法實現擴展。
// application.js
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; // 保存原生 req 對象
context.res = request.res = response.res = res; // 保存原生 res 對象
request.ctx = response.ctx = context;
request.response = response; // response 拓展
response.request = request; // request 拓展
context.originalUrl = request.originalUrl = req.url;
context.state = {};
// 最終返回完整的context上下文對象
return context;
}
在Koa中要區別這兩組對象:
- request、response: Koa擴展的對象
- res、req: NodeJS原生對象
// request.js
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
}
此時已經可以採用這樣的方式訪問header屬性:
ctx.request.header
delegates 屬性代理
koa對response和request使用了屬性代理,使我們可以直接在context中使用request和response的方法,其中method方法是委託方法,getter方法用來委託getter,access方法委託getter+setter
// context.js
/**
* Response delegation.
*/
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('has')
.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')
.access('accept')
.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');
delegates 實現
對於 setter 和 getter方法,是通過調用對象上的 __defineSetter__ 和 __defineGetter__ 來實現的
// delegates/index.js
// getter
Delegator.prototype.getter = function(name){
var proto = this.proto;
var target = this.target;
this.getters.push(name);
proto.__defineGetter__(name, function(){
return this[target][name];
});
return this;
};
// setter
Delegator.prototype.setter = function(name){
var proto = this.proto;
var target = this.target;
this.setters.push(name);
proto.__defineSetter__(name, function(val){
return this[target][name] = val;
});
return this;
};
// access
Delegator.prototype.access = function(name){
return this.getter(name).setter(name);
};
// method
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;
};
中間件實現原理
在Koa中,通過app.use() 來註冊中間件,Koa支持三種不同類型的中間件:普通函數,async 函數,Generator函數,如果是Generator函數,那就用 convert 把函數包起來,然後在push到 this.middleware
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
convert作用
convert是用於將koa中以前基於generator寫法的中間件轉為基於promise寫法。convert()的源碼實現邏輯如下:
- convert 方法首先判斷傳入的中間件是否是一個函數,如果不是就拋出異常;
- 接着判斷是否是一個 generator 函數,如果不是就直接返回,不做處理;
-
利用co將 generator 函數形式的中間件轉成 promise 形式的中間件。
function convert (mw) { if (typeof mw !== 'function') { throw new TypeError('middleware must be a function') } // assume it's Promise-based middleware if ( mw.constructor.name !== 'GeneratorFunction' && mw.constructor.name !== 'AsyncGeneratorFunction' ) { return mw } const converted = function (ctx, next) { return co.call( ctx, mw.call( ctx, (function * (next) { return yield next() })(next) )) } converted._name = mw._name || mw.name return converted }中間件的執行
Koa中間件的執行流程主要通過koa-compose中的compose函數完成,基於洋葱圈模型:
koa-compose 的代碼很短,一共才不到50行,主要執行順序如下:// koa-compose function compose (middleware) { //傳入middleware數組 // 不是數組拋出異常 if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!') // 判斷每個middleware中的每一項是否為函數 // 不是函數拋出異常 for (const fn of middleware) { if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!') } // 返回一個函數 return function (context, next) { //index計數 let index = -1 return dispatch(0) //調用dispatch,從第一個中間件開始 function dispatch (i) { // i小於index,證明在中間件內調用了不止一次的next(),拋出錯誤 if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i // 更新index的值 let fn = middleware[i] //middleware中的函數,從第i個開始 if (i === middleware.length) fn = next //如果i走到最後一個的後面,就讓fn為next,此時fn為undefined if (!fn) return Promise.resolve()// 那麼這時候就直接resolve try { // 把下一個中間件作為當前中間件的next傳入 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } }