1. 前言
鉴于之前使用express 和koa 的经验,最近想尝试构建出一个koa 精简版,利用最少的代码实现koa和koa-router ,同时也梳理一下Node.js 网络框架开发的核心内容。
实现的源代码将会放在文末,配有详细的注释。
2. 核心设计
2.1 API调用
在mini-koa 的API设计中,参考koa和koa-router 的API 调用方式。
Node.js 的网络框架封装其实并不复杂,其核心点在于http/https 的createServer 方法上,这个方法是http请求 的入口。
首先,我们先回顾一下用Node.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...')
})
2.2 路由原理
既然我们知道Node.js 的请求入口在createServer 方法上,那么我们可以在这个方法中找出请求的地址,然后根据地址映射出监听函数(通过get/post 等方法添加的路由函数)即可。
其中,路由列表的格式设计如下:
{
'/': [fn1, fn2, ...],
'/user': [fn, ...],
...
}
{
method: 'get/post/use/all',
fn: '路由处理函数'
}
3. 难点分析
3.1 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 {
return
}
3.2 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 的函数列表中,没有则继续下一次循环。
详细代码如下:
let prefix = '/'
let useFnList = []
const filterUrl = url.split('/').filter(item => item !== '')
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)
3.3 ctx.body响应
通过ctx.body = '响应内容' 的方式可以响应http请求。它的实现原理是利用了ES6 的Object.defineProperty 函数,通过设置它的setter/getter 函数来达到数据追踪的目的。
详细代码如下:
Object.defineProperty(ctx, 'body', {
set(val) {
response.end(val)
},
get() {
throw new Error(`ctx.body can't read, only support assign value.`)
}
})
3.4 子路由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])
})
}
4. 用法
使用示例如下:
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...')
})
5. 总结
本次实现的精简版mini-koa ,虽然跟常用的koa框架 有很大区别,但是也实现了最基本的API调用 和原理。
造轮子是一件难能可贵的事,程序员在学习过程中不应该一直崇尚拿来主义,学习到一定程度后,在自己的个人项目中,可以秉持能造就造的态度,去尝试理解和挖掘源码背后的原理和思想。
当然,通常来说,自己造的轮子本身不具备多大的实用性,没有经历过社区大量的测试和实际应用场景的打磨,但是能加深自己的理解和提高自己的能力也是一件值得坚持的事。
附录:源代码
mini-koa-router.js :
class KoaRouter {
constructor(props) {
this.prefix = props.prefix || '/'
this.binding = {}
}
request(method, url, callback) {
if (typeof url === 'function') {
callback = url
url = '/'
}
if (this.prefix) {
url =
'/' +
[this.prefix, url]
.join('/')
.split('/')
.filter(item => item)
.join('/')
}
if (!this.binding[url]) {
this.binding[url] = []
}
this.binding[url].push({
method: method,
fn: callback
})
}
use(url, callback) {
this.request('use', url, callback)
}
all(url, callback) {
this.request('all', url, callback)
}
get(url, callback) {
this.request('get', url, callback)
}
post(url, callback) {
this.request('post', url, callback)
}
}
module.exports = KoaRouter
mini-koa.js :
const http = require('http')
const parseUrlParams = url => {
const query = {}
const index = url.indexOf('?')
if (index < 0) {
return query
}
url = url.substring(index + 1)
url.split('&').forEach(function(item) {
let obj = item.split('=')
query[obj[0]] = obj[1] || undefined
})
return query
}
class Koa {
constructor() {
this.binding = {}
this.httpApp = null
this.init()
}
init() {
this.httpApp = http.createServer(this._requestServer.bind(this))
}
async _requestServer(request, response) {
const ctx = {}
request.query = {}
request.params = {}
ctx.req = request
ctx.request = request
ctx.res = response
ctx.response = response
ctx.query = request.query
ctx.params = request.params
response.statusCode = 200
response.setHeader('Content-Type', 'text/plain;charset=utf-8')
response.setHeader('Access-Control-Allow-Origin', '*')
response.setHeader(
'Access-Control-Allow-Methods',
'PUT,POST,GET,DELETE,OPTIONS'
)
Object.defineProperty(ctx, 'body', {
set(val) {
response.end(val)
},
get() {
throw new Error(`ctx.body can't read, only support assign value.`)
}
})
const method = request.method
const rawUrl = request.url
const resUrl = rawUrl.match(/(\/[^?&=]*)/i)
let url = rawUrl
if (resUrl) {
url = resUrl[1]
}
request.query = parseUrlParams(rawUrl)
ctx.query = request.query
let prefix = '/'
let useFnList = []
const filterUrl = url.split('/').filter(item => item !== '')
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)
if (useFnList.length) {
for (let i = 0, length = useFnList.length; i < length; i++) {
let router = useFnList[i]
let isNext = false
const next = () => {
isNext = true
return Promise.resolve()
}
await router.fn(ctx, next)
if (isNext) {
continue
} else {
return
}
}
}
const routerList = []
if (this.binding[url] && this.binding[url].length) {
routerList.push(...this.binding[url])
}
let bindingUrlList = Object.keys(this.binding).map(item => {
return item.split('/').filter(i => i !== '')
})
bindingUrlList = bindingUrlList.filter(item => {
return item.length === filterUrl.length
})
filterUrl.forEach((key, index) => {
bindingUrlList = bindingUrlList.filter(item => {
if (item[index].startsWith(':')) {
let variableName = item[index].replace(':', '')
request.params[variableName] = key
return true
} else if (item[index] === key) {
return true
} else {
return false
}
})
})
bindingUrlList.forEach(item => {
let url = '/' + item.join('/')
if (this.binding[url] && this.binding[url].length) {
routerList.push(...this.binding[url])
}
routerList.push(...this.binding[url])
})
if (routerList.length) {
for (let i = 0, length = routerList.length; i < length; i++) {
let router = routerList[i]
if (router.method === method.toLowerCase() || router.method === 'all') {
let isNext = false
const next = () => {
isNext = true
return Promise.resolve()
}
await router.fn(ctx, next)
if (isNext) {
continue
} else {
return
}
}
}
response.statusCode = 404
ctx.body = `不支持的方法 - ${method}`
} else {
response.statusCode = 404
ctx.body = `${url}不存在`
}
}
request(method, url, callback) {
if (typeof url === 'function') {
callback = url
url = '/'
}
if (!this.binding[url]) {
this.binding[url] = []
}
this.binding[url].push({
method: method,
fn: callback
})
}
use(url, callback) {
this.request('use', url, callback)
}
all(url, callback) {
this.request('all', url, callback)
}
get(url, callback) {
this.request('get', url, callback)
}
post(url, callback) {
this.request('post', url, callback)
}
listen(...args) {
this.httpApp.listen(...args)
}
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])
})
}
}
module.exports = Koa
作者:mask.qi
|