天天看點

技術分享 | Koa架構源碼分析 Koa中間件(middleware)實作探索首先用一段簡單的代碼了解一下中間件執行順序。3、 context的來源

一、Why Koa ?

koa 是由 Express 原班人馬打造的,緻力于成為一個更小、更富有表現力、更健壯的 Web 架構。 使用 koa 編寫 web 應用,通過組合不同的 generator,可以免除重複繁瑣的回調函數嵌套, 并極大地提升錯誤處理的效率。koa 不在核心方法中綁定任何中間件, 它僅僅提供了一個輕量優雅的函數庫,使得編寫 Web 應用變得得心應手。
  • async await寫法,讓異步更加優雅。(代碼是寫給人看的,順便讓機器執行而已)
  • 良好的抽象,簡潔的api,簡單、吊炸天的中間件機制。
  • 強大的社群,GayHub上數不勝數的中間件。

二、小試牛刀之源碼檔案一覽

npm上download下koa之後打開koa/lib檔案夾可以看到四個檔案,足以證明koa架構是多麼簡潔。(不過koa架構依賴了很多node_modules)
技術分享 | Koa架構源碼分析 Koa中間件(middleware)實作探索首先用一段簡單的代碼了解一下中間件執行順序。3、 context的來源
  • application.js 入口檔案,也是骨架檔案,建立一個服務。
  • context.js app 的 context 對象, 傳入中間件的上下文對象。
  • request.js app 的請求對象,包含請求相關的一些屬性。
  • response.js app 的響應對象,包含響應相關的一些屬性。

下面大緻總的分為三章節來分享一下koa源碼。

1、Hello Koa

首先看一下原生node和koa分别是如何建立一個server。

/**

*原生Node建立server

*/

const http = require('http');

const server = http.createServer((req, res) => {

  res.statusCode = 200;

  res.setHeader('Content-Type', 'text/plain');

  res.end('Hello World\n');

});

server.listen(3000);

/**

*koa建立server

*/

const Koa = require('koa');

const app = new Koa();

app.use(ctx => {

  ctx.body = 'Hello Koa';

});

app.listen(3000);           
看一下源碼中koa究竟都做了什麼           
//1.首先建立了一個Application的類,繼承了Emitter類,然後暴露出去。

module.exports = class Application extends Emitter {

  constructor() {

    super();

  }

}

//2.類下面有一個listen方法,建立http服務

 listen(...args) {

    debug('listen');

    const server = http.createServer(this.callback());

    return server.listen(...args);

 }

           

在Hello Koa中,引入這個類,然後執行個體化這個類,在使用下面的listen即搭建起了一個簡單的koa服務。

然後講一下初始化koa的時候,這個類的下面有一個use方法,也就是我們添加中間件的方法。
use(fn) {

      //如果use裡面傳的不是方法,直接錯誤抛出去

    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');  

      //如果是星号函數,用一個庫轉成async await方法(去相容koa1的中間件)

    if (isGeneratorFunction(fn)) { 

      //如果是generator函數,用convert轉成async函數,然後執行。也是就是koa2能支援koa1的中間件。

      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); //轉async await方法

    }

    debug('use %s', fn._name || fn.name || '-');

    this.middleware.push(fn); //koa把所有的中間件都放在一個數組裡面

    return this;

  }           

如代碼所示,koa把所有的中間件放在一個數組裡面,然後koa2也是相容generator函數,為了相容koa1的中間件。

callback方法Koa處理請求的方法。

callback() {

    const fn = compose(this.middleware); //組裝中間件

    if (!this.listeners('error').length) this.on('error', this.onerror);

    const handleRequest = (req, res) => {

      const ctx = this.createContext(req, res);

      return this.handleRequest(ctx, fn);

    };

    return handleRequest;

  }

           

接下來就是看一下callback中compose如何處理了中間價和中間件的執行機制

 Koa中間件(middleware)實作探索

首先用一段簡單的代碼了解一下中間件執行順序。

const Koa = require('koa')

const app = new Koa()

app.use(async function m1(ctx, next) {

  console.log('1')

  await next();

  console.log('1 end')

})

app.use(async function m2(ctx, next) {

  console.log('2')

  await next();

  console.log('2 end')

})

