KOA2框架原理解析和實現

koa是一個基於node實現的一個新的web框架,它是由express框架的原班人馬打造的。它的特色是優雅、簡潔、表達力強、自由度高。它更express相比,它是一個更輕量的node框架,由於它全部功能都經過插件實現,這種插拔式的架構設計模式,很符合unix哲學。node

koa框架如今更新到了2.x版本,本文從零開始,按部就班,講解koa2的框架源碼結構和實現原理,展現和詳解koa2框架源碼中的幾個最重要的概念,而後手把手教你們親自實現一個簡易的koa2框架,幫助你們學習和更深層次的理解koa2,看完本文之後,再去對照koa2的源碼進行查看,相信你的思路將會很是的順暢。es6

本文所用的框架是koa2,它跟koa1不一樣,koa1使用的是generator+co.js的執行方式,而koa2中使用了async/await,所以本文的代碼和demo須要運行在node 8版本及其以上,若是讀者的node版本較低,建議升級或者安裝babel-cli,用其中的babel-node來運行本文涉及到的代碼。web

koa源碼結構

上圖是koa2的源碼目錄結構的lib文件夾,lib文件夾下放着四個koa2的核心文件:application.js、context.js、request.js、response.js。express

application.js

application.js是koa的入口文件,它向外導出了建立class實例的構造函數,它繼承了events,這樣就會賦予框架事件監聽和事件觸發的能力。application還暴露了一些經常使用的api,好比toJSON、listen、use等等。設計模式

listen的實現原理其實就是對http.createServer進行了一個封裝,重點是這個函數中傳入的callback,它裏面包含了中間件的合併,上下文的處理,對res的特殊處理。api

use是收集中間件,將多箇中間件放入一個緩存隊列中,而後經過koa-compose這個插件進行遞歸組合調用這一些列的中間件。數組

context.js

這部分就是koa的應用上下文ctx,其實就一個簡單的對象暴露,裏面的重點在delegate,這個就是代理,這個就是爲了開發者方便而設計的,好比咱們要訪問ctx.repsponse.status可是咱們經過delegate,能夠直接訪問ctx.status訪問到它。promise

request.js、response.js

這兩部分就是對原生的res、req的一些操做了,大量使用es6的get和set的一些語法,去取headers或者設置headers、還有設置body等等,這些就不詳細介紹了,有興趣的讀者能夠自行看源碼。瀏覽器

實現koa2的四大模塊

上文簡述了koa2源碼的大致框架結構,接下來咱們來實現一個koa2的框架,筆者認爲理解和實現一個koa框架須要實現四個大模塊,分別是:緩存

  • 封裝node http server、建立Koa類構造函數

  • 構造request、response、context對象

  • 中間件機制和剝洋蔥模型的實現

  • 錯誤捕獲和錯誤處理

下面咱們就逐一分析和實現。

模塊一:封裝node http server和建立Koa類構造函數

閱讀koa2的源碼得知,實現koa的服務器應用和端口監聽,其實就是基於node的原生代碼進行了封裝,以下圖的代碼就是經過node原生代碼實現的服務器監聽。

let http = require('http');
let server = http.createServer((req, res) => {
    res.writeHead(200);
    res.end('hello world');
});
server.listen(3000, () => {    
    console.log('listenning on 3000');
});

咱們須要將上面的node原生代碼封裝實現成koa的模式:

const http = require('http');
const Koa = require('koa');
const app = new Koa();
app.listen(3000);

實現koa的第一步就是對以上的這個過程進行封裝,爲此咱們須要建立application.js實現一個Application類的構造函數:

let http = require('http');
class Application {    
    constructor() {        
        this.callbackFunc;
    }
    listen(port) {        
        let server = http.createServer(this.callback());
        server.listen(port);
    }
    use(fn) {
        this.callbackFunc = fn;
    }
    callback() {
        return (req, res) => {
            this.callbackFunc(req, res);
        };
    }
}
module.exports = Application;

而後建立example.js,引入application.js,運行服務器實例啓動監聽代碼:

let Koa = require('./application');
let app = new Koa();
app.use((req, res) => {
    res.writeHead(200);
    res.end('hello world');
});
app.listen(3000, () => {
    console.log('listening on 3000');
});

