Koa2源碼學習
Koa – 基於 Node.js 平臺的下一代 web 開發框架
簡單使用
const koa = require('koa');
const app = new koa();
app.use((context, next) => {
// do some thing
});
app.listen(8080);
以上代碼構建了一個簡單的服務器,你可以在瀏覽器輸入 localhost:8080 來訪問
下面我們通過創建koa服務器,且發送一次HTTP請求來了解源碼
實例化koa前
在koa實例化前,先介紹四個對象
// lib/application.js
...
const context = require('./context');
const request = require('./request');
const response = require('./response');
...
// 實例化前,主要是引入koa三個對象
- Application對象:koa 應用對象,封裝了端口監聽,請求回調,中間件使用…
- Context對象:上下文對象,封裝了Response、Request及HTTP原生response、request對象的方法和屬性
- Request對象: koa Request對象,是對http.IncomingMessage的抽象
- Response對象:koa Response對象,是對http.ServerResponse的抽象
Context
koa上下文對象,大部分操作都是通過ctx(context簡寫)完成的
Context對象包含兩部分:
- 自身對象的屬性和方法
- 通過delegate方法委託的Response對象及Request對象的屬性和方法
// lib/context.js
// context對象自身的方法及屬性
const proto = module.exports = {
...
inspect() {
if (this === proto) return this;
return this.toJSON();
},
...
}
// 委託的Request和Response對象的方法
delegate(proto, 'response')
.method('attachment')
...
delegate(proto, 'request')
.method('acceptsLanguages')
...
Request
對http.IncomingMessage的抽象,提供了很多方法和屬性,使操作更加方便
// lib/request.js
module.exports = {
...
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
...
}
Response
對http.ServerResponse的抽象,提供了很多方法和屬性,使操作更加方便
// lib/response.js
module.exports = {
...
get socket() {
return this.res.socket;
},
get header() {
const { res } = this;
return typeof res.getHeaders === 'function'
? res.getHeaders()
: res._headers || {}; // Node < 7.7
},
...
}
實例化
...
// 應用代碼
const koa = require('koa');
const app = new koa();
...
實例化koa(即Application對象)時,執行constructor裏面代碼
// lib/application.js
...
constructor() {
super();
...
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
...
}
...
// 主要是賦值,這裏的middleware就是存儲中間件的數組
值得注意的是這裏僅僅是賦值,並沒有任何調用HTTP服務的代碼
Object.create語法參考
調用listen方法
// 應用代碼
app.listen(8080);
使用listen方法時,調用Application的listen方法
// lib/application.js
...
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
...
// listen方法就是是HTTP的語法糖
這裏值得注意的是
- this.callback返回值會自動添加到 "request"事件中去,每當有HTTP請求時,都會去執行
- server.listen(…args)返回一個net.Server實例,可以控制該服務器,你可以通過返回值來控制當前服務器,例如關閉服務器
node api:http_http_createserver 和 net_server_listen
callback方法
在listen方法中,主要是this.callback方法,其他都是http基本用法
...
// lib/application.js
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
...
callback重要的兩個部分:
- compose初始化中間件
- handleRequest加載中間件且處理響應
以下分別來介紹這兩個部分
compose方法
在koa中,中間件非常重要,絕大部分多功能都是由中間件來實現的,中間件重要的幾個特點:
- 保證中間件的調用順序,使用next方法調用下一個中間件
- 洋蔥圈模型
- 支持async/await
讓我們看一下koa-compose是如何實現的
...
// koa-compose/index.js
return 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 {
// 最重要的代碼 實現next方法和洋蔥圈
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
...
以上代碼中,最重要的dispatch函數,通過遞歸dispatch,來實現中間件的連續且按順序調用
以兩個中間件爲例子
app.use((context, next) => {
// do some thing1
next()
// do some thing3
});
app.use((context, next) => {
// do some thing2
});
-
調用第一個中間件
-
調用dispatch(0)
-
調用fn(context, dispatch.bind(null, i + 1))
next的實現:- 中間件需要兩個傳參,第一個參數爲context,第二個參數爲next,此時next爲dispatch.bind(null, i + 1),即next此時是第二個中間件。當你在第一個中間件內部執行next方法時,其實調用的是第二個中間件,依次類推;
-
執行第一個中間件代碼:do some thing1
-
調用了next方法
-
執行第二個中間件代碼:do some thing2,後續無中間件,即返回
-
next返回後,再次執行第一個中間件 do some thing3
-
執行完畢後返回
這就是koa中間件的特性之一,洋蔥圈的原理
值得注意的是:
- fn(context, dispatch.bind(null, i + 1)) ,這裏使用了bind方法,如果這裏不使用bind的方法話,第二個中間件會立即執行,無論你是否調用next方法;
- 這裏的返回值都是Promise類型,爲了支持async/await語法
- 在使用async函數後,可以阻塞異步操作,保證中間件執行順序
handleRequest方法
這裏調用了兩個方法createContext和handleRequest
// lib/application.js
...
const handleRequest = (req, res) => {
// 這裏接受的req,res參數爲http.IncomingMessage和http.ServerResponse的實例
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
...
createContext
createContext方法主要是創建context對象及賦值
// lib/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;
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和this.context不相等,即在使用中間件的時候,context和context.app.context是不相等的
- 因爲this.callback是在每次"request"請求時調用的,每個request請求都會調用createContext方法,即每個請求的context都是不一樣的,可以放心操作
handleRequest
該方法主要是調用中間件及處理響應
// lib/application.js
...
handleRequest(ctx, fnMiddleware) {
// fnMiddleware爲compose處理後的返回函數,當調用該函數時,即調用整個中間件
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx); // 組裝返回值的方法
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror); //此步驟執行中間件且處理響應,在中間件全部未拋出錯誤的下,對響應值進行處理,否則捕獲錯誤
}
...
use方法
// 應用代碼
app.use((context, next) => {
// do some thing1
next()
// do some thing3
});
使用use方法時,調用Application的use方法
// lib/application.js
...
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;
}
...
創建一個簡單服務器及響應一次請求
下面按照"創建一個簡單服務器及響應一次請求"來闡述代碼執行順序
-
實例化Application,聲明對象屬性、方法及賦值
-
執行listen函數
-
執行callback函數
- 執行koa-compose的compose函數,返回一個匿名函數
- 返回handleRequet函數,但並不執行,且此函數爲"request"事件的回調函數
-
服務器收到請求,觸發"request"事件
-
執行handleRequest函數
- 執行createContext函數,生成上下文
- 執行handleRequest函數(與上文的handleRequest不是一個函數,具體可以看代碼)
- 生成處理響應函數和錯誤捕獲函數
- 執行中間件
- 中間件執行文完畢,成功則進入響應函數,錯誤則進入錯誤捕獲函數
- 響應函數根據內容和狀態碼返回不同的值
- 錯誤捕獲函數捕獲錯誤,拋出