[源碼實現]koa核心代碼

導語:實現簡單的koa核心代碼,便於理解koa原理。順便學習koa代碼的那些騷操做。node

簡單分析koa

建立一個 http 服務,只綁一箇中間件。建立 index.jsnpm

/** index.js */
const Koa = require('koa')

const app = new Koa()

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

app.listen(8080, function() {
  console.log('server start 8080')
})
複製代碼

從這段代碼中能夠看出json

  • Koa 是一個構造函數
  • Koa 的原形上至少有 ues、listen 兩個方法
  • listen 的參數與 http 一致
  • ues 接受一個方法,在用戶訪問的時候調用,並傳入上下文 ctx (這裏先不考慮異步與next,一步步實現)

咱們再來看看 koa 源碼的目錄結構數組

|-- koa
    |-- .npminstall.done
    |-- History.md
    |-- LICENSE
    |-- Readme.md
    |-- package.json
    |-- lib
        |-- application.js
        |-- context.js
        |-- request.js
        |-- response.js
複製代碼

其中 application.js 是入口文件,打開後能夠看到是一個 class。context.js、request.js、response.js 都是一個對象,用來組成上下文 ctxbash

啓動http服務

先編寫 application.js 部分代碼。建立 myKoa 文件夾,咱們的koa代碼將會放在這個文件內。建立 myKoa/application.jsmarkdown

經過分析已經知道 application.js 導出一個 class,原形上至少有 listen 和 use 兩個方法。listen 建立服務並監聽端口號 http服務,use 用來收集中間件。實現代碼以下app

/** myKoa/application.js */
const http = require('http')

module.exports = class Koa {
  constructor() {
    // 存儲中間件
    this.middlewares = []
  }
  // 收集中間件
  use(fn) {
    this.middlewares.push(fn)
  }
  // 處理當前請求方法
  handleRequest(req, res) { // node 傳入的 req、res
    res.end('手寫koa核心代碼') // 爲了訪問頁面有顯示,暫時加上
  }
  // 建立服務並監聽端口號
  listen(...arges) {
    const app = http.createServer(this.handleRequest.bind(this))
    app.listen(...arges)
  }
}
複製代碼

代碼很簡單。use 把中間件存入 middlewareslisten 啓動服務,每次請求到來調用 handleRequestkoa

建立 ctx (一)

context.js、request.js、response.js 都是一個對象,用來組成上下文 ctx。代碼以下異步

/** myKoa/context.js */
const proto = {}

module.exports = proto
複製代碼
/** myKoa/request.js */
module.exports = {}
複製代碼
/** myKoa/response.js */
module.exports = {}
複製代碼

三者的關係是: request.js、response.js 兩個文件的導出會綁定到 context.js 文件導出的對象上,分別做爲 ctx.request 和 ctx.response 使用。async

koa 爲了每次 new Koa() 使用的 ctx 都是相互獨立的,對 context.js、request.js、response.js 導出的對象作了處理。源碼中使用的是 Object.create() 方法建立一個新對象,使用現有的對象來提供新建立的對象的__proto__。一會在代碼中演示用法

建立ctx以前,再看一下ctx上的幾個屬性,和他們直接的關係。必定要分清哪些是node自帶的,哪些是koa的屬性

app.use(async ctx => {
  ctx; // 這是 Context
  ctx.req; // 這是 node Request
  ctx.res; // 這是 node Response
  ctx.request; // 這是 koa Request
  ctx.response; // 這是 koa Response
  ctx.request.req; // 這是 node Request
  ctx.response.res;  // 這是 node Response
});
複製代碼

爲何這個設計,在文章後面將會解答

開始建立 ctx。部分代碼以下

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

module.exports = class Koa {
  constructor() {
    // 存儲中間件
    this.middlewares = []
    // 綁定 context、request、response
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
  }
  // 建立上下文 ctx
  createContext(req, res) {
    const ctx = this.context
    // koa 的 Request、Response
    ctx.request = this.request
    ctx.response = this.response
    // node 的 Request、Response
    ctx.request.req = ctx.req = req
    ctx.response.res = ctx.res = res
    return ctx
  }
  // 收集中間件
  use(fn) {/* ... */}
  // 處理當前請求方法
  handleRequest(req, res) {
    // 建立 上下文,準備傳給中間件
    const ctx = this.createContext(req, res)
    
    res.end('手寫koa核心代碼') // 爲了訪問頁面有顯示,暫時加上
  }
  // 建立服務並監聽端口號
  listen(...arges) {/* ... */}
}
複製代碼

此時就建立了一個基礎的上下文 ctx。

建立 ctx (二)

獲取上下文上的屬性

實現一個 ctx.request.url。開始前先考慮幾個問題

  • request.js 導出的是一個對象,不能接受參數
  • ctx.req.urlctx.request.urlctx.request.req.url 三者直接應該始終相等

koa 是這樣作的

  • 第一步 ctx.request.req = ctx.req = req
  • 訪問 ctx.request.url 轉成訪問 ctx.request.req.url