如今在瀏覽器輸入localhost:3000便可看到瀏覽器裏顯示「hello world」。如今第一步咱們已經完成了,對http server進行了簡單的封裝和建立了一個能夠生成koa實例的類class,這個類裏還實現了app.use用來註冊中間件和註冊回調函數,app.listen用來開啓服務器實例並傳入callback回調函數,第一模塊主要是實現典型的koa風格和搭好了一個koa的簡單的架子。接下來咱們開始編寫和講解第二模塊。

模塊二:構造request、response、context對象

閱讀koa2的源碼得知,其中context.js、request.js、response.js三個文件分別是request、response、context三個模塊的代碼文件。context就是咱們平時寫koa代碼時的ctx,它至關於一個全局的koa實例上下文this,它鏈接了request、response兩個功能模塊,而且暴露給koa的實例和中間件等回調函數的參數中,起到承上啓下的做用。

request、response兩個功能模塊分別對node的原生request、response進行了一個功能的封裝,使用了getter和setter屬性,基於node的對象req/res對象封裝koa的request/response對象。咱們基於這個原理簡單實現一下request.js、response.js,首先建立request.js文件,而後寫入如下代碼:

let url = require('url');
module.exports = {
    get query() {
        return url.parse(this.req.url, true).query;
    }
};

這樣當你在koa實例裏使用ctx.query的時候,就會返回url.parse(this.req.url, true).query的值。看源碼可知,基於getter和setter,在request.js裏還封裝了header、url、origin、path等方法,都是對原生的request上用getter和setter進行了封裝,筆者再也不這裏一一實現。

接下來咱們實現response.js文件代碼模塊,它和request原理同樣,也是基於getter和setter對原生response進行了封裝,那咱們接下來經過對經常使用的ctx.body和ctx.status這個兩個語句當作例子簡述一下若是實現koa的response的模塊,咱們首先建立好response.js文件,而後輸入下面的代碼:

module.exports = {
    get body() {
        return this._body;
    },
    set body(data) {
        this._body = data;
    },
    get status() {
        return this.res.statusCode;
    },
    set status(statusCode) {
        if (typeof statusCode !== 'number') {
            throw new Error('something wrong!');
        }
        this.res.statusCode = statusCode;
    }
};

以上代碼實現了對koa的status的讀取和設置,讀取的時候返回的是基於原生的response對象的statusCode屬性,而body的讀取則是對this._body進行讀寫和操做。這裏對body進行操做並無使用原生的this.res.end,由於在咱們編寫koa代碼的時候,會對body進行屢次的讀取和修改,因此真正返回瀏覽器信息的操做是在application.js裏進行封裝和操做。

如今咱們已經實現了request.js、response.js,獲取到了request、response對象和他們的封裝的方法,而後咱們開始實現context.js,context的做用就是將request、response對象掛載到ctx的上面,讓koa實例和代碼能方便的使用到request、response對象中的方法。如今咱們建立context.js文件,輸入以下代碼:

let proto = {};

function delegateSet(property, name) {
    proto.__defineSetter__(name, function (val) {
        this[property][name] = val;
    });
}

function delegateGet(property, name) {
    proto.__defineGetter__(name, function () {
        return this[property][name];
    });
}

let requestSet = [];
let requestGet = ['query'];

let responseSet = ['body', 'status'];
let responseGet = responseSet;

requestSet.forEach(ele => {
    delegateSet('request', ele);
});

requestGet.forEach(ele => {
    delegateGet('request', ele);
});

responseSet.forEach(ele => {
    delegateSet('response', ele);
});

responseGet.forEach(ele => {
    delegateGet('response', ele);
});

module.exports = proto;

context.js文件主要是對經常使用的request和response方法進行掛載和代理,經過context.query直接代理了context.request.query,context.body和context.status代理了context.response.body與context.response.status。而context.request,context.response則會在application.js中掛載

原本能夠用簡單的setter和getter去設置每個方法,可是因爲context對象定義方法比較簡單和規範,在koa源碼裏能夠看到,koa源碼用的是defineSetterdefineSetter來代替setter/getter每個屬性的讀取設置,這樣作主要是方便拓展和精簡了寫法,當咱們須要代理更多的res和req的方法的時候,能夠向context.js文件裏面的數組對象裏面添加對應的方法名和屬性名便可。

目前爲止,咱們已經獲得了request、response、context三個模塊對象了,接下來就是將request、response全部方法掛載到context下,讓context實現它的承上啓下的做用,修改application.js文件,添加以下代碼:

