【從零到壹】Koa 從理解到實現

image.png

點擊查看文中的相關源碼git

根據官網的介紹,Koa 是一個新的 Web 框架,致力於成爲 Web 應用和 API 開發領域中的一個更小、更富有表現力和更健壯的基石。github

經過 async 函數,Koa 不只遠離回調地獄,同時還有力地加強了錯誤處理。並且,一個關鍵的設計點是在其低級中間件層中提供了高級「語法糖」,這包括諸如內容協商,緩存清理,代理支持和重定向等常見任務的方法。npm

基礎

實際上,咱們常見的一些 Web 框架都是經過使用 Http 模塊來建立了一個服務,在請求到來時經過一系列的處理後把結果返回給前臺,事實上 Koa 內部大體也是如此。數組

經過查看源碼不難發現 Koa 主要分爲四個部分:應用程序、上下文、請求對象和響應對象,當咱們引入 Koa 時實際上就是拿到了負責建立應用程序的這個類。緩存

咱們先來看一下一個簡單的 Hello World 應用:app

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

app.use(async ctx => {
  ctx.body = 'Hello World'
})

app.listen(3000, () => console.log('The app is running on localhost:3000'))

運行上面的代碼並訪問 http://localhost:3000/,一個簡單的應用就這樣建立好了。框架

實現

根據上面的使用方式咱們能夠很容易的想到下面的實現:koa

const http = require('http')

module.exports = class Application {
  use(fn) {
    this.middleware = fn
  }

  callback() {
    const handleRequest = (req, res) => {
      this.middleware(req, res)
    }

    return handleRequest
  }

  listen(...args) {
    const server = http.createServer(this.callback())

    return server.listen(...args)
  }
}

在上面的例子中,中間件獲得的參數仍是原生的請求和響應對象。按照 Koa 的實現,如今咱們須要建立一個貫穿整個請求的上下文對象,上下文中包括了原生的和封裝的請求、響應對象。async

// request.js
module.exports = {}

// response.js
module.exports = {}

// context.js
module.exports = {}

// application.js
const http = require('http')
const request = require('./request')
const response = require('./response')
const context = require('./context')

module.exports = class Application {
  constructor() {
    // 確保每一個實例都擁有本身的 request response context 三個對象
    this.request = Object.create(request)
    this.response = Object.create(response)
    this.context = Object.create(context)
  }

  createContext() {
    // ...
  }

  callback() {
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res)

      this.middleware(ctx)
    }

    return handleRequest
  }
}

在上面咱們建立了三個對象並放置到了應用的實例上面,最後將建立好的上下文對象傳遞給中間件。在建立上下文的函數中首先要處理的就是請求、響應等幾個對象之間的關係:函數

module.exports = class Application {
  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

    return context
  }
}

其中上下文上的 requestresponse 是咱們後面要進一步封裝的請求和響應對象,而 reqres 則是原生的請求和響應對象。

Context

如上,在每一次收到用戶請求時都會建立一個 Context 對象,這個對象封裝了此次用戶請求的信息,並提供了許多便捷的方法來獲取請求參數或者設置響應信息。

除了自行封裝的一些屬性和方法外,其中也有許多屬性和方法都是經過代理的方式獲取的請求和響應對象上的值。

const delegate = require('delegates')

const context = (module.exports = {
  onerror(err) {
    const msg = err.stack || err.toString()

    console.error(msg)
  },
})

delegate(context, 'response')
  // ...
  .access('body')

delegate(context, 'request')
  .method('get')
  // ...
  .access('method')

這裏咱們看到的 delegates 模塊是由大名鼎鼎的 TJ 所寫的,利用委託模式,它使得外層暴露的對象將請求委託給內部的其餘對象進行處理。

Delegator

接下來咱們來看看delegates 模塊中的核心邏輯。

function Delegator(proto, target) {
  if (!(this instanceof Delegator)) return new Delegator(proto, target)

  this.proto = proto
  this.target = target
}

Delegator.prototype.method = function(name) {
  const proto = this.proto
  const target = this.target

  // 調用時這裏的 this 就是上下文對象,target 則是 request 或 response
  // 因此,最終都會交給請求對象或響應對象上的方法去處理
  proto[name] = function() {
    return this[target][name].apply(this[target], arguments)
  }

  return this
}

Delegator.prototype.access = function(name) {
  return this.getter(name).setter(name)
}

Delegator.prototype.getter = function(name) {
  const proto = this.proto
  const target = this.target

  // __defineGetter__ 方法能夠爲一個已經存在的對象設置(新建或修改)訪問器屬性
  proto.__defineGetter__(name, function() {
    return this[target][name]
  })

  return this
}

