鉴于以前使用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
支持,谢谢。