2018年10月02日(週二)

Koa2源碼學習

Koa – 基於 Node.js 平臺的下一代 web 開發框架

簡單使用

const koa = require('koa');

const app = new koa();
app.use((context, next) => {
  // do some thing
});
app.listen(8080);

以上代碼構建了一個簡單的服務器,你可以在瀏覽器輸入 localhost:8080 來訪問
下面我們通過創建koa服務器,且發送一次HTTP請求來了解源碼

實例化koa前

在koa實例化前,先介紹四個對象

// lib/application.js
...
const context = require('./context');
const request = require('./request');
const response = require('./response');
...
// 實例化前,主要是引入koa三個對象 
  • Application對象:koa 應用對象,封裝了端口監聽,請求回調,中間件使用…
  • Context對象:上下文對象,封裝了Response、Request及HTTP原生response、request對象的方法和屬性
  • Request對象: koa Request對象,是對http.IncomingMessage的抽象
  • Response對象:koa Response對象,是對http.ServerResponse的抽象

Context

koa上下文對象,大部分操作都是通過ctx(context簡寫)完成的

Context對象包含兩部分:

  • 自身對象的屬性和方法
  • 通過delegate方法委託的Response對象及Request對象的屬性和方法
// lib/context.js
// context對象自身的方法及屬性
const proto = module.exports = {
  ...
  inspect() {
    if (this === proto) return this;
    return this.toJSON();
  },
   ...
}

// 委託的Request和Response對象的方法
delegate(proto, 'response')
  .method('attachment')
...

delegate(proto, 'request')
  .method('acceptsLanguages')
  ...

delegate方法實現

Request

對http.IncomingMessage的抽象,提供了很多方法和屬性,使操作更加方便

// lib/request.js
module.exports = {
  ...
  get header() {
    return this.req.headers;
  },
  set header(val) {
    this.req.headers = val;
  },
  ...
}

Response

對http.ServerResponse的抽象,提供了很多方法和屬性,使操作更加方便

// lib/response.js
module.exports = {
  ...
 get socket() {
    return this.res.socket;
  },
  get header() {
    const { res } = this;
    return typeof res.getHeaders === 'function'
      ? res.getHeaders()
      : res._headers || {};  // Node < 7.7
  },
  ...
}

實例化

...
// 應用代碼
const koa = require('koa');
const app = new koa();
...

實例化koa(即Application對象)時,執行constructor裏面代碼

// lib/application.js
...
 constructor() {
    super();
    ...
    this.middleware = []; 
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
	...
}
...
// 主要是賦值,這裏的middleware就是存儲中間件的數組

值得注意的是這裏僅僅是賦值,並沒有任何調用HTTP服務的代碼
Object.create語法參考

調用listen方法

// 應用代碼
app.listen(8080);

使用listen方法時,調用Application的listen方法

// lib/application.js
...
  listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
 ...
 // listen方法就是是HTTP的語法糖

這裏值得注意的是

  • this.callback返回值會自動添加到 "request"事件中去,每當有HTTP請求時,都會去執行
  • server.listen(…args)返回一個net.Server實例,可以控制該服務器,你可以通過返回值來控制當前服務器,例如關閉服務器
    node api:http_http_createservernet_server_listen

callback方法

在listen方法中,主要是this.callback方法,其他都是http基本用法

...
// lib/application.js
  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;
  }
...

callback重要的兩個部分:

  • compose初始化中間件
  • handleRequest加載中間件且處理響應

以下分別來介紹這兩個部分

compose方法

在koa中,中間件非常重要,絕大部分多功能都是由中間件來實現的,中間件重要的幾個特點:

  • 保證中間件的調用順序,使用next方法調用下一個中間件
  • 洋蔥圈模型
  • 支持async/await

讓我們看一下koa-compose是如何實現的

...
// koa-compose/index.js
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 {
        // 最重要的代碼 實現next方法和洋蔥圈
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
...

以上代碼中,最重要的dispatch函數,通過遞歸dispatch,來實現中間件的連續且按順序調用

以兩個中間件爲例子

app.use((context, next) => {
  // do some thing1
  next()
  // do some thing3
});
app.use((context, next) => {
  // do some thing2
});
  • 調用第一個中間件

  • 調用dispatch(0)

  • 調用fn(context, dispatch.bind(null, i + 1))
    next的實現:

    • 中間件需要兩個傳參,第一個參數爲context,第二個參數爲next,此時next爲dispatch.bind(null, i + 1),即next此時是第二個中間件。當你在第一個中間件內部執行next方法時,其實調用的是第二個中間件,依次類推;
  • 執行第一個中間件代碼:do some thing1

  • 調用了next方法

  • 執行第二個中間件代碼:do some thing2,後續無中間件,即返回

  • next返回後,再次執行第一個中間件 do some thing3

  • 執行完畢後返回
    這就是koa中間件的特性之一,洋蔥圈的原理

值得注意的是:

  • fn(context, dispatch.bind(null, i + 1)) ,這裏使用了bind方法,如果這裏不使用bind的方法話,第二個中間件會立即執行,無論你是否調用next方法;
  • 這裏的返回值都是Promise類型,爲了支持async/await語法
  • 在使用async函數後,可以阻塞異步操作,保證中間件執行順序
handleRequest方法

這裏調用了兩個方法createContext和handleRequest

// lib/application.js
...
    const handleRequest = (req, res) => {
      // 這裏接受的req,res參數爲http.IncomingMessage和http.ServerResponse的實例
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
  ...

createContext
createContext方法主要是創建context對象及賦值

// lib/application.js
...
 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;
  }
...

這裏值得注意的:

  • context和this.context不相等,即在使用中間件的時候,context和context.app.context是不相等的
  • 因爲this.callback是在每次"request"請求時調用的,每個request請求都會調用createContext方法,即每個請求的context都是不一樣的,可以放心操作

handleRequest

該方法主要是調用中間件及處理響應
// lib/application.js
...
  handleRequest(ctx, fnMiddleware) {
    // fnMiddleware爲compose處理後的返回函數,當調用該函數時,即調用整個中間件
    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); //此步驟執行中間件且處理響應,在中間件全部未拋出錯誤的下,對響應值進行處理,否則捕獲錯誤
  }
  ...

use方法

// 應用代碼
app.use((context, next) => {
  // do some thing1
  next()
  // do some thing3
});

使用use方法時,調用Application的use方法

// lib/application.js
...
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;
  }
 ...

創建一個簡單服務器及響應一次請求

下面按照"創建一個簡單服務器及響應一次請求"來闡述代碼執行順序

  • 實例化Application,聲明對象屬性、方法及賦值

  • 執行listen函數

  • 執行callback函數

    • 執行koa-compose的compose函數,返回一個匿名函數
    • 返回handleRequet函數,但並不執行,且此函數爲"request"事件的回調函數
  • 服務器收到請求,觸發"request"事件

  • 執行handleRequest函數

    • 執行createContext函數,生成上下文
    • 執行handleRequest函數(與上文的handleRequest不是一個函數,具體可以看代碼)
      • 生成處理響應函數和錯誤捕獲函數
      • 執行中間件
      • 中間件執行文完畢,成功則進入響應函數,錯誤則進入錯誤捕獲函數
        • 響應函數根據內容和狀態碼返回不同的值
        • 錯誤捕獲函數捕獲錯誤,拋出
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章