let http = require('http');
let context = require('./context');
let request = require('./request');
let response = require('./response');

createContext(req, res) {       
   let ctx = Object.create(this.context);
   ctx.request = Object.create(this.request);
   ctx.response = Object.create(this.response);
   ctx.req = ctx.request.req = req;
   ctx.res = ctx.response.res = res; 
   return ctx;
}

能夠看到,咱們添加了createContext這個方法,這個方法是關鍵,它經過Object.create建立了ctx,並將request和response掛載到了ctx上面,將原生的req和res掛載到了ctx的子屬性上,往回看一下context/request/response.js文件,就能知道當時使用的this.res或者this.response之類的是從哪裏來的了,原來是在這個createContext方法中掛載到了對應的實例上,構建了運行時上下文ctx以後,咱們的app.use回調函數參數就都基於ctx了。

模塊三:中間件機制和剝洋蔥模型的實現

目前爲止咱們已經成功實現了上下文context對象、 請求request對象和響應response對象模塊,還差一個最重要的模塊,就是koa的中間件模塊,koa的中間件機制是一個剝洋蔥式的模型,多箇中間件經過use放進一個數組隊列而後從外層開始執行,遇到next後進入隊列中的下一個中間件,全部中間件執行完後開始回幀,執行隊列中以前中間件中未執行的代碼部分,這就是剝洋蔥模型,koa的中間件機制。

koa的剝洋蔥模型在koa1中使用的是generator + co.js去實現的,koa2則使用了async/await + Promise去實現的,接下來咱們基於async/await + Promise去實現koa2中的中間件機制。首先,假設當koa的中間件機制已經作好了,那麼它是能成功運行下面代碼的:

let Koa = require('../src/application');

let app = new Koa();

app.use(async (ctx, next) => {
    console.log(1);
    await next();
    console.log(6);
});

app.use(async (ctx, next) => {
    console.log(2);
    await next();
    console.log(5);
});

app.use(async (ctx, next) => {
    console.log(3);
    ctx.body = "hello world";
    console.log(4);
});

app.listen(3000, () => {
    console.log('listenning on 3000');
});

運行成功後會在終端輸出123456,那就能驗證咱們的koa的剝洋蔥模型是正確的。接下來咱們開始實現,修改application.js文件,添加以下代碼:

compose() {
        return async ctx => {
            function createNext(middleware, oldNext) {
                return async () => {
                    await middleware(ctx, oldNext);
                }
            }
            let len = this.middlewares.length;
            let next = async () => {
                return Promise.resolve();
            };
            for (let i = len - 1; i >= 0; i--) {
                let currentMiddleware = this.middlewares[i];
                next = createNext(currentMiddleware, next);
            }
            await next();
        };
    }

    callback() {
        return (req, res) => {
            let ctx = this.createContext(req, res);
            let respond = () => this.responseBody(ctx);
            let onerror = (err) => this.onerror(err, ctx);
            let fn = this.compose();
            return fn(ctx);
        };
    }

koa經過use函數,把全部的中間件push到一個內部數組隊列this.middlewares中,剝洋蔥模型能讓全部的中間件依次執行,每次執行完一箇中間件,遇到next()就會將控制權傳遞到下一個中間件,下一個中間件的next參數,剝洋蔥模型的最關鍵代碼是compose這個函數:

compose() {
        return async ctx => {
            function createNext(middleware, oldNext) {
                return async () => {
                    await middleware(ctx, oldNext);
                }
            }
            let len = this.middlewares.length;
            let next = async () => {
                return Promise.resolve();
            };
            for (let i = len - 1; i >= 0; i--) {
                let currentMiddleware = this.middlewares[i];
                next = createNext(currentMiddleware, next);
            }
            await next();
        };
    }

createNext函數的做用就是將上一個中間件的next當作參數傳給下一個中間件,而且將上下文ctx綁定當前中間件,當中間件執行完,調用next()的時候,其實就是去執行下一個中間件。

for (let i = len - 1; i >= 0; i--) {
        let currentMiddleware = this.middlewares[i];
        next = createNext(currentMiddleware, next);
 }

上面這段代碼其實就是一個鏈式反向遞歸模型的實現,i是從最大數開始循環的,將中間件從最後一個開始封裝,每一次都是將本身的執行函數封裝成next當作上一個中間件的next參數,這樣當循環到第一個中間件的時候,只須要執行一次next(),就能鏈式的遞歸調用全部中間件,這個就是koa剝洋蔥的核心代碼機制。