Delegator.prototype.setter = function(name) {
  const proto = this.proto
  const target = this.target

  // __defineSetter__ 方法能夠將一個函數綁定在當前對象的指定屬性上,當那個屬性被賦值時,綁定的函數就會被調用
  proto.__defineSetter__(name, function(val) {
    return (this[target][name] = val)
  })

  return this
}

module.exports = Delegator

經過 method 方法在上下文上建立指定的函數,調用時會對應調用請求對象或響應對象上的方法進行處理,而對於一些普通屬性的讀寫則直接經過__defineGetter____defineSetter__ 方法來進行代理。

Request

Request 是一個請求級別的對象,封裝了 Node.js 原生的 HTTP Request 對象,提供了一系列輔助方法獲取 HTTP 請求經常使用參數。

module.exports = {
  get method() {
    // 直接獲取原生請求對象上對應的屬性
    return this.req.method
  },

  set method(val) {
    this.req.method = val
  },
}

和請求上下文對象相似,請求對象上除了會封裝一些常見的屬性和方法外,也會去直接讀取並返回一些原生請求對象上對應屬性的值。

Response

Response 是一個請求級別的對象,封裝了 Node.js 原生的 HTTP Response 對象,提供了一系列輔助方法設置 HTTP 響應。

module.exports = {
  get body() {
    return this._body
  },

  set body(val) {
    // 省略了詳細的處理邏輯
    this._body = val
  },
}

其中的處理方式和請求對象的處理相似。

中間件

和 Express 不一樣,Koa 的中間件選擇了洋蔥圈模型,全部的請求通過一箇中間件的時候都會執行兩次,這樣能夠很是方便的實現後置處理邏輯。

function compose(middlewares) {
  return function(ctx) {
    const dispatch = (i = 0) => {
      const middleware = middlewares[i]

      if (i === middlewares.length) {
        return Promise.resolve()
      }

      return Promise.resolve(middleware(ctx, () => dispatch(i + 1)))
    }

    return dispatch()
  }
}

module.exports = compose

Koa 的中間件處理被單獨的放在了 koa-compose 模塊中,上面是插件處理的主要邏輯,核心思想就是將調用下一個插件的函數經過回調的方式交給當前正在執行的中間件。

存在的一個問題是,開發者可能會屢次調用執行下箇中間件的函數(next),爲此咱們能夠添加一個標識:

function compose(middlewares) {
  return function(ctx) {
    let index = -1

    const dispatch = (i = 0) => {
      if (i <= index) {
        return Promise.reject(new Error('next() called multiple times'))
      }

      index = i

      const middleware = middlewares[i]

      if (i === middlewares.length) {
        return Promise.resolve()
      }

      return Promise.resolve(middleware(ctx, () => dispatch(i + 1)))
    }

    return dispatch()
  }
}

module.exports = compose

因爲在每個 dispatch 函數(也就是中間件中的 next 函數)中 i 的值是固定的,在調用一次後它的值就和 index 的值相等了,再次調用就會報錯。

Application

Application 是全局應用對象,在一個應用中,只會實例化一個,在它上面咱們創建了幾個對象之間的關係,同時還會負責組織上面提到的插件。

另外,以前咱們的 use 方法直接將指定的插件賦值給了 middleware,但是這樣只能有一個插件,所以咱們須要改變一下,維護一個數組。

const compose = require('../koa-compose')

module.exports = class Application {
  constructor() {
    // ...
    this.middleware = []
  }

  use(fn) {
    this.middleware.push(fn)
  }

  callback() {
    const fn = compose(this.middleware)
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res)

      fn(ctx)
    }

    return handleRequest
  }
}

目前爲止,咱們基本已經完成了本次請求的處理,但並無完成響應,咱們還須要在最後返回 ctx.body 上的數據。

module.exports = class Application {
  callback() {
    const fn = compose(this.middleware)
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res)

      this.handleRequest(ctx, fn)
    }

    return handleRequest
  }

  handleRequest(ctx, fnMiddleware) {
    const onerror = err => ctx.onerror(err)
    const handleResponse = () => respond(ctx)

    return fnMiddleware(ctx)
      .then(handleResponse)
      .catch(onerror)
  }
}

function respond(ctx) {
  ctx.res.end(ctx.body)
}

如今一個基礎的 Koa 就算實現了。

其它

這裏寫下的實現也只是提供一個思路,歡迎你們一塊兒交流學習。

輕拍【滑稽】。。。

相關文章
相關標籤/搜索