Koa源碼解析

Koa是一款設計優雅的輕量級Node.js框架,它主要提供了一套巧妙的中間件機制與簡練的API封裝,所以源碼閱讀起來也十分輕鬆,不論你從事前端或是後端研發,相信都會有所收穫。前端

目錄結構

首先將源碼下載到本地,能夠看到Koa的源碼只包含下述四個文件:node

lib
├── application.js
├── context.js
├── request.js
└── response.js

application.js

application.js爲Koa的主程序入口文件,在package.json的main字段有定義。它主要負責HTTP服務的註冊、封裝請求相應對象,並初始化中間件數組並經過compose方法進行執行。git

context.js

context.js的核心工做爲將請求與響應方法集成到一個上下文(Context)中,上下文中的大多數方法都是直接委託到了請求與響應對象上,自己並沒作什麼改變,它能爲編寫Web應用程序提供便捷。github

request.js

request.js將http庫的request方法進行抽象與封裝,經過它能夠訪問到各類請求信息。json

response.js

response.js與request功能相似,它是對response對象的抽象與封裝。後端

中間件

示例

對於Koa的中間件機制相信你們都耳熟能詳了,如今讓咱們來看看源碼實現。在這裏仍是先舉一個最簡單的例子:api

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

app.use((ctx, next) => {
  console.log('enter 1');
  next();
  console.log('out 1');
});

app.use((ctx, next) => {
  console.log('enter 2');
  next();
  console.log('out 2');
});

app.use((ctx, next) => {
  console.log('enter 3');
  next();
  console.log('out 3');
});

app.listen(3000);

如今讓咱們來訪問應用:curl 127.0.0.1:3000,能夠看到如下輸出結果:數組

enter 1
enter 2
enter 3
out 3
out 2
out 1

next是什麼?

經過以上的結果進行分析,當咱們執行next()的時候,可能程序的執行權交給了下一個中間件,next函數會等待下一個中間件執行完畢,而後接着執行,這樣的執行機制被稱爲「洋蔥模型」,由於它就像請求穿過一層洋蔥同樣,先從外向內一層一層執行,再從內向外一層一層返回,而next就是進行下一層的一把鑰匙:
clipboard.png閉包

原理

聊完了理想,如今咱們來聊現實。首先來看看app.use函數:app

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

整個函數只作了一件事情,將中間件函數添加到了實例中的middleware數組,其餘的便是對類型進行校驗,若不爲函數則直接報TypeError,若爲生成器則發出deprecated警告並使用koa-convert[注1]對其轉化。

中間件在何時執行的呢?首先咱們找到listen的回調函數:

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

而後來看看這個神奇的callback函數:

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

函數首先將中間件使用koa-compose進行處理,那個compose究竟是個什麼呢?不如直接來看源碼吧(省略掉了註釋與類型檢測):

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

首先咱們把目光放到indexi兩個變量上,當執行執行compose(middleware)函數時,會返回一個閉包函數distpach(0),閉包函數執行時,dispatch函數內部的判斷邏輯以下:

  1. 若i小於等於index 則報出錯誤:'next() called multiple times'。
  2. 若i大於index時,將i賦予index,此時i與index相等。

邏輯很簡單,但這樣作的目的是什麼呢?倘若程序按着預期執行,每一箇中間件內部都執行next(),倘若有3箇中間件,那麼當每次執行dispatch(i)時,到Line8以前index與i的值分別爲:-1/0, 0/1, 1/2,能夠看出i始終要大於index,index的閉包變量每次在執行完函數後都會加1,所以能夠知道的是若同一個中間件執行了兩次,index就會等於i,再執行一次index就會大於i,由此可知,index的存在乎義在於限制next能執行不超過1次。

Line9Line11用於取出middleware中的當前中間件,若數組爲最大索引標識,則會將fn等於next函數,意味着將再執行一次越級的索引i + 1,因爲取不到值,因而就執行到Line11返回Promise.resolve()。

當函數執行到Line13,則會運行當前中間件,並將是否執行下一個中間件dispatch(i + 1)的決定權傳遞到next參數,將運行結果返回,返回函數的運行結果的意義在於每次執行next的返回結果都是下一個中間件的執行結果的Promise對象。

