前言鉴于之前使用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.jsconst 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 = falseconst 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支持,谢谢。