不到300行程式碼構建精簡的koa和koa-router(mini-koa)
前言
鑑於之前使用 express
和 koa
的經驗,這兩天想嘗試構建出一個 koa
精簡版,利用最少的程式碼實現 koa和koa-router
,同時也梳理一下 Node.js
網路框架開發的核心內容。
實現後的核心程式碼不超過 300
行, 原始碼 配有詳細的註釋。
核心設計
API呼叫
在 mini-koa
的API設計中,參考 koa和koa-router
的 API
呼叫方式。
Node.js
的網路框架封裝其實並不複雜,其核心點在於 http/https
的 createServer
方法上,這個方法是 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請求。它的實現原理是利用了 ES6
的 Object.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-koa
的 addRoutes
函式新增到主路由列表上。
mini-koa
的 addRoutes
實現如下:
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
支援,謝謝。