Koa2源碼閱讀筆記

引言

最近空閒時間讀了一下Koa2的源碼;在閱讀Koa2(version 2.2.0)的源碼的過程當中,個人感覺是代碼簡潔、思路清晰(不得不佩服大神的水平)。
下面是我讀完以後的一些感覺。node

Koa的設計理念

Koa 是一個輕量級的、極富表現力的 http 框架。
一個web request會經過 Koa 的中間件棧,來動態完成 response 的處理。
Koa2 採用了 async 和 await 的語法來加強中間件的表現力。
Koa 不在內核方法中綁定任何中間件,它僅僅提供了一個輕量優雅的函數庫。git

Koa基本組成

Koa源碼很是精簡,只有四個文件:es6

  • application.js:框架入口;負責管理中間件,以及處理請求
  • context.js:context對象的原型,代理request與response對象上的方法和屬性
  • request.js:request對象的原型,提供請求相關的方法和屬性
  • response.js:response對象的原型,提供響應相關的方法和屬性

application.js

// application.js

module.exports = class Application extends Emitter {
  constructor() {
    super();

    this.proxy = false; // 是否信任 proxy header 參數,默認爲 false
    
    this.middleware = []; //保存經過app.use(middleware)註冊的中間件
    
    this.subdomainOffset = 2; // 子域默認偏移量,默認爲 2
    
    this.env = process.env.NODE_ENV || 'development'; // 環境參數,默認爲 NODE_ENV 或 ‘development’
    
    this.context = Object.create(context); //context模塊,經過context.js建立
    
    this.request = Object.create(request); //request模塊,經過request.js建立
    
    this.response = Object.create(response); //response模塊,經過response.js建立
  }

  // ...
}

application.js 是 koa 的入口主要文件,暴露應用的 class, 這個 class 繼承自 EventEmitter ,這裏能夠看出跟 koa1.x 的不一樣,koa1.x 是用的是構造函數的方式,koa2 大量使用 es6 的語法。調用的時候就跟 koa1.x 有區別github

var koa = require('koa');
// koa 1.x
var app = koa();
// koa 2.x
// 使用class必須使用new來調用
var app = new koa();

application.js除了上面的的構造函數外,還暴露了一些公用的api,好比兩個常見的,一個是listen,一個是useweb

use函數

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

use函數作的事很簡單:註冊一箇中間件fn,其實就是將fn放入middleware數組。json

listen函數

// application.js

listen(...args) {
  debug('listen');
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

listen方法首先會經過this.callback方法來返回一個函數做爲http.createServer的回調函數,而後進行監聽。咱們已經知道,http.createServer的回調函數接收兩個參數:reqres,下面來看this.callback的實現:api

// application.js

callback() {
  const fn = compose(this.middleware);
    
  if (!this.listeners('error').length) this.on('error', this.onerror);
    
  const handleRequest = (req, res) => {
    res.statusCode = 404;
    const ctx = this.createContext(req, res);
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fn(ctx).then(handleResponse).catch(onerror);
  };
    
  return handleRequest;
}

首先,callback方法把全部middleware進行了組合,使用了koa-compose,咱們來看一下koa-compose的代碼:數組

// koa-compose

function compose (middleware) {
// 傳入的middleware必須是一個數組
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
// 傳入的middleware的每個元素都必須是函數
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  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]
      //下面兩行代碼是處理最後一箇中間件還有next的狀況的,其實就是直接resolve出來
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve() 
      try {
        // 這裏就是傳入next執行中間件代碼了
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

能夠看到koa-compose基本就是個dispatch函數的遞歸調用。其中最重要的就是下面這段代碼:cookie

return Promise.resolve(fn(context, function next () {
  return dispatch(i + 1)
}))

這段代碼等價於:app

fn(context, function next () {
 return dispatch(i + 1)
})
return Promise.resolve()

這裏middlewareFunction的第二個參數(也就是next)是動態傳遞進去的信使,它會調取dispatch(index)執行下一個的middleware。最後會返回一個Resolved(已完成)狀態的Promise對象。這個對象的做用咱們稍後再說。

咱們先暫時回到callback方法裏面,前面說了它先對middleware進行了組合,生成了一個函數fn
而後,callback方法返回http.createServer所須要的回調函數handleRequest

handleRequest函數,先把http code默認設置爲404,接着利用createContext函數把node返回的req和res進行了封裝建立出context
而後經過onFinished(res, onerror)監聽http response,當請求結束時執行回調。這裏傳入的回調是context.onerror(err),即當錯誤發生時才執行。
最後返回 fn(ctx).then(handleResponse).catch(onerror)的執行結果,這裏的fn函數就是就是組合全部middleware後生成的函數,調用它執行全部middleware後會返回前面提到的Resolved(已完成)狀態的Promise對象,以後執行響應處理函數respond(ctx)respond函數裏面也主要是一些收尾工做,例如判斷http code爲空如何輸出啦,http method是head如何輸出啦,body返回是流或json時如何輸出;代碼就不貼了,感興趣的小夥伴本身能夠去看一下),當拋出異常時一樣使用 context.onerror(err)處理。

咱們能夠看看createContext函數

// 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.cookies = new Cookies(req, res, {
    keys: this.keys,
    secure: request.secure
  });
  request.ip = request.ips[0] || req.socket.remoteAddress || '';
  context.accept = request.accept = accepts(req);
  context.state = {};
  return context;
}

createContext建立context的時候,還會同時建立request和response,經過下圖能夠比較直觀地看到全部這些對象之間的關係。
圖片描述

圖中:

  • 最左邊一列表示每一個文件的導出對象
  • 中間一列表示每一個Koa應用及其維護的屬性
  • 右邊兩列表示對應每一個請求所維護的一些列對象
  • 黑色的線表示實例化
  • 紅色的線表示原型鏈
  • 藍色的線表示屬性

經過上面的分析,咱們已經能夠大概得知Koa處理請求的過程:當請求到來的時候,會經過 req 和 res 來建立一個 context (ctx) ,而後執行中間件。

content.js

content.js 主要的功能提供了對requestresponse對象的方法與屬性便捷訪問能力。
其中使用了node-delegates(有興趣的能夠看一下源碼),將context.requestcontext.response上的方法與屬性代理到context上。
在源碼中,咱們能夠看到:

// context.js

delegate(proto, 'response')
  .method('attachment')
  // ...
  .access('status')
  // ...
  .getter('writable');

delegate(proto, 'request')
  .method('acceptsLanguages')
  // ...
  .access('querystring')
  // ...
  .getter('ip');

request.js

request.js 封裝了請求相關的屬性以及方法。經過 application.js 中的createContext方法,代理對應的 request 對象。

const request = context.request = Object.create(this.request);
// ...
context.req = request.req = response.req = req;
// ...
request.response = response;

request.req爲原生的請求對象,在 request.js 中屬性的獲取都是經過 ths.req來獲取的(即 request.req)。

response.js

response.js 封裝了響應相關的屬性以及方法。與 request 相同,經過createContext方法代理對應的 response 對象。

const response = context.response = Object.create(this.response);
// ...
context.res = request.res = response.res = res;
// ...
response.request = request;

結語

關於Koa2的源碼就先分析到這,但願對你們有所幫助。若有不一樣的見解,歡迎交流!

相關文章
相關標籤/搜索