不到300行代碼構建精簡的koa和koa-router(mini-koa)

mini-koa

前言

鑑於之前使用expresskoa的經驗,這兩天想嘗試構建出一個koa精簡版,利用最少的代碼實現koa和koa-router,同時也梳理一下Node.js網絡框架開發的核心內容。

實現後的核心代碼不超過300行,源代碼配有詳細的註釋。

核心設計

API調用

mini-koa的API設計中,參考koa和koa-routerAPI調用方式。

Node.js的網絡框架封裝其實並不複雜,其核心點在於http/httpscreateServer方法上,這個方法是http請求的入口。

首先,我們先回顧一下用Node.js來啓動一個簡單服務。

// https://github.com/qzcmask/mini-koa/blob/master/examples/simple.js
const http = require('http')
const app = http.createServer((request, response) => {
  response.end('hello Node.js')
})
app.listen(3333, () => {
  console.log('App is listening at port 3333...')
})

路由原理

既然我們知道Node.js的請求入口在createServer方法上,那麼我們可以在這個方法中找出請求的地址,然後根據地址映射出監聽函數(通過get/post等方法添加的路由函數)即可。

其中,路由列表的格式設計如下:

// binding的格式
{
'/': [fn1, fn2, ...],
'/user': [fn, ...],
...
}
// fn/fn1/fn2的格式
{
  method: 'get/post/use/all',
  fn: '路由處理函數'
}

難點分析

next()方法設計

我們知道在koa中是可以添加多個url監聽函數的,其中決定是否傳遞到下一個監聽函數的關鍵在於是否調用了next()函數。如果調用了next()函數則先把路由權轉移到下一個監聽函數中,處理完畢再返回當前路由函數。

mini-koa中,我把next()方法設計成了一個返回Promise fullfilled的函數(這裏簡單設計,不考慮next()傳參的情況),用戶如果調用了該函數,那麼就可以根據它的值來決定是否轉移路由函數處理權。

判斷是否轉移路由函數處理權的代碼如下:

let isNext = false
const next = () => {
  isNext = true
  return Promise.resolve()
}
await router.fn(ctx, next)
if (isNext) {
  continue
} else {
  // 沒有調用next,直接中止請求處理函數
  return
}

use()方法設計

mini-koa提供use方法,可供擴展日誌記錄/session/cookie處理等功能。

use方法執行的原理是根據請求地址在執行特定路由函數之前先執行mini-koa調用use監聽的函數

所以這裏的關鍵點在於怎麼找出use監聽的函數列表,假設現有監聽情況如下:

app.use('/', fn1)
app.use('/user', fn2)

如果訪問的url/user/add,那麼fn1和fn2都必須要依次執行。

我採取的做法是先根據/字符來分割請求url,然後循環拼接,查看路由綁定列表(binding)中有沒有要use的函數,如果發現有,添加進要use的函數列表中,沒有則繼續下一次循環。

詳細代碼如下:

// 默認use函數前綴
let prefix = '/'
// 要預先調用的use函數列表
let useFnList = []

// 分割url,使用use函數
// 比如item爲/user/a/b映射成[('user', 'a', 'b')]
const filterUrl = url.split('/').filter(item => item !== '')
// 該reduce的作用是找出本請求要use的函數列表
filterUrl.reduce((cal, item) => {
  prefix = cal
  if (this.binding[prefix] && this.binding[prefix].length) {
    const filters = this.binding[prefix].filter(router => {
      return router.method === 'use'
    })
    useFnList.push(...filters)
  }
  return (
    '/' +
    [cal, item]
      .join('/')
      .split('/')
      .filter(item => item !== '')
      .join('/')
  )
}, prefix)

ctx.body響應

通過ctx.body = '響應內容'的方式可以響應http請求。它的實現原理是利用了ES6Object.defineProperty函數,通過設置它的setter/getter函數來達到數據追蹤的目的。

詳細代碼如下:

// 追蹤ctx.body賦值
Object.defineProperty(ctx, 'body', {
  set(val) {
    // set()裏面的this是ctx
    response.end(val)
  },
  get() {
    throw new Error(`ctx.body can't read, only support assign value.`)
  }
})

子路由mini-koa-router設計

子路由mini-koa-router設計這個比較簡單,每個子路由維護一個路由監聽列表,然後通過調用mini-koaaddRoutes函數添加到主路由列表上。

mini-koaaddRoutes實現如下:

addRoutes(router) {
  if (!this.binding[router.prefix]) {
    this.binding[router.prefix] = []
  }
  // 路由拷貝
  Object.keys(router.binding).forEach(url => {
    if (!this.binding[url]) {
      this.binding[url] = []
    }
    this.binding[url].push(...router.binding[url])
  })
}

用法

使用示例如下,源代碼可以在github上找到:

// examples/server.js
// const { Koa, KoaRouter } = require('mini-koa')
const { Koa, KoaRouter } = require('../index')
const app = new Koa()
// 路由用法
const userRouter = new KoaRouter({
  prefix: '/user'
})

// 中間件函數
app.use(async (ctx, next) => {
  console.log(`請求url, 請求method: `, ctx.req.url, ctx.req.method)
  await next()
})

// 方法示例
app.get('/get', async ctx => {
  ctx.body = 'hello ,app get'
})

app.post('/post', async ctx => {
  ctx.body = 'hello ,app post'
})

app.all('/all', async ctx => {
  ctx.body = 'hello ,/all 支持所有方法'
})

// 子路由使用示例
userRouter.post('/login', async ctx => {
  ctx.body = 'user login success'
})

userRouter.get('/logout', async ctx => {
  ctx.body = 'user logout success'
})

userRouter.get('/:id', async ctx => {
  ctx.body = '用戶id: ' + ctx.params.id
})

// 添加路由
app.addRoutes(userRouter)

// 監聽端口
app.listen(3000, () => {
  console.log('> App is listening at port 3000...')
})

總結

挺久沒有造輪子了,這次突發奇想造了個精簡版的koa,雖然跟常用的koa框架有很大差別,但是也實現了最基本的API調用和原理。

造輪子是一件難能可貴的事,程序員在學習過程中不應該崇尚拿來主義,學習到一定程度後,要秉持能造就造的態度,去嘗試理解和挖掘輪子背後的原理和思想。

當然,通常來說,自己造的輪子本身不具備多大的實用性,沒有經歷過社區大量的測試和實際應用場景的打磨,但是能加深自己的理解和提高自己的能力也是一件值得堅持的事。

人生是一段不斷攀登的高峯,只有堅持向前,才能看到新奇的東西。

最後附上項目的Github地址,歡迎Star或Fork支持,謝謝。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章