沒錯,就是 get 語法糖

/** myKoa/request.js */
module.exports = {
  get url() {
    return this.req.url
  }
}
複製代碼

此時的 this 指向的是 Object.create(request) 生成的對象,並非 request.js 導出的對象

設置上下文上的屬性

接下來咱們實現 ctx.response.body = 'Hello World'。當設置 ctx.response.body 時其實是把屬性存到了 ctx.response._body 上,當獲取 ctx.response.body 時只須要在 ctx.response._body 上取出就能夠了 。代碼以下

/** myKoa/response.js */
module.exports = {
  set body(v) {
    this._body = v
  },
  get body() {
    return this._body
  }
}
複製代碼

此時的 this 指向的是 Object.create(response) 生成的對象,並非 response.js 導出的對象

設置 ctx 別名

koa 給咱們設置了不少別名,好比 ctx.body 就是 ctx.response.body

有了以前的經驗,獲取/設置屬性就比較容易。直接上代碼

/** myKoa/context.js */
const proto = {
  get url() {
    return this.request.req.url
  },
  get body() {
    return this.response.body
  },
  set body(v) {
    this.response.body = v
  },
}
module.exports = proto
複製代碼

有沒有感受很簡單。固然koa上下文部分沒有到此結束。看 koa/lib/context.js 代碼,在最下面能夠看到這樣的代碼(從只挑選了access方法)

delegate(proto, 'response')
  .access('status')
  .access('body')

delegate(proto, 'request')
  .access('path')
  .access('url')
複製代碼

koa 對 get/set 作了封裝。用的是 Delegator 第三方包。核心是用的 __defineGetter____defineSetter__ 兩個方法。這裏爲了簡單易懂,只是簡單封裝兩個方法代替 Delegator 實現簡單的功能。

// 獲取屬性。調用方法如 defineGetter('response', 'body')
function defineGetter(property, key) {
  proto.__defineGetter__(key, function() {
    return this[property][key]
  })
}
// 設置屬性。調用方法如 defineSetter('response', 'body')
function defineSetter(property, key) {
  proto.__defineSetter__(key, function(v) {
    this[property][key] = v
  })
}
複製代碼

myKoa/context.js 文件最終修改成

/** myKoa/context.js */
const proto = {}

function defineGetter(property, key) {
  proto.__defineGetter__(key, function() {
    return this[property][key]
  })
}

function defineSetter(property, key) {
  proto.__defineSetter__(key, function(v) {
    this[property][key] = v
  })
}

// 請求
defineGetter('request', 'url')
// 響應
defineGetter('response', 'body')
defineSetter('response', 'body')

module.exports = proto
複製代碼

讓 ctx.body 顯示在頁面上

這步很是簡單,只須要判斷 ctx.body 是否有值,並觸發 req.end() 就完成了。相關代碼以下

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

module.exports = class Koa {
  constructor() {/* ... */}
  // 建立上下文 ctx
  createContext(req, res) {/* ... */}
  // 收集中間件
  use(fn) {/* ... */}
  // 處理當前請求方法
  handleRequest(req, res) {
    const ctx = this.createContext(req, res)
    
    res.statusCode = 404 //默認 status
    if (ctx.body) {
      res.statusCode = 200
      res.end(ctx.body)
    } else {
      res.end('Not Found')
    }
  }
  // 建立服務並監聽端口號
  listen(...arges) {/* ... */}
}
複製代碼

同理能夠處理 header 等屬性

實現同步中間件

中間件接受兩個參數,一個上下文ctx,一個 next方法。上下文ctx已經寫好了,主要是怎麼實現next方法。

寫一個dispatch方法,他的主要功能是:好比傳入下標0,找出數組中下標爲0的方法middleware,調用middleware並傳入一個方法next,而且當next調用時, 查找下標加1的方法。實現以下

const middlewares = [f1, f2, f3]
function dispatch(index) {
  if (index === middlewares.length) return
  const middleware = middlewares[index]
  const next = () => dispatch(index+1)
  middleware(next)
}
dispatch(0)
複製代碼

此時就實現了next方法。

在koa中,是不容許一個請求中一箇中間件調用兩次next。好比

app.use((ctx, next) => {
  ctx.body = 'Hello World'
  next()
  next() // 報錯 next() called multiple times
})
複製代碼

koa 用了一個小技巧。記錄每次調用的中間件下標,當發現調用的中間件下標沒有加1(中間件下標 <= 上一次中間件下標)時,就報錯。修改代碼以下

const middlewares = [f1, f2, f3] // 好比中間件中有三個方法
let i = -1 
function dispatch(index) {
  if (index <= i) throw new Error('next() called multiple times')
  if (index === middlewares.length) return
  i = index
  const middleware = middlewares[index]
  const next = () => dispatch(index+1)
  middleware(next)
}
dispatch(0)
複製代碼

中間件代碼基本完成,傳入 ctx 、加入 myKoa/application.js 文件。

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