回到callback

讓咱們繼續看callback函數等剩餘邏輯:

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

首先來看看Line3,由於Application繼承與Emitter,故此方法是用於監聽實例中的error事件的,當listenerCount的數值爲0時,表示沒有監聽過,則註冊監聽函數。

接着生成一個handleRequest回調,當每一個請求過來時,都會建立ctx上下文對象,並將中間件函數傳入實例方法handleRequest,讓咱們來看看此時的處理函數:

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

在這裏多出了幾個函數:

  • on-finished,監聽請求是否正常結束。
  • respond, 當中間件執行完畢後,處理response對象的status與body的字段。

回到示例

回到示例,是否恍如隔日?如今的代碼還困擾你嗎?讓咱們稍做修改:

app.use((ctx, next) => {
  console.log('enter 1');
  next();
  console.log('out 1');
});

app.use((ctx, next) => {
  console.log('enter 2');
});

app.use((ctx, next) => {
  console.log('enter 3');
  next();
  console.log('out 3');
});

此時你能準確的知道執行結果嗎?此時打印順序爲:enter 1 -> enter 2 -> out 1。由於只有next纔是進入到下一中間件的鑰匙。若再將程序改一改:

app.use(async (ctx, next) => {
  console.log('enter 1');
  next();
  console.log('out 1');
});

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

此時執行結果爲:enter1 -> enter2 -> out 1 -> out2,這你能答對嗎?你不須要記住範式與結果,回想一下核心的compose函數:return Promise.resolve(fn(context, dispatch.bind(null, i + 1))),首先中間件全爲async函數,若使用await next(),則會等待下一個中間件返回resolve狀態纔會執行此代碼,若是某一個Promise中間件不使用await關鍵字呢?它會在主進程上進行排隊等待,等到函數執行棧返回到當前函數後當即執行。對於此示例來說,當進入到第二個中間件,遇到await關鍵字時,console.log('out 2')則不會再執行,而是進入到微任務隊列中,此時主進程已無其餘任務,則函數退出當前棧,返回到了第一個函數中,此時輸出out 1,當第一個中間件執行結束後,事件循環纔會將中間件2的微任務取出來執行,所以你見到了上述的輸出順序。

上下文

經過上述分析,咱們瞭解到http.createServer中有一個callback函數,它不只負責執行compose函數,也會調用createContext方法建立函數上下文,源碼以下:

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

能夠由這個函數得知,ctx對象包含了`context.js
request.js、response.js`的代碼。經過訪問req與res的源代碼,你們能夠發如今request與response對象中封裝了許許多多http庫的請求方法與各種工具函數,若對http底層實現感興趣的小夥伴能夠仔細讀一下request與response文件,不然多查閱幾遍官網文檔,大概瞭解其中的api便可。

而對於context.js,其實十分簡單,它也封裝了部分工具方法,並使用node-delegates進行委託方法與屬性,對於此類方法的時間,估計koa3會將這一部分進行重構爲Proxy吧。

註解

1. koa-convert轉化

在Koa版本號爲1.x時,中間件都是使用Generator實現的,所以能夠經過官方提供的koa-convert臨時對其進行轉化與兼容,基本用法爲:

function * legacyMiddleware (next) {
  // before
  yield next
  // after
}
app.use(convert(legacyMiddleware))

而後打開源碼發現,核心代碼大概以下:

function convert (mw) {
  return (ctx, next) => co.call(ctx, mw.call(ctx, createGenerator(next)))
}

convert函數將生成器經過co進行包裝爲Promise函數,在ctx上下文進行執行,並傳入next函數。

總結

凡是涉及到原理性的東西,感受本身很難避免自顧自說,用圖片進行可視化的方式會更加直觀,易於理解,但願以後本身多多使用圖片來闡述原理。

經過源碼分析,咱們知道了Koa的核心思想創建於中間件機制,它是一個設計十分簡潔、巧妙的Web框架,擴展性極強,egg.js就是創建於Koa之上的上層框架。

相關文章
相關標籤/搜索