Koa2框架原理及實(shí)現(xiàn)

Koa2是一個(gè)基于Node實(shí)現(xiàn)的Web框架黍瞧,特點(diǎn)是優(yōu)雅、簡(jiǎn)潔原杂、健壯印颤、體積小、表現(xiàn)力強(qiáng)穿肄。它所有的功能通過(guò)插件的形式來(lái)實(shí)現(xiàn)年局。

本文主要介紹如何自己實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Koa际看,通過(guò)這種方式來(lái)深入理解Koa原理,尤其是中間件部分的理解矢否。Koa的具體實(shí)現(xiàn)可以看的koa的源碼仲闽。

// koa 的簡(jiǎn)單使用
const Koa = require('koa')
const app = new Koa()

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

app.listen(3000)

通過(guò)上面的代碼,如果要實(shí)現(xiàn)koa僵朗,我們需要實(shí)現(xiàn)三個(gè)模塊赖欣,分別是http的封裝,ctx對(duì)象的構(gòu)建验庙,中間件機(jī)制的實(shí)現(xiàn)顶吮,當(dāng)然koa還實(shí)現(xiàn)了錯(cuò)誤捕獲和錯(cuò)誤處理。

封裝http模塊

通過(guò)閱讀Koa2的源碼可知Koa是通過(guò)封裝原生的node http模塊粪薛。

// server.js
const http = require('http')

const server = http.createServer((req, res) => {
  res.writeHead(200)
  res.end('hello world')
})

server.listen(3000, () => {
  console.log('server running on port 3000')
})

以上是使用Node.js創(chuàng)建一個(gè)HTTP服務(wù)的代碼片段悴了,關(guān)鍵是使用http模塊中的createServer()方法,接下來(lái)我們對(duì)上面這面這部分過(guò)程進(jìn)行一個(gè)封裝汗菜,首先創(chuàng)建application.js让禀,并創(chuàng)建一個(gè)Application類用于創(chuàng)建Koa實(shí)例。通過(guò)創(chuàng)建use()方法來(lái)注冊(cè)中間件和回調(diào)函數(shù)陨界。并通過(guò)listen()方法開(kāi)啟服務(wù)監(jiān)聽(tīng)實(shí)例巡揍,并傳入use()方法注冊(cè)的回調(diào)函數(shù),如下代碼所示:

// application.js
let http = require('http')

class Application {
  constructor () {
    this.callback = () => {}
  }
  listen(...args) {
    const server = http.createServer((req, res) => {
      this.callback(req, res)
    })
    server.listen(...args)
  }
  use(callback){
    this.callback = callback
  }
}

module.exports = Application

接下來(lái)創(chuàng)建一個(gè)server.js菌瘪,引入application.js進(jìn)行測(cè)試

// server.js
const MiniKoa = require('./application')
const app = new MiniKoa()

app.use((req, res) => {
  res.writeHead(200)
  res.end('hello world')
})
app.listen(3000, () => {
  console.log('server running on port 3000')
})

啟動(dòng)后腮敌,在瀏覽器中輸入localhost:3000就能看到顯示"hello world"。這樣就完成http server的簡(jiǎn)單封裝了俏扩。

構(gòu)造ctx對(duì)象

Koa 的 Context 把 Node 的 Request 對(duì)象和 Response 對(duì)象封裝到單個(gè)對(duì)象中糜工,并且暴露給中間件等回調(diào)函數(shù)。比如獲取 url录淡,封裝之前通過(guò)req.url的方式獲取捌木,封裝之后只需要ctx.url就可以獲取。因此我們需要達(dá)到以下效果:

app.use(async ctx => {
  ctx // 這是 Context
  ctx.request // 這是 koa Request
  ctx.response // 這是 koa Response
});

JavaScript 的 getter 和 setter

在此之前嫉戚,需要了解 setter 和 getter 屬性刨裆,通過(guò) setter 和 getter 屬性,我們可以自定義屬性的特性彬檀。

// test.js
let person = {
  _name: 'old name',
  get name () {
    return this._name
  },
  set name (val) {
    console.log('new name is: ' + val)
    this._name = val
  }
}

console.log(person.name)
person.name = 'new name'
console.log(person.name)