module.exports = class Koa {
  constructor() {/* ... */}
  // 建立上下文 ctx
  createContext(req, res) {/* ... */}
  // 收集中間件
  use(fn) {/* ... */}
  // 處理中間件
  compose(ctx) {
    const middlewares = this.middlewares
    let i = -1 
    function dispatch(index) {
      if (index <= i) throw new Error('next() called multiple times')
      if (index === middlewares.length) return
      i = index
      const middleware = middlewares[index]
      const next = () => dispatch(index+1)
      middleware(ctx, next)
    }
    dispatch(0)
  }
  // 處理當前請求方法
  handleRequest(req, res) {
    const ctx = this.createContext(req, res)
    this.compose(ctx)

    res.statusCode = 404 //默認 status
    if (ctx.body) {
      res.statusCode = 200
      res.end(ctx.body)
    } else {
      res.end('Not Found')
    }
  }
  // 建立服務並監聽端口號
  listen(...arges) {/* ... */}
}
複製代碼

到此就實現了同步中間件

實現異步中間件

koa 中使用異步中間件的寫法以下

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

app.use(async (ctx, next) => {
  await new Promise((res, rej) => setTimeout(res,1000))
  console.log('ctx.body:', ctx.body)
})
複製代碼

上述代碼接受請求後大約1s 控制檯打印 ctx.body: Hello World。能夠看出,koa是基於 async/await 的。指望每次 next() 後返回的是一個 Promise

同時考慮到中間件變爲異步執行,那麼handleRequest應該等待中間件執行完再執行相關代碼。那麼compose也應該返回Promise

能夠經過async快速完成 普通函數 =》Promise 的轉化。

修改compose代碼和handleRequest代碼

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

module.exports = class Koa {
  constructor() {/* ... */}
  // 建立上下文 ctx
  createContext(req, res) {/* ... */}
  // 收集中間件
  use(fn) {/* ... */}
  // 處理中間件
  compose(ctx) {
    const middlewares = this.middlewares
    let i = -1 
    async function dispatch(index) {
      if (index <= i) throw new Error('next() called multiple times')
      if (index === middlewares.length) return
      i = index
      const middleware = middlewares[index]
      const next = () => dispatch(index+1)
      return middleware(ctx, next)
    }
    return dispatch(0)
  }
  // 處理當前請求方法
  handleRequest(req, res) {
    const ctx = this.createContext(req, res)
    const p = this.compose(ctx)
    p.then(() => {
      res.statusCode = 404 //默認 status
      if (ctx.body) {
        res.statusCode = 200
        res.end(ctx.body)
      } else {
        res.end('Not Found')
      }
    }).catch((err) => {
      console.log(err)
    })
  }
  // 建立服務並監聽端口號
  listen(...arges) {/* ... */}
}
複製代碼

代碼展現

application.js

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

module.exports = class Koa {
  constructor() {
    // 存儲中間件
    this.middlewares = []
    // 綁定 context、request、response
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
  }
  // 建立上下文 ctx
  createContext(req, res) {
    const ctx = this.context
    // koa 的 Request、Response
    ctx.request = this.request
    ctx.response = this.response
    // node 的 Request、Response
    ctx.request.req = ctx.req = req
    ctx.response.res = ctx.res = res
    return ctx
  }
  // 收集中間件
  use(fn) {
    this.middlewares.push(fn)
  }
  // 處理中間件
  compose(ctx) {
    const middlewares = this.middlewares
    let i = -1 
    async function dispatch(index) {
      if (index <= i) throw new Error('multi called next()')
      if (index === middlewares.length) return
      i = index
      const middleware = middlewares[index]
      const next = () => dispatch(index+1)
      return middleware(ctx, next)
    }
    return dispatch(0)
  }
  // 處理當前請求方法
  handleRequest(req, res) {
    const ctx = this.createContext(req, res)
    const p = this.compose(ctx)
    p.then(() => {
      res.statusCode = 404 //默認 status
      if (ctx.body) {
        res.statusCode = 200
        res.end(ctx.body)
      } else {
        res.end('Not Found')
      }
    }).catch((err) => {
      console.log(err)
    })
  }
  // 建立服務並監聽端口號
  listen(...arges) {
    const app = http.createServer(this.handleRequest.bind(this))
    app.listen(...arges)
  }
}
複製代碼

context.js

/** myKoa/context.js */
const proto = {}

function defineGetter(property, key) {
  proto.__defineGetter__(key, function() {
    return this[property][key]
  })
}

function defineSetter(property, key) {
  proto.__defineSetter__(key, function(v) {
    this[property][key] = v
  })
}

// 請求
defineGetter('request', 'url')
// 響應
defineGetter('response', 'body')
defineSetter('response', 'body')

module.exports = proto
複製代碼

request.js

/** myKoa/request.js */
module.exports = {
  get url() {
    return this.req.url
  }
}
複製代碼

response.js

/** myKoa/response.js */
module.exports = {
  set body(v) {
    this._body = v
  },
  get body() {
    return this._body
  }
}
複製代碼
相關文章
相關標籤/搜索