鑑於以前使用express
和koa
的經驗,這兩天想嘗試構建出一個koa
精簡版,利用最少的代碼實現koa和koa-router
,同時也梳理一下Node.js
網絡框架開發的核心內容。javascript
實現後的核心代碼不超過300
行,源代碼配有詳細的註釋。java
在mini-koa
的API設計中,參考koa和koa-router
的API
調用方式。git
Node.js
的網絡框架封裝其實並不複雜,其核心點在於http/https
的createServer
方法上,這個方法是http請求
的入口。程序員
首先,咱們先回顧一下用Node.js
來啓動一個簡單服務。github
// 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
等方法添加的路由函數)便可。express
其中,路由列表的格式設計以下:cookie
// binding的格式
{
'/': [fn1, fn2, ...],
'/user': [fn, ...],
...
}
// fn/fn1/fn2的格式
{
method: 'get/post/use/all',
fn: '路由處理函數'
}
複製代碼
咱們知道在koa
中是能夠添加多個url監聽函數
的,其中決定是否傳遞到下一個監聽函數的關鍵在因而否調用了next()
函數。若是調用了next()
函數則先把路由權轉移到下一個監聽函數中,處理完畢再返回當前路由函數。網絡
在mini-koa
中,我把next()
方法設計成了一個返回Promise fullfilled
的函數(這裏簡單設計,不考慮next()
傳參的狀況),用戶若是調用了該函數,那麼就能夠根據它的值來決定是否轉移路由函數處理權。session
判斷是否轉移路由函數處理權的代碼以下:app
let isNext = false
const next = () => {
isNext = true
return Promise.resolve()
}
await router.fn(ctx, next)
if (isNext) {
continue
} else {
// 沒有調用next,直接停止請求處理函數
return
}
複製代碼
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 = '響應內容'
的方式能夠響應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
的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
支持,謝謝。