write in front
In February-March of this year, I did a campus community project. It is divided into front and back ends, and was responsible for the construction of the back end at that time. The back-end uses the Koa framework, and the thought and use of the middle shocked me! (mainly to understand why await next()
you can skip to the next middleware) So, I went to understand the source code of Koa and want to write a blog!
What is middleware?
Let's first understand what is 中间件
?
// 注册接口
router.post('/register', userValidator, verifyUser, cryptPassword, verifySex, register)
复制代码
- Taking the registration interface as an example, the route goes through
userValidator
,verifyUser
,cryptPassword
,verifySex
middleware, and finally the control functionregister
userValidator
Determine whether the user password input is validverifyUser
Query whether the user has registered (need to query the database)cryptPassword
bcryptjs
Password encryption via pluginverifySex
Identify the gender of the inputregister
Store information in database and return information
Why use middleware?
Students should see
- 1. Middleware makes the thinking of the code clearer. What does each step do? The semantics of middleware can greatly increase the readability of the code .
- 2. Improve code reusability
- Although it doesn't seem to be very obvious here, let's give another example (in the following routes, the
auth
middleware is reused十二次
)
- Although it doesn't seem to be very obvious here, let's give another example (in the following routes, the
// 上传头像接口
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 middleware source code
Let's take middleware with a lot of reuse auth
as an example to see how to write a middleware. All middleware is essentially a async/await
function (this will be discussed in the Koa source code analysis later!)
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!!!)
}
复制代码
- The identity of the user is verified through the token. This code can indeed be reused. When a middleware completes its mission, it can
await next()
- The work is handed over to the next middleware
So why await next()? What is the specific process? Let us reveal the secrets together!
Analysis of Koa source code
Let's take a gif picture first
这是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()之后')
}
复制代码