Koa洋葱圈模型源码浅析
写在前面
在今年的2-3月份做了一个校园社区的项目。分为前后端,当时是负责了后端的搭建。
后端使用了Koa框架,中间间的思想和使用让我震撼!(主要是弄懂为啥await next()
可以跳到下一个中间件)
所以,去了解了一下Koa的源码,想要写一篇博客!
什么是中间件?
我们先来了解一下什么是中间件
?
// 注册接口
router.post('/register', userValidator, verifyUser, cryptPassword, verifySex, register)
- 以注册接口为例,路由一次经过了
userValidator
,verifyUser
,cryptPassword
,verifySex
中间件,然后最后是控制函数register
userValidator
判断用户密码输入是否有效verifyUser
查询用户是否已经注册(需要查询数据库)cryptPassword
通过bcryptjs
插件进行密码加密verifySex
对输入的性别进行判别register
存储信息到数据库返回信息
为什么要使用中间件?
同学们应该看出来了
- 1.中间件让代码的思路变得更加清晰,每一步干什么,通过中间件的语义化能够很好的增加代码可读性
- 2.提高代码的复用性
- 虽然在这里好像看的不是很明显,我们再举一个例子(在下面的这些路由里面,
auth
中间件复用了十二次
)
- 虽然在这里好像看的不是很明显,我们再举一个例子(在下面的这些路由里面,
// 上传头像接口
router.post('/upload', auth, upload)
// 封号接口
router.post('/blockadeornot', auth, verifyAdmin, blockade)
// 切换管理员接口
router.post('/admin', auth, verifyAdmin, changeAdmin)
// 用户密码一键重置接口
router.post('/reset', auth, verifyAdmin, cryptPassword, reset)
// 修改密码接口
router.patch('/password', auth, cryptPassword, changePassword)
// 修改昵称接口
router.patch('/name', auth, changeName)
// 修改昵称接口
router.patch('/city', auth, changeCity)
// 修改性别接口
router.patch('/sex', auth, verifySex, changeSex)
// 修改的总接口
router.patch('/change', auth, verifySex, change)
// token更新接口
router.get('/updatetoken', updatetoken)
// 查询所有用户信息的接口
router.get('/info', auth, findall)
// 根据id查询用户信息的接口
router.get('/searchbyid', auth, findone)
// 查询active或者not_active用户,正常用户的接口
router.get('/active', auth, verifyAdmin, findAllactive)
auth中间件源码
我们以复用次数很多的auth
中间件为例,来看看如何写一个中间件
所有的中间件本质上都是一个async/await
的函数(这个在后面的Koa源码解析里面会讲到!)
const jwt = require('jsonwebtoken') // jwt插件
const { JWT_SECRET } = require('../config/config.default') // 加密使用到的私钥
const auth = async (ctx, next) => {
const { authorization } = ctx.request.header // 解构出authorization
const token = authorization.replace('Bearer ', '') // 得到token
try {
const user = jwt.verify(token, JWT_SECRET) // 使用jwt解析接收到的token
ctx.state.user = user // 把解析内容挂载到ctx.state上,方便后面的中间件使用
} catch (err) {
switch (err.name) { // 错误抛出处理
case 'TokenExpiredError':
console.error('token已过期', err);
return ctx.app.emit('error', tokenExpiredError, ctx) // 把错误抛出到app最后进行统一的处理
case 'JsonWebTokenError':
console.error('无效token', err);
return ctx.app.emit('error', invalidToken, ctx) // 把错误抛出到app最后进行统一的处理
}
}
await next() // 中间件的灵魂(next!!!)
}
- 通过token来验证用户的身份,这个代码确实是可以复用的,当一个中间件完成了他的使命后,就可以
await next()
- 讲工作交给下一个中间件了
那么为什么是await next()呢?具体过程是怎样的呢?
下面带大家一起揭秘!
Koa源码浅析
我们先来康一张gif图片
这是Koa源码中的一张解释middleware的图片!过程很清晰了!
虽然我在实际编程的时候只使用到了1-5
的步骤…
本文将向你解释为什么是这样?
- 这里是
app.use
- 实际我们是
use
的可能是router,router里面有很多的接口 - 可以简单理解为,最终是
app.use
写的所有的接口
我们的探索流程图
listen函数
从最上层开始看起,listen主要是完成了对server的监听
server怎么来的?用callback()
创建的
listen (...args) {
debug('listen')
const server = http.createServer(this.callback())
return server.listen(...args)
}
callback函数
- return的是
this.handleRequest(ctx,fn)
- 1.我们下面要去看
createContext
如何生成ctx
- 2.再看
handleRequest
如何运行的 - 3.关键点,
middleware
是什么?compose
对它进行了怎样的封装?
- 1.我们下面要去看
callback () {
const fn = compose(this.middleware)
if (!this.listenerCount('error')) this.on('error', this.onerror)
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
return handleRequest
}
createContext函数
简单来说就是把req
和res
都挂载到context
上,然后返回这个上下文
- 这就是为什么我们的数据都是从
ctx
上解析出来的!!!(ctx.request.body
/ctx.request.params
) - 我们可以看到
context.state
是空的,这是为什么我们前面把信息挂载到这个上面
createContext (req, res) {
const context = Object.create(this.context)
const request = context.request = Object.create(this.request) // request插件 这里不细说了...
const response = context.response = Object.create(this.response) // response插件
context.app = request.app = response.app = this
context.req = request.req = response.req = req
context.res = request.res = response.res = res
request.ctx = response.ctx = context
request.response = response
response.request = request
context.originalUrl = request.originalUrl = req.url
context.state = {}
return context
}
handleRequest函数
简单理解为将ctx
传递给fnMiddleware
函数执行(这里就解释了为啥我们把信息挂载到ctx.state.use
后,后面的中间件可以使用ctx.state.use
拿到数据)
handleRequest (ctx, fnMiddleware) {
const res = ctx.res
res.statusCode = 404
const onerror = err => ctx.onerror(err)
const handleResponse = () => respond(ctx)
onFinished(res, onerror)
return fnMiddleware(ctx).then(handleResponse).catch(onerror)
}
middleware是什么?— use函数
我们可以看到use函数关键的一步就是将fn
放入middleware
数组中。
所以app.use
并不是马上执行,而是将函数先放入数组中
this.middleware = []
use (fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
debug('use %s', fn._name || fn.name || '-')
this.middleware.push(fn)
return this
}
compose函数((await next()
为什么能够形成洋葱圈模型?))
const compose = require('koa-compose')
- 我们来看看这个插件的源码
- 1.判断middleware是否是一个数组(存放中间件函数)
- 2.判断数组中每个元素是否是一个函数
- 3.简单来说是一个递归调用中间件
- 1.
index=-1
这里很巧妙,用了一个闭包的技巧,执行函数后,index=i
,所以index>=i
时reject
(说明多次调用了!) - 2.不断取
fn=middle[i]
fn不为空就执行 - 3.递归调用下一层
- 1.这里比较有趣的一点:
dispatch.bind(null, i + 1))
一定要用bind(null)吗? - 是的,这里不是单纯的递归调用,而是传入一个函数,所以必须用到bind
- 这里也不仔细阐述bind和call,apply的区别了!
- 2.将
下一个中间件
作为next
参数传递下去了(这就是为什么await next()
能够形成洋葱圈模型了)
- 1.这里比较有趣的一点:
- 1.
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
总结
compose插件将下一个中间件
作为next
参数传递下去了(这就是为什么await next()能够形成洋葱圈模型了)
可能有的同学还是不理解,用代码来演示一下(重复一下gif图中的思路)
我们的实际使用
async function a (context, next) {
console.log('1.中间件1')
await next()
console.log('6.中间件1next()之后')
}
async function b (context, next) {
console.log('2.中间件2')
await next()
console.log('5.中间件2next()之后')
}
async function c (context, next) {
console.log('3.中间件3')
await next()
console.log('4.中间件3next()之后')
}
var composeMiddles = compose([a, b, c])
composeMiddles()
1.中间件1
2.中间件2
3.中间件3
4.中间件3next()之后
5.中间件2next()之后
6.中间件1next()之后
compose转换后的伪代码
async function a (context, next) {
console.log('1.中间件1')
async function b (context, next) {
console.log('2.中间件2')
async function c (context, next) {
console.log('3.中间件3')
await next()
console.log('4.中间件3next()之后')
}
console.log('5.中间件2next()之后')
}
console.log('6.中间件1next()之后')
}