koa原始碼解讀
koa是有express原班人馬打造的基於node.js的下一代web開發框架。koa 1.0使用generator實現非同步,相比於回撥簡單和優雅和不少。koa團隊並沒有止步於koa 1.0, 隨著node.js開始支援async/await,他們又馬不停蹄的釋出了koa 2.0,koa2完全使用Promise並配合async/await來實現非同步,使得非同步操作更臻完美。
一、快速開始
koa使用起來非常簡單,安裝好node.js後執行以下命令安裝koa:
npm init npm install --save koa
一個簡單的Hello World程式開場,
//index.js const Koa = require('koa') const app = new Koa() app.use( async ctx => { ctx.body = 'Hello World' }) app.listen(3000,()=>{ console.log("server is running at 3000 port"); })
在命令列執行
node index.js
開啟瀏覽器檢視http://localhost:3000就可以看到頁面輸出的 Hello World。
中介軟體 middleware
Koa中使用 app.use()用來載入中介軟體,基本上Koa 所有的功能都是通過中介軟體實現的。
中介軟體的設計非常巧妙,多箇中間件會形成一個棧結構(middle stack),以”先進後出”(first-in-last-out)的順序執行。每個中介軟體預設接受兩個引數,第一個引數是 Context 物件,第二個引數是 next函式。只要呼叫 next函式,就可以把執行權轉交給下一個中介軟體,最裡層的中介軟體執行完後有會把執行權返回給上一級呼叫的中介軟體。整個執行過程就像一個剝洋蔥的過程。
比如你可以通過在所有中介軟體的頂端新增以下中介軟體來列印請求日誌到控制檯:
app.use(async function (ctx, next) { let start = new Date() await next() let ms = new Date() - start console.log('%s %s - %s', ctx.method, ctx.url, ms) })
常用的中介軟體列表可以在這裡找到: https://github.com/koajs/koa/wiki
二、koa原始碼解讀
開啟專案根目錄下的node_modules資料夾,開啟並找到koa的資料夾,如下所示:
開啟lib資料夾,這裡一共有4個檔案,
-
application.js - koa主程式入口
-
context.js - koa中介軟體引數ctx物件的封裝
-
request.js - request物件封裝
-
response.js - response物件封裝
我們這裡主要看下application.js,我這裡摘取了主要功能相關的 程式碼如下:
/**
* Shorthand for:
*
* http.createServer(app.callback()).listen(...)
*
* @param {Mixed} ...
* @return {Server}
* @api public
*/
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
/**
* Use the given middleware `fn`.
*
* Old-style middleware will be converted.
*
* @param {Function} fn
* @return {Application} self
* @api public
*/
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;
}
/**
* Return a request handler callback
* for node's native http server.
*
* @return {Function}
* @api public
*/
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;
}
/**
* Handle request in callback.
*
* @api private
*/
handleRequest(ctx, fnMiddleware) {
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);
}
通過註釋我們可以看出上面程式碼主要乾的事情是初始化http服務物件並啟動。我們注意到 callback()方法裡面有這樣一段程式碼 :
const fn = compose(this.middleware);
compose其實是Node模組koa-compose,它的作用是 將多箇中間件函式合併成一個大的中介軟體函式,然後呼叫這個中介軟體函式就可以依次執行新增的中介軟體函式,執行一系列的任務。遇到await next()時就停止當前中介軟體函式的執行並把執行權交個下一個中介軟體函式,最後next()執行完返回上一個中介軟體函式繼續執行下面的程式碼。
它是用了什麼黑魔法實現的呢?我們開啟node_modules/koa-compose/index.js,程式碼如下 :
function compose(middleware) {
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 {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
乍一看好難好複雜,沒事,我們一步一步的來梳理一下。
這個方法裡面的核心就是dispatch函式(廢話,整個compose方法就返回了一個函式)。沒有辦法簡寫,但是我們可以將dispatch函式類似遞迴的呼叫展開,以三個中介軟體為例:
第一次,此時第一個中介軟體被呼叫,dispatch(0),展開:
Promise.resolve(function(context, next){
//中介軟體一第一部分程式碼
await/yield next();
//中介軟體一第二部分程式碼}());
很明顯這裡的next指向dispatch(1),那麼就進入了第二個中介軟體;
第二次,此時第二個中介軟體被呼叫,dispatch(1),展開:
Promise.resolve(function(context, 中介軟體2){
//中介軟體一第一部分程式碼
await/yield Promise.resolve(function(context, next){
//中介軟體二第一部分程式碼
await/yield next();
//中介軟體二第二部分程式碼
}())
//中介軟體一第二部分程式碼}());
很明顯這裡的next指向dispatch(2),那麼就進入了第三個中介軟體;
第三次,此時第二個中介軟體被呼叫,dispatch(2),展開:
Promise.resolve(function(context, 中介軟體2){
//中介軟體一第一部分程式碼
await/yield Promise.resolve(function(context, 中介軟體3){
//中介軟體二第一部分程式碼
await/yield Promise(function(context){
//中介軟體三程式碼
}());
//中介軟體二第二部分程式碼
})
//中介軟體一第二部分程式碼}());
此時中介軟體三程式碼執行完畢,開始執行中介軟體二第二部分程式碼,執行完畢,開始執行中間一第二部分程式碼,執行完畢,所有中介軟體載入完畢。
再舉一個例子加深下理解。新建index.js並貼上如下程式碼:
const compose = require('koa-compose')
const middleware1 = (ctx, next) => {
console.log('here is in middleware1, before next:');
next();
console.log('middleware1 end');
}
const middleware2 = (ctx, next) => {
console.log('here is in middleware2, before next:');
next();
console.log('middleware2 end');
}
const middleware3 = (ctx, next) => {
console.log('here is in middleware3, before next:');
next();
console.log('middleware3 end');
}
const middlewares = compose([middleware1, middleware2, middleware3])
console.dir(middlewares())
在命令列輸入node index.js執行,輸出結果如下:
here is in middleware1, before next: here is in middleware2, before next: here is in middleware3, before next: middleware3 end middleware2 end middleware1 end Promise { undefined }
可以看到每個中介軟體都按照“剝洋蔥”的流程一次執行。當我們初始化app物件並呼叫app.use()時,就是在不斷往app.middleware數組裡新增中介軟體函式,當呼叫app.listen()再執行組合出來的函式。
-END-
轉載請註明來源
掃描下方二維碼,或者搜尋 前端提高班 關注公眾號,即可獲取最新走心文章
記得把我設為星標或置頂哦
在公眾號後臺回覆 前端資源 即可獲取最新前端開發資源