乐趣区

不到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 支持,谢谢。

退出移动版