到這裏咱們總結一下上面全部剝洋蔥模型代碼的流程,經過use傳進來的中間件是一個回調函數,回調函數的參數是ctx上下文和next,next其實就是控制權的交接棒,next的做用是中止運行當前中間件,將控制權交給下一個中間件,執行下一個中間件的next()以前的代碼,當下一箇中間件運行的代碼遇到了next(),又會將代碼執行權交給下下箇中間件,當執行到最後一箇中間件的時候,控制權發生反轉,開始回頭去執行以前全部中間件中剩下未執行的代碼,這整個流程有點像一個僞遞歸,當最終全部中間件所有執行完後,會返回一個Promise對象,由於咱們的compose函數返回的是一個async的函數,async函數執行完後會返回一個Promise,這樣咱們就能將全部的中間件異步執行同步化,經過then就能夠執行響應函數和錯誤處理函數。

當中間件機制代碼寫好了之後,運行咱們的上面的例子,已經能輸出123456了,至此,咱們的koa的基本框架已經基本作好了,不過一個框架不能只實現功能,爲了框架和服務器實例的健壯,還須要加上錯誤處理機制。

模塊四:錯誤捕獲和錯誤處理

要實現一個基礎框架,錯誤處理和捕獲必不可少,一個健壯的框架,必須保證在發生錯誤的時候,可以捕獲到錯誤和拋出的異常,並反饋出來,將錯誤信息發送到監控系統上進行反饋,目前咱們實現的簡易koa框架尚未能實現這一點,咱們接下加上錯誤處理和捕獲的機制。

throw new Error('oooops');

基於如今的框架,若是中間件代碼中出現如上錯誤異常拋出,是捕獲不到錯誤的,這時候咱們看一下application.js中的callback函數的return返回代碼,以下:

return fn(ctx).then(respond);

能夠看到,fn是中間件的執行函數,每個中間件代碼都是由async包裹着的,並且中間件的執行函數compose返回的也是一個async函數,咱們根據es7的規範知道,async返回的是一個promise的對象實例,咱們若是想要捕獲promise的錯誤,只須要使用promise的catch方法,就能夠把全部的中間件的異常所有捕獲到,修改後callback的返回代碼以下:

return fn(ctx).then(respond).catch(onerror);

如今咱們已經實現了中間件的錯誤異常捕獲,可是咱們還缺乏框架層發生錯誤的捕獲機制,咱們但願咱們的服務器實例能有錯誤事件的監聽機制,經過on的監聽函數就能訂閱和監聽框架層面上的錯誤,實現這個機制不難,使用nodejs原生events模塊便可,events模塊給咱們提供了事件監聽on函數和事件觸發emit行爲函數,一個發射事件,一個負責接收事件,咱們只須要將koa的構造函數繼承events模塊便可,構造後的僞代碼以下:

let EventEmitter = require('events');
class Application extends EventEmitter {}

繼承了events模塊後,當咱們建立koa實例的時候,加上on監聽函數,代碼以下:

let app = new Koa();

app.on('error', err => {
    console.log('error happends: ', err.stack);
});

這樣咱們就實現了框架層面上的錯誤的捕獲和監聽機制了。總結一下,錯誤處理和捕獲,分中間件的錯誤處理捕獲和框架層的錯誤處理捕獲,中間件的錯誤處理用promise的catch,框架層面的錯誤處理用nodejs的原生模塊events,這樣咱們就能夠把一個服務器實例上的全部的錯誤異常所有捕獲到了。至此,咱們就完整實現了一個輕量版的koa框架了。

結尾

前爲止,咱們已經實現了一個輕量版的koa框架了,咱們實現了封裝node http server、建立Koa類構造函數、構造request、response、context對象、中間件機制和剝洋蔥模型的實現、錯誤捕獲和錯誤處理這四個大模塊,理解了這個輕量版koa的實現原理,再去看koa2的源碼,你就會發現一切都豁然開朗,koa2的源碼無非就是在這個輕量版基礎上加了不少工具函數和細節的處理,限於篇幅筆者就再也不一一介紹了。

 

原文連接:https://www.jianshu.com/p/a64e098ae017

相關文章
相關標籤/搜索