app.use(async function m3(ctx, next) {

  console.log('3')

  ctx.body = 'Hello World'

  console.log('3 end')

})

app.listen(3000);

// 列印的順序是

// 1、2、3、3end、2end、1end           
koa通過compose方法把中間件封裝成一個promise對象,然後遞歸執行。使中間件數組變成洋蔥圈,能遞歸線性處理上下文。
下面是官方的配圖:
技術分享 | Koa架構源碼分析 Koa中間件(middleware)實作探索首先用一段簡單的代碼了解一下中間件執行順序。3、 context的來源

我們主要看一下這個庫:

function compose (middleware) {

  return function (context, next) {

    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)));

       /**

          dispatch.bind(null, i + 1) 利用了js的bind繼承

          其實可以寫成 

          function() {

            dispatch(i + 1)

          }

         一個等待被執行的函數,然後用next()去執行下一個中間件。

       */

      } catch (err) {

        return Promise.reject(err)

      }

    }

  }

}

//通過調用第一個函數,層層遞歸調用,然後達到洋蔥模型

//大白話講一下就是,compose方法把所有的中間的執行的時候,把第一個中間件封裝成了一個psomise對象執行,然後同時在中間件裡面寫next()方法去遞歸執行下一個中間件。

           
技術分享 | Koa架構源碼分析 Koa中間件(middleware)實作探索首先用一段簡單的代碼了解一下中間件執行順序。3、 context的來源
這裡的 Promise.resolve(fn(..)) 幫助我們異步執行的中間件函數,這裡的next函數就解釋了為什麼Koa的中間件調用是遞歸執行的,它遞歸調用了 dispatch 函數來周遊數組中的,同時,所有的中間件函數享有同一個 ctx。

然後我根據compose處理後的中間件,大緻中間件執行中理想的樣子如圖:

(async function(){

    // middleware1

    console.log('1');

    await  (async function(){

        // middleware2

        console.log('2');

        await ( async function(){

            // middleware3

            ...

            console.log('3');

            console.log('3 end');

            // ...middleware3

        })

        console.log('2 end')

        // ...middleware2

    })

    console.log('1 end');

    // ...middleware1

})()           

組裝成promise的中間件然後指派到fn上去執行。

fn(ctx).then(handleResponse).catch(onerror)。 來看看這一句,fn 之前說過了,是所有的中間件函數的 “集合”, 用這一個中間件來表示整個處理過程。 同時 fn 也是一個 async 函數,執行結果傳回一個 promise 對象, 同時 handleResponse 作為其 resolved 函數,onerror 是 rejected 函數。 也就是說koa中捕捉錯誤不是用try catch 而且是用promise中的catch。

3、 context的來源

context 使用node原生的 http 的監聽回調函數中的 req res 來進行進一步的封裝,意味着對于每一個 http 請求,koa都會建立一個 context 并共享給所有的全局中間件使用,當所有的中間件執行完過後,會将最後要傳回的所有資料統一再交還給 res 進行傳回,是以我們在每一個中間件中才能夠從 ctx 中取得自己所需要的 req 中的資料進行處理,最後 ctx 再把要傳回的 body 給原生的 res 進行傳回。

每一個請求都有唯一一個 context 對象,所有的關于請求和傳回的東西都統一放在裡面 createContext 方法将 req res 進一步封裝。

const context = Object.create(this.context); 

// 建立一個對象,使之擁有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的原則,context 必須是作為一個臨時對象而存在,所有的東西都必須封進一個對象中,是以 app req res 三個屬性就此誕生。

三、講到最後

本文主要圍繞“koa2的骨架”、“剖析koa中間件”、“ctx的來源”這三個子產品來闡述了koa的原理和實作。并根據思路流程漸進梳理講解了一些細節思路和比較關鍵的内容點,以及通過展示部分關鍵代碼講述了koa中間件的執行原理。文中肯定會有一些不夠嚴謹的思考和錯誤,歡迎大家指正,有興趣歡迎一起探讨和改進~

最後,感謝您的閱讀!

原文釋出時間為:2018年06月24日

原文作者:掘金

本文來源: 

掘金 https://juejin.im/entry/5b3a29f95188256228041f46

如需轉載請聯系原作者

繼續閱讀