在今年的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
存储信息到数据库返回信息同学们应该看出来了
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
中间件为例,来看看如何写一个中间件
所有的中间件本质上都是一个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!!!)
}
await next()
那么为什么是await next()呢?具体过程是怎样的呢?
下面带大家一起揭秘!
这是Koa源码中的一张解释middleware的图片!过程很清晰了!
虽然我在实际编程的时候只使用到了1-5
的步骤…
本文将向你解释为什么是这样?
app.use
use
的可能是router,router里面有很多的接口app.use
写的所有的接口从最上层开始看起,listen主要是完成了对server的监听
server怎么来的?用callback()
创建的
listen (...args) {
debug('listen')
const server = http.createServer(this.callback())
return server.listen(...args)
}
this.handleRequest(ctx,fn)
createContext
如何生成ctx
handleRequest
如何运行的middleware
是什么?compose
对它进行了怎样的封装? 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
}
简单来说就是把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
}
简单理解为将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)
}
我们可以看到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
}
await next()
为什么能够形成洋葱圈模型?))const compose = require('koa-compose')
index=-1
这里很巧妙,用了一个闭包的技巧,执行函数后,index=i
,所以index>=i
时reject
(说明多次调用了!)fn=middle[i]
fn不为空就执行dispatch.bind(null, i + 1))
一定要用bind(null)吗?下一个中间件
作为next
参数传递下去了(这就是为什么await next()
能够形成洋葱圈模型了)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()之后
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()之后')
}