// 輸出:
// old name
// new name is: new name
// new name

上面的代碼在每次給name屬性賦值的時(shí)會(huì)打印new name is: new name帆啃,添加了console.log這個(gè)行為,當(dāng)然還可以做許多別的操作

構(gòu)造 context

因此窍帝,我們可以使用 getter 和 setter 來(lái)構(gòu)造 context努潘,如下所示:

const http = require('http')

// 獲取 request 的 url
let request = {
  get url() {
    return this.req.url
  }
}

let response = {
  get body() {
    return this._body
  },
  set body(val) {
    this._body = val
  }
}

let context = {
  get url() {
    return this.request.url
  },
  get body() {
    return this.response.body
  },
  set body(val) {
    this.response.body = val
  }
}

class Application {
  constructor() {
    // this.callback = () => {}
    // 把 context、request 和 response 掛載到 Application 里面
    this.context = context
    this.request = request
    this.response = response
  }

  use(callback) {
    this.callback = callback
  }

  // 改造 listen
  listen(...args) {
    // 可能是一個(gè) 異步函數(shù) 因此需要 async
    const server = http.createServer(async (req, res) => {
      let ctx = this.createCtx(req, res)
      // 此時(shí)就可以直接給callback一個(gè) ctx
      await this.callback(ctx)
      // this.callback(req, res)
      // 此時(shí)的 ctx.body 是可以直接獲取的
      /**
       * get body() {
       *  return this.response.body
       * }
       */
      ctx.res.end(ctx.body)
    })
    server.listen(...args)
  }

  // 把原生的 req 和 res 掛載到 ctx 上
  createCtx(req, res) {
    // 模擬 req 和 res
    let ctx = Object.create(this.context) // 生成 context 對(duì)象,里面掛載 body 和 url
    ctx.request = Object.create(this.request) // 把 request 掛載到 ctx 上
    ctx.response = Object.create(this.response) // 把 response 掛載到 ctx 上
    // 把原生的 req 和 res 都掛載到 request 和 response 以及 ctx 上
    ctx.req = ctx.request.req = req
    ctx.res = ctx.response.res = res
    return ctx
  }
}

這時(shí)疯坤,我們就可以通過(guò) ctx 來(lái)獲取 url 了

// server.js
const MiniKoa = require('./application')
const app = new MiniKoa()

// 此時(shí)可以使用 ctx
app.use(async (ctx) => {
  ctx.body = 'ctx url: ' + ctx.url
})
app.listen(3000, () => {
  console.log('server running on port 3000')
})

// 在瀏覽器輸入 localhost:3000/path
// 瀏覽器顯示 ctx url: /path

Koa中間件及洋蔥圈模型的理解與實(shí)現(xiàn)

koa洋蔥圈模型

koa的中間件機(jī)制是一個(gè)洋蔥圈模型报慕,通過(guò)use()注冊(cè)多個(gè)中間件放入數(shù)組中,然后從外層開(kāi)始往內(nèi)執(zhí)行贴膘,遇到next()后進(jìn)入下一個(gè)中間件卖子,當(dāng)所有中間件執(zhí)行完后,開(kāi)始返回刑峡,依次執(zhí)行中間件中未執(zhí)行的部分洋闽,如上圖所示。

在實(shí)現(xiàn)之前突梦,我們先來(lái)了解一下中間件的原理诫舅,根據(jù)中間件的原理可知,要層層遞進(jìn)執(zhí)行多個(gè)函數(shù)宫患,比如下面的例子

// test.js
function add (x, y) {
  return x + y
}

function double (z) {
  return z * 2
}

const res1 = add (1, 2)
const res2 = double (res1)
console.log(res2)           // 6

上面的例子中刊懈,我們把add()函數(shù)傳入double()中,把函數(shù)作為參數(shù)娃闲,這樣最終就會(huì)先執(zhí)行add()然后執(zhí)行double()虚汛,這時(shí)我們把這種模式編寫(xiě)成一個(gè)通用的compose()函數(shù)皇帮。

// test.js
function add(x, y) {
  return x + y
}

function double(z) {
  return z * 2
}

