Node.js理論實踐之《Koa原理淺析》

學習Koa框架以前,不得不提到Express。javascript

Express是一個基於Node.js平臺的極簡、靈活的 web 應用開發框架,主要基於 Connect 中間件,而且自身封裝了路由、視圖處理等功能,使用人數衆多。java

Koa相對更爲年輕,是 Express 原班人馬基於 ES6/7 異步流程控制從新開發的框架, 框架自身不包含任何中間件,不少功能須要藉助第三方中間件解決,解決了回調地獄和麻煩的錯誤處理問題。node

  • koa2與koa1的最大區別是koa2實現異步是經過async/await,koa1實現異步是經過generator + co,而express實現異步是經過回調函數的方式。
  • koa2與express 提供的API大體相同,express是大而全,內置了大多數的中間件,更讓人省心,koa2不綁定任何的框架,乾淨簡潔,小而精,更容易實現定製化,擴展性好。
  • express是沒有提供ctx來提供上下流服務,須要更多的手動處理,express自己是不支持洋蔥模型的數據流入流出能力的,須要引入其餘的插件。

源碼結構

咱們主要學習Koa2,首先看一下它的源碼結構: es6

image
koa2的核心文件: application.jscontext.jsrequest.jsresponse.js

  1. application.js:Application(或Koa)負責管理中間件,以及處理請求web

    • application.js是koa的入口文件,它向外導出了建立class實例的構造函數,它繼承了events,這樣就會賦予框架事件監聽和事件觸發的能力。 application還暴露了一些經常使用的api,好比toJSON、listen、use等等。
    • listen的實現原理其實就是對http.createServer進行了一個封裝,重點是這個函數中傳入的callback,它裏面包含了中間件的合併,上下文的處理,對res的特殊處理。
    • use是收集中間件,將多箇中間件放入一個緩存隊列中,而後經過koa-compose這個插件進行遞歸組合調用這一些列的中間件。
  2. context.js:Context維護了一個請求的上下文環境express

    • koa的應用上下文ctx,其實就一個簡單的對象暴露,裏面的重點在delegate,這個就是代理,這個就是爲了開發者方便而設計的。
    • ctx 主要的功能是代理 request 和 response 的功能,提供了對 request 和 response 對象的便捷訪問能力。 好比咱們要訪問ctx.response.status可是咱們經過delegate,能夠直接訪問ctx.status訪問到它。
  3. request.js:Request對req作了抽象和封裝api

  4. response.js:Response對res作了抽象和封裝數組

    這兩部分就是對原生的res、req的一些操做了,大量使用es6的get和set的一些語法,去取headers或者設置headers、還有設置body等等。promise

功能模塊

koa框架須要實現四個大模塊,分別是:緩存

  1. 封裝http模塊的server、建立Koa類構造函數
  2. 構造request、response、context對象
  3. 中間件機制和洋蔥模型的實現
  4. 錯誤捕獲和錯誤處理

封裝http模塊的server、建立Koa類構造函數:

經過application.js源碼得知,koa的服務器應用和端口監聽,其實就是基於node的http.createServer原生代碼進行了封裝。咱們來簡單實現一下這個功能。

let http = require('http');

class Application{    
    constructor() {        
        this.callbackFunc = ()=>{};
    }
    //開啓服務器實例並傳入callback回調函數
    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;
複製代碼

而後建立demo.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');
});
複製代碼

構造request、response、context對象:

request、response兩個功能模塊分別對node的原生request、response進行了一個功能的封裝,使用了getter和setter屬性,基於node的對象req/res對象封裝koa的request/response對象。

  • request.js 封裝了query、header、url、origin、path等,好比ctx.query就是返回url.parse(this.req.url, true).query。
  • response.js 封裝了status、body、message等,好比ctx.status就是返回res.statusCode。
  • context.js 再將request和response掛載到了ctx上。
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;
}
複製代碼

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

koa的中間件機制是洋蔥模型,多箇中間件經過use放進一個數組隊列,符合先進後出的原則。而後從外層開始執行,遇到next後進入隊列中的下一個中間件,全部中間件執行完後開始回幀,執行隊列中以前中間件中未執行的代碼部分。

image

示例:

const Koa = require('koa');
const app = new Koa();

//#1
app.use(async (ctx, next) => {
  console.log('中間件 1 進入');
  await next();
  console.log('中間件 1 退出');
});

//#2
app.use(async (ctx, next) => {
  console.log('中間件 2 進入');
  await next();
  console.log('中間件 2 退出');
});

//#3
app.use(async (ctx, next) => {
  console.log('中間件 3');
});

app.listen(3000);
複製代碼

訪問http://localhost:3000,控制檯打印:

中間件 1 進入
中間件 2 進入
中間件 3 
中間件 2 退出
中間件 1 退出
複製代碼

當程序運行到await next()的時候就會暫停當前程序,進入下一個中間件,處理完以後纔會仔回過頭來繼續處理。 也就是說,當一個請求進入,#1會被第一個和最後一個通過,#2則是被第二和倒數第二個通過,依次類推。

經過use傳進來的中間件是一個回調函數,回調函數的參數是ctx上下文和next,next其實就是控制權的交接棒,next的做用是中止運行當前中間件,將控制權交給下一個中間件, 執行下一個中間件的next()以前的代碼,當下一箇中間件運行的代碼遇到了next(),又會將代碼執行權交給下下箇中間件,當執行到最後一箇中間件的時候,控制權發生反轉, 開始回頭去執行以前全部中間件中剩下未執行的代碼,當最終全部中間件所有執行完後,會返回一個Promise對象,由於咱們的compose函數返回的是一個async的函數,async函數執行完後會返回一個Promise。

function _createNext(middleware, oldNext) {
    return async () => {
        await middleware(ctx, oldNext);
    }
}

compose() {
    return async (ctx) => {
        let next = async () => {
            return Promise.resolve();
        };
        for (let i = this.middlewares.length - 1; i >= 0; i--) {
            let currentMiddleware = this.middlewares[i];
            next = _createNext(currentMiddleware, next);
        }
        await next();
    };
}
複製代碼

錯誤捕獲和錯誤處理

錯誤處理和捕獲,分爲中間件的異常Koa框架框架層的異常

前者在application.jscallback方法中,根據es7的規範知道,async返回的是一個promise的對象實例,咱們若是想要捕獲promise的錯誤,只須要使用promise的catch方法,就能夠把全部的中間件的異常所有捕獲到。 相似這樣:

return fn(ctx).then(respond).catch(onerror);
複製代碼

後者,經過將koa的構造函數繼承events模塊,使其具有事件監聽on函數和事件觸發emit行爲的能力。

let EventEmitter = require('events');
class Application extends EventEmitter {}
複製代碼

這樣,當咱們建立koa實例的時候,就能夠加上on監聽函數。

let app = new Koa();

app.on('error', err => {
    console.log('error happends: ', err.stack);
});
複製代碼
相關文章
相關標籤/搜索