Koa源碼解析,帶你實現一個迷你版的Koa

前言

本文是我在閱讀 Koa 源碼后,並實現迷你版 Koa 的過程。如果你使用過 Koa 但不知道內部的原理,我想這篇文章應該能夠幫助到你,實現一個迷你版的 Koa 不會很難。

本文會循序漸進的解析內部原理,包括:

  1. 基礎版本的 koa
  2. context 的實現
  3. 中間件原理及實現

文件結構

  • application.js: 入口文件,裡面包括我們常用的 use 方法、listen 方法以及對 ctx.body 做輸出處理
  • context.js: 主要是做屬性和方法的代理,讓用戶能夠更簡便的訪問到request和response的屬性和方法
  • request.js: 對原生的 req 屬性做處理,擴展更多可用的屬性和方法,比如:query 屬性、get 方法
  • response.js: 對原生的 res 屬性做處理,擴展更多可用的屬性和方法,比如:status 屬性、set 方法

基礎版本

用法:

const Coa = require('./coa/application')
const app = new Coa()

// 應用中間件
app.use((ctx) => {
  ctx.body = '<h1>Hello</h1>'
})

app.listen(3000, '127.0.0.1')

application.js:

const http = require('http')

module.exports = class Coa {
  use(fn) {
    this.fn = fn
  }
  // listen 只是語法糖  本身還是使用 http.createServer
  listen(...args) {
    const server = http.createServer(this.callback())
    server.listen(...args)
  }
  callback() {
    const handleRequest = (req, res) => {
      // 創建上下文
      const ctx = this.createContext(req, res)
      // 調用中間件
      this.fn(ctx)
      // 輸出內容
      res.end(ctx.body)
    }
    return handleRequest
  }
  createContext(req, res) {
    let ctx = {}
    ctx.req = req
    ctx.res = res
    return ctx
  }
}

基礎版本的實現很簡單,調用 use 將函數存儲起來,在啟動服務器時再執行這個函數,並輸出 ctx.body 的內容。

但是這樣是沒有靈魂的。接下來,實現 context 和中間件原理,Koa 才算完整。

Context

ctx 為我們擴展了很多好用的屬性和方法,比如 ctx.query、ctx.set()。但它們並不是 context 封裝的,而是在訪問 ctx 上的屬性時,它內部通過屬性劫持將 request 和 response 內封裝的屬性返回。就像你訪問 ctx.query,實際上訪問的是 ctx.request.query。

說到劫持你可能會想到 Object.defineProperty,在 Kao 內部使用的是 ES6 提供的對象的 setter 和 getter,效果也是一樣的。所以要實現 ctx,我們首先要實現 request 和 response。

在此之前,需要修改下 createContext 方法:

// 這三個都是對象
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Coa {
  constructor() {
    this.context = context
    this.request = request
    this.response = response
  }
  createContext(req, res) {
    const ctx = Object.create(this.context)
    // 將擴展的 request、response 掛載到 ctx 上
    // 使用 Object.create 創建以傳入參數為原型的對象,避免添加屬性時因為衝突影響到原對象
    const request = ctx.request = Object.create(this.request)
    const response = ctx.response = Object.create(this.response)
    
    ctx.app = request.app = response.app = this;
    // 掛載原生屬性
    ctx.req = request.req = response.req = req
    ctx.res = request.res = response.res = res
    
    request.ctx = response.ctx = ctx;
    request.response = response;
    response.request = request;
    
    return ctx
  }
}

上面一堆花里胡哨的賦值,是為了能通過多種途徑獲取屬性。比如獲取 query 屬性,可以有 ctx.query、ctx.request.query、ctx.app.query 等等的方式。

如果你覺得看起來有點冗餘,也可以主要理解這幾行,因為我們實現源碼時也就用到下面這些:

const request = ctx.request = Object.create(this.request)
const response = ctx.response = Object.create(this.response)

ctx.req = request.req = response.req = req
ctx.res = request.res = response.res = res

request

request.js:

const url = require('url')

module.exports = {
 /* 查看這兩步操作
  * const request = ctx.request = Object.create(this.request)
  * ctx.req = request.req = response.req = req 
  * 
  * 此時的 this 是指向 ctx,所以這裏的 this.req 訪問的是原生屬性 req
  * 同樣,也可以通過 this.request.req 來訪問
  */
  // 請求的 query 參數
  get query() {
    return url.parse(this.req.url).query
  },
  // 請求的路徑
  get path() {
    return url.parse(this.req.url).pathname
  },
  // 請求的方法
  get method() {
    return this.req.method.toLowerCase()
  }
}

response

response.js:

module.exports = {
  // 這裏的 this.res 也和上面同理 
  // 返回的狀態碼
  get status() {
    return this.res.statusCode
  },
  set status(val) {
    return this.res.statusCode = val
  },
  // 返回的輸出內容
  get body() {
    return this._body
  },
  set body(val) {
    return this._body = val
  },
  // 設置頭部
  set(filed, val) {
    if (typeof filed === 'string') {
      this.res.setHeader(filed, val)
    }
    if (toString.call(filed) === '[object Object]') {
      for (const key in filed) {
        this.set(key, filed[key])
      }
    }
  }
}

屬性代理

通過上面的實現,我們可以使用 ctx.request.query 來訪問到擴展的屬性。但是在實際應用中,更常用的是 ctx.query。不過 query 是在 request 的屬性,通過 ctx.query 是無法訪問的。

這時只需稍微做個代理,在訪問 ctx.query 時,將 ctx.request.query 返回就可以實現上面的效果。

context.js:

module.exports = {
    get query() {
        return this.request.query
    }
}

實際的代碼中會有很多擴展的屬性,總不可能一個一個去寫吧。為了優雅的代理屬性,Koa 使用 delegates 包實現。這裏我就直接簡單封裝下代理函數,代理函數主要用到__defineGetter__ 和 __defineSetter__ 兩個方法。

在對象上都會帶有 __defineGetter__ 和 __defineSetter__,它們可以將一個函數綁定在當前對象的指定屬性上,當屬性被獲取或賦值時,綁定的函數就會被調用。就像這樣:

let obj = {}
let obj1 = {
    name: 'JoJo'
}
obj.__defineGetter__('name', function(){
    return obj1.name
})

此時訪問 obj.name,獲取到的是 obj1.name 的值。

了解這個兩個方法的用處后,接下來開始修改 context.js:

const proto = module.exports = {
}

// getter代理
function delegateGetter(prop, name){
  proto.__defineGetter__(name, function(){
    return this[prop][name]
  })
}
// setter代理
function delegateSetter(prop, name){
  proto.__defineSetter__(name, function(val){
    return this[prop][name] = val
  })
}
// 方法代理
function delegateMethod(prop, name){
  proto[name] = function() {
    return this[prop][name].apply(this[prop], arguments)
  }
}

delegateGetter('request', 'query')
delegateGetter('request', 'path')
delegateGetter('request', 'method')

delegateGetter('response', 'status')
delegateSetter('response', 'status')
delegateGetter('response', 'body')
delegateSetter('response', 'body')
delegateMethod('response', 'set')

中間件原理

中間件思想是 Koa 最精髓的地方,為擴展功能提供很大的幫助。這也是它雖然小,卻很強大的原因。還有一個優點,中間件使功能模塊的職責更加分明,一個功能就是一个中間件,多个中間件組合起來成為一個完整的應用。

下面是著名的“洋蔥模型”。這幅圖很形象的表達了中間件思想的作用,它就像一個流水線一樣,上游加工后的東西傳遞給下游,下游可以繼續接着加工,最終輸出加工結果。

原理分析

在調用 use 註冊中間件的時候,內部會將每个中間件存儲到數組中,執行中間件時,為其提供 next 參數。調用 next 即執行下一个中間件,以此類推。當數組中的中間件執行完畢后,再原路返回。就像這樣:

app.use((ctx, next) => {
  console.log('1 start')
  next()
  console.log('1 end')
})

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

app.use((ctx, next) => {
  console.log('3 start')
  next()
  console.log('3 end')
})

輸出結果如下:

1 start
2 start
3 start
3 end
2 end
1 end

有點數據結構知識的同學,很快就想到這是一個“棧”結構,執行的順序符合“先入后出”。

下面我將內部中間件實現原理進行簡化,模擬中間件執行:

function next1() {
  console.log('1 start')
  next2()
  console.log('1 end')
}
function next2() {
  console.log('2 start')
  next3()
  console.log('2 end')
}
function next3() {
  console.log('3 start')
  console.log('3 end')
}
next1()

執行過程:

  1. 調用 next1,將其入棧執行,輸出 1 start
  2. 遇到 next2 函數,將其入棧執行,輸出 2 start
  3. 遇到 next3 函數,將其入棧執行,輸出 3 start
  4. 輸出 3 end,函數執行完畢,next3 彈出棧
  5. 輸出 2 end,函數執行完畢,next2 彈出棧
  6. 輸出 1 end,函數執行完畢,next1 彈出棧
  7. 棧空,全部執行完畢

相信通過這個簡單的例子,都大概明白中間件的執行過程了吧。

原理實現

中間件原理實現的關鍵點主要是 ctx 和 next 的傳遞。

function compose(middleware) {
  return function(ctx) {
    return dispatch(0)
    function dispatch(i){
      // 取出中間件
      let fn = middleware[i]
      if (!fn) {
        return
      }
      // dispatch.bind(null, i + 1) 為應用中間件接受到的 next
      // next 即下一個應用中間件
      fn(ctx, dispatch.bind(null, i + 1))
    }
  }
}

可以看到,實現過程本質是函數的遞歸調用。在內部實現時,其實 next 沒有做什麼神奇的操作,它就是下一个中間件調用的函數,作為參數傳入供使用者調用。

下面我們來單獨測試 compose,你可以將它粘貼到控制台上運行:

function next1(ctx, next) {
  console.log('1 start')
  next()
  console.log('1 end')
}
function next2(ctx, next) {
  console.log('2 start')
  next()
  console.log('2 end')
}
function next3(ctx, next) {
  console.log('3 start')
  next()
  console.log('3 end')
}

let ctx = {}
let fn = compose([next1, next2, next3])
fn(ctx)

最後,因為 Koa 中間件是可以使用 async/await 異步執行的,所以還需要修改下 compose 返回 Promise:

function compose(middleware) {
  return function(ctx) {
    return dispatch(0)
    function dispatch(i){
      // 取出中間件
      let fn = middleware[i]
      if (!fn) {
        return Promise.resolve()
      }
      // dispatch.bind(null, i + 1) 為應用中間件接受到的 next
      // next 即下一個應用中間件
      try {
        return Promise.resolve( fn(ctx, dispatch.bind(null, i + 1)) )
      } catch (error) {
        return Promise.reject(error)
      }
    }
  }
}

應用

實現完成中間件的邏輯后,將它應用到迷你版Koa中,原來的代碼邏輯要做一些修改(部分代碼忽略)

application.js:

module.exports = class Coa {
  constructor() {
    // 存儲中間件的數組 
    this.middleware = []
  }

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    // 將中間件加入數組
    this.middleware.push(fn)
    return this
  }
  
  listen(...args) {
    const server = http.createServer(this.callback())
    server.listen(...args)
  }

  callback() {
    const handleRequest = (req, res) => {
      // 創建上下文
      const ctx = this.createContext(req, res)
      // fn 為第一個應用中間件
      const fn = this.compose(this.middleware)
      // 在所有中間件執行完畢后 respond 函數用於處理 ctx.body 輸出
      return fn(ctx).then(() => respond(ctx)).catch(console.error)
    }
    return handleRequest
  }
  
  compose(middleware) {
    return function(ctx) {
      return dispatch(0)
      function dispatch(i){
        let fn = middleware[i]
        if (!fn) {
          return Promise.resolve()
        }
        // dispatch.bind(null, i + 1) 為應用中間件接受到的 next
        try {
          return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)))
        } catch (error) {
          return Promise.reject(error)
        }
      }
    }
  }
}

function respond(ctx) {
  let res = ctx.res
  let body = ctx.body
  if (typeof body === 'string') {
    return res.end(body)
  }
  if (typeof body === 'object') {
    return res.end(JSON.stringify(body))
  }
}

完整實現

application.js:

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

module.exports = class Coa {
  constructor() {
    this.middleware = []
    this.context = context
    this.request = request
    this.response = response
  }

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    this.middleware.push(fn)
    return this
  }

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

  callback() {
    const handleRequest = (req, res) => {
      // 創建上下文
      const ctx = this.createContext(req, res)
      // fn 為第一個應用中間件
      const fn = this.compose(this.middleware)
      return fn(ctx).then(() => respond(ctx)).catch(console.error)
    }
    return handleRequest
  }

  // 創建上下文
  createContext(req, res) {
    const ctx = Object.create(this.context)
    // 處理過的屬性
    const request = ctx.request = Object.create(this.request)
    const response = ctx.response = Object.create(this.response)
    // 原生屬性
    ctx.app = request.app = response.app = this;
    ctx.req = request.req = response.req = req
    ctx.res = request.res = response.res = res

    request.ctx = response.ctx = ctx;
    request.response = response;
    response.request = request;

    return ctx
  }

  // 中間件處理邏輯實現
  compose(middleware) {
    return function(ctx) {
      return dispatch(0)
      function dispatch(i){
        let fn = middleware[i]
        if (!fn) {
          return Promise.resolve()
        }
        // dispatch.bind(null, i + 1) 為應用中間件接受到的 next
        // next 即下一個應用中間件
        try {
          return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)))
        } catch (error) {
          return Promise.reject(error)
        }
      }
    }
  }
}

// 處理 body 不同類型輸出
function respond(ctx) {
  let res = ctx.res
  let body = ctx.body
  if (typeof body === 'string') {
    return res.end(body)
  }
  if (typeof body === 'object') {
    return res.end(JSON.stringify(body))
  }
}

寫在最後

本文的簡單實現了 Koa 主要的功能。有興趣最好還是自己去看源碼,實現自己的迷你版 Koa。其實 Koa 的源碼不算多,總共4個文件,全部代碼包括註釋也就 1800 行左右。而且邏輯不會很難,很推薦閱讀,尤其適合源碼入門級別的同學觀看。

最後附上完整實現的代碼:github

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※回頭車貨運收費標準