// 把需要執(zhí)行的函數(shù)都按順序放到一個(gè)數(shù)組里,類似于koa中間件的use()方法
const middleware = [add, double]
let len = middleware.length
// compose 把所有函數(shù)都?jí)撼梢粋€(gè)函數(shù)
function compose(middleware) {
  return (...args) => {
    // step1: 先把第一個(gè)函數(shù)拿出來(lái)執(zhí)行一下将谊,作為初始值
    let res = middleware[0](...args)
    // step2: 初始值執(zhí)行完成之后塞給第二個(gè)函數(shù)
    for (let i = 1; i < len; i++) {
      // 從 1 開(kāi)始遍歷尊浓,把所有的函數(shù)都執(zhí)行一下
      // 把執(zhí)行的結(jié)果傳給下一個(gè)函數(shù)
      res = middleware[i](res)
    }
    return res
  }
}
const fn = compose(middleware)
const res = fn(1, 2)
console.log(res) // 6

上面的compose()函數(shù)還有一個(gè)缺點(diǎn)栋齿,它是一個(gè)同步的方法,并沒(méi)有異步的等待,如果要使用異步应结,直接使用for循環(huán)是不行的,它不能等待異步執(zhí)行完畢鹅龄,此外 koa 還對(duì)外暴露了next()方法來(lái)實(shí)現(xiàn)異步等待扮休,它是一個(gè)Promise,當(dāng)執(zhí)行到它時(shí)就執(zhí)行下一個(gè)中間件蜗搔。

// test.js
async function fn1(next) {
  console.log('fn1')
  await next()
  console.log('end fn1')
}

async function fn2(next) {
  console.log('fn2')
  await delay()
  await next()
  console.log('end fn2')
}

async function fn3(next) {
  console.log('fn3')
}

function delay() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve()
    }, 2000)
  })
}

function compose(middleware) {
  // console.log(middleware)
  // [ [AsyncFunction: fn1], [AsyncFunction: fn2], [AsyncFunction: fn3] ]
  return () => {
    // 先執(zhí)行第一個(gè)函數(shù)
    return dispatch(0)

    function dispatch(i) {
      let fn = middleware[i]
      // 如何不存在直接返回 Promise
      if (!fn) {
        return Promise.resolve()
      }
      // step1: 返回一個(gè) Promise樟凄,因此單純變成一個(gè) Promise 且 立即執(zhí)行
      // step2: 往當(dāng)前中間件傳入一個(gè)next()方法兄渺,當(dāng)這個(gè)中間件有執(zhí)行 next 的時(shí)候才執(zhí)行下一個(gè)中間件
      return Promise.resolve(fn(function next() {
        // 執(zhí)行下一個(gè)中間件
        return dispatch(i + 1)
      }))
    }
  }
}

const middleware = [fn1, fn2, fn3]
const finalFn = compose(middleware)
finalFn()

// fn1
// fn2
// 等待兩秒
// fn3
// end fn2
// end fn1

上面已經(jīng)實(shí)現(xiàn)一個(gè)了一個(gè)簡(jiǎn)單的中間件示例挂谍,接下來(lái)再把它整合到 Application 類中

// Application.js
const http = require('http')

let request = {
  get url() {
    return this.req.url
  }
}

let response = {
  get body() {
    return this._body
  },
  set body(val) {
    this._body = val
  }
}

let context = {
  get url() {
    return this.request.url
  },
  get body() {
    return this.response.body
  },
  set body(val) {
    this.response.body = val
  }
}

class Application {
  constructor() {
    this.context = context
    this.request = request
    this.response = response
    this.middleware = []
  }
  use(callback) {
    // 創(chuàng)建一個(gè) middleware 數(shù)組,通過(guò) push 傳入多個(gè) callback
    // 然后通過(guò) compose 控制整個(gè) middleware 執(zhí)行的順序
    // 每個(gè) callback 回調(diào)函數(shù)給兩個(gè)參數(shù) 第一個(gè)是 context 第二個(gè)是 next
    this.middleware.push(callback)
    // this.callback = callback
  }
  // 直接把 compose 移植過(guò)來(lái)
  compose(middleware) {
    // 每個(gè)中間件需要一個(gè) context
    return function (context) {
      return dispatch(0)

      function dispatch(i) {
        let fn = middleware[i]
        if (!fn) {
          return Promise.resolve()
        }
        // 中間件第一個(gè)參數(shù)是一個(gè) context,第二個(gè)參數(shù)是 next()
        return Promise.resolve(fn(context, function next() {
          return dispatch(i + 1)
        }))
      }
    }
  }
  listen(...args) {
    const server = http.createServer(async (req, res) => {
      let ctx = this.createCtx(req, res)
      // await this.callback(ctx)
      // 這里不能直接執(zhí)行 callback 而是先獲取經(jīng)過(guò) compose 處理后的中間件集合
      const fn = this.compose(this.middleware)
      await fn(ctx)
      ctx.res.end(ctx.body)
    })
    server.listen(...args)
  }
  createCtx(req, res) {
    let ctx = Object.create(this.context)
    ctx.request = Object.create(this.request)
    ctx.response = Object.create(this.response)
    ctx.req = ctx.request.req = req
    ctx.res = ctx.response.res = res
    return ctx
  }
}

module.exports = Application

這時(shí)一個(gè)精簡(jiǎn)的 koa 就實(shí)現(xiàn)了,我們來(lái)測(cè)試它是否好用

// server.js
const MiniKoa = require('./application')
const app = new MiniKoa()

function delay() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve()
    }, 2000)
  })
}

app.use(async (ctx, next) => {
  ctx.body = '(fn1) '
  await next()
  ctx.body += '(end fn1) '
})
app.use(async (ctx, next) => {
  ctx.body += '(fn2) '
  await delay()
  await next()
  ctx.body += '(end fn2) '
})

app.use(async (ctx, next) => {
  ctx.body += '(fn3) '
})

app.listen(3000, () => {
  console.log('server running on port 3000')
})

// 瀏覽器輸出:(fn1) (fn2) (fn3) (end fn2) (end fn1) 

總結(jié)

到此為止,一個(gè)簡(jiǎn)單的 Koa 就實(shí)現(xiàn)了斟珊,但是這里還缺少了異常處理富纸,更詳細(xì)的實(shí)現(xiàn)方式請(qǐng)查看 Koa 源碼晓褪,無(wú)非也只是一些工具函數(shù)以及一些功能點(diǎn)的細(xì)化,其基本原理大概就是如此了勤庐。其中的難點(diǎn)是中間件原理,通過(guò)這個(gè)例子徹底理解中間件原理后米罚,以后再使用起這個(gè)框架來(lái)录择,就更加得心應(yīng)手了隘竭。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末遗锣,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子精偿,更是在濱河造成了極大的恐慌弧圆,老刑警劉巖笔咽,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異叶组,居然都是意外死亡拯田,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門甩十,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人侣监,你說(shuō)我怎么就攤上這事橄霉∏砸” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵钱慢,是天一觀的道長(zhǎng)束莫。 經(jīng)常有香客問(wèn)我御吞,道長(zhǎng),這世上最難降的妖魔是什么漓藕? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮挟裂,結(jié)果婚禮上享钞,老公的妹妹穿的比我還像新娘。我一直安慰自己诀蓉,他們只是感情好栗竖,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著渠啤,像睡著了一般狐肢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上沥曹,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天份名,我揣著相機(jī)與錄音,去河邊找鬼妓美。 笑死僵腺,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的壶栋。 我是一名探鬼主播辰如,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼贵试!你這毒婦竟也來(lái)了琉兜?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤毙玻,失蹤者是張志新(化名)和其女友劉穎豌蟋,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體淆珊,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡夺饲,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了施符。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片往声。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖戳吝,靈堂內(nèi)的尸體忽然破棺而出浩销,到底是詐尸還是另有隱情,我是刑警寧澤听哭,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布慢洋,位于F島的核電站塘雳,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏普筹。R本人自食惡果不足惜败明,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望太防。 院中可真熱鬧妻顶,春花似錦、人聲如沸蜒车。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)酿愧。三九已至沥潭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間嬉挡,已是汗流浹背钝鸽。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留庞钢,地道東北人寞埠。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像焊夸,于是被迫代替她去往敵國(guó)和親仁连。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353