NODE.JS前端开发对express VS koa 中间件机制的理解
express 中间件的使用
中间件的注册
1. 参数只有一个
中间件就是一个函数(其实函数就是一个中间件),最后通过next执行下一个中间件。
app.use((req, res, next) => {
console.log('处理 /api 路由')
next()
})
复制代码
所有的请求都会执行这个没有路由的中间件。
2. 参数有两个
第一个是路由,第二个是中间件,只有命中了路由才会执行这个中间件。
// 如果访问的是/api/user,也会执行这个中间件,因为命中了父路由
app.use('/api', (req, res, next) => {
console.log('处理 /api 路由')
next()
})
// 也可以用这种方式来注册,如果是get、或者是post请求,并且命中了路由,就去执行里面的中间件
app.get('/api', (req, res, next) => {
console.log('get /api 路由')
next()
})
app.post('/api', (req, res, next) => {
console.log('post /api 路由')
next()
})
复制代码
app.use app.get app.post都可以注册中间件
中间件的使用
// 模拟登录验证
function loginCheck(req, res, next) {
setTimeout(() => {
// 如果模拟登陆失败,执行以下逻辑
if () {
console.log('模拟登陆失败')
res.json({
errno: -1,
msg: '登录失败'
})
} else {
// 如果模拟登陆成功,执行以下逻辑
console.log('模拟登陆成功')
next()
}
})
}
app.get('/api/get-cookie', loginCheck, (req, res, next) => {
console.log('get /api/get-cookie')
res.json({
errno: 0,
data: req.cookie
})
})
复制代码
异常处理的中间件
// catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404))
})
// error handler
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message
res.locals.error = req.app.get('env') === 'dev' ? err : {}
// render the error page
res.status(err.status || 500)
res.render('error')
})
复制代码
使用时需要注意两点:
- 参数是四个,第一个参数是err,否则会视为普通的中间件
- 异常处理中间件需要在请求之后
express中间件机制
首先需要明确的一点是: Express 为线型模型,而 Koa 则为洋葱型模型。
下面将使用 Express 实现一个简单的 demo 来进行中间件机制的讲解:
var express = require('express');
var app = express();
app.use(function (req, res, next) {
console.log('第一个中间件start');
setTimeout(() => {
next();
}, 1000)
console.log('第一个中间件end');
});
app.use(function (req, res, next) {
console.log('第二个中间件start');
setTimeout(() => {
next();
}, 1000)
console.log('第二个中间件end');
});
app.use('/foo', function (req, res, next) {
console.log('接口逻辑start');
next();
console.log('接口逻辑end');
});
app.listen(4000);
复制代码
此时的输出比较符合我们对 Express 线性的理解,其输出为:
第一个中间件start
第一个中间件end
第二个中间件start
第二个中间件end
接口逻辑start
接口逻辑end
复制代码
但是,如果我们取消掉中间件内部的异步处理直接调用 next():
app.use(function (req, res, next) {
console.log('第一个中间件start');
next()
console.log('第一个中间件end');
});
复制代码
输出结果为:
第一个中间件start
第二个中间件start
接口逻辑start
接口逻辑end
第二个中间件end
第一个中间件end
复制代码
这种结果不是和 Koa 的输出很相似吗?是的,但是它和剥洋葱模型还是不一样的,其实这种输出的结果是由于代码的同步运行导致的,并不是说 Express 不是线性的模型。
当我们的中间件内没有进行异步操作时,其实我们的代码最后是以下面这种方式运行的:
app.use(function middleware1(req, res, next) {
console.log('第一个中间件start')(
// next()
function (req, res, next) {
console.log('第二个中间件start')(
// next()
function (req, res, next) {
console.log('接口逻辑start')(
// next()
function handler(req, res, next) {
// do something
}
)()
console.log('接口逻辑end')
}
)()
console.log('第二个中间件end')
}
)()
console.log('第一个中间件end')
})
复制代码
我们可以模拟手写一个express中间件的机制,来看看是如何导致上面的执行顺序的。
const http = require('http')
const slice = Array.prototype.slice
class LikeExpress {
constructor() {
// 存放中间件
this.routes = {
all: [], // app.use(...)
get: [], // app.get(...)
post: [] // app.post(...)
}
}
register(path) {
const info = {}
// 如果第一个参数是路由
if (typeof path === 'string') {
info.path = path
// 从第二个参数开始,转化为数组,存入stack
info.stack = slice.call(arguments, 1)
} else {
// 如果第一个参数传的不是路径,那么就默认为是根目录
info.path = '/'
// 从第1个参数开始,转化为数组,存入stack
info.stack = slice.call(arguments, 0)
}
return info
}
use() {
const info = this.register.apply(this, arguments)
this.routes.all.push(info)
}
get() {
const info = this.register.apply(this, arguments)
this.routes.get.push(info)
}
post() {
const info = this.register.apply(this, arguments)
this.routes.post.push(info)
}
match(method, url) {
let stack = []
if (url === '/favicon.ico') {
return stack
}
// 获取routes
let curRoutes = []
// all 注册的方法都要
curRoutes = curRoutes.concat(this.routes.all)
curRoutes = curRoutes.concat(this.routes[method])
curRoutes.forEach(item => {
if (url.indexof(item.path) === 0) {
stack = stack.concat(item.stack)
}
})
return stack
}
// 核心的 next 机制
handle(req, res, stack) {
const next = () => {
// 拿到第一个中间件
const middleware = stack.shift()
if (middleware) {
middleware(req, res, next)
}
}
next()
}
callback() {
return (req, res) => {
// express 框架有个json方法
res.json = (data) => {
res.setHeader('Content-type', 'application/json')
res.end(
JSON.stringify(data)
)
}
// 通过url 和 method来命中那些中间件
const url = req.url
const method = req.method.toLowerCase()
const resultList = this.match(method, url)
this.handle(req, res, resultList)
}
}
listen(...args) {
const server = http.createServer(this.callback())
server.listen(...args)
}
}
// 工厂函数
module.exports = () => {
return new LikeExpress()
}
复制代码
- 注册中间件registry
- 根据路由匹配中间件match,把符合条件的中间件挑选出出来
- 把挑选出来的中间件执行handle
// 核心的 next 机制
handle(req, res, stack) {
const next = () => {
// 拿到第一个中间件
const middleware = stack.shift()
if (middleware) {
middleware(req, res, next)
}
}
next()
}
复制代码
koa 中间件
koa2 中间件是一个async函数(express的中间件是一个普通函数),它返回一个promise,所以可以跟在await后面,如图所示:
从这个图可以看出,从一个中间件开始,最后也是从第一个中间件结束,这样一层套一层,就形成了一个洋葱圈的模型。所以 koa 的错误中间件一般是放在第一个。
这个洋葱圈模式会导致和express的响应机制不同:
- Express: 我们直接操作的是 res 对象,直接 res.send() 之后就立即响应了,后面的中间件不会执行了。
- Koa2: 数据的响应是通过 ctx.body 进行设置,注意这里仅是设置并没有立即响应,后面的中间件还是可以执行的,即所有的中间件结束之后做了响应。这样有什么好处呢?就是所有的中间件都能对ctx 进行一些操作。
koa2 使用ctx (上下文对象)封装了 req 和 res,以及一些常用的功能。
下面手写一个koa2中间件机制:
const http = require('http')
// 组合中间件
function compose(middlewareList) {
return function(ctx) {
function dispatch(i) {
const fn = middlewareList[i]
try {
return Promise.resolve(
// fn 因为是个async函数本来返回一个promise,但是外面为什么还要包一层Promise.resolve呢,是因为为了防止用户传的中间件没有用async开头,那就不能用await next()
fn(ctx, dispatch.bind(null, i + 1))
)
} catch (err) {
return Promise.reject(err)
}
}
dispatch(0)
}
}
class LikeKoa2 {
constructor() {
this.middlewareList = []
}
use(fn) {
this.middlewareList.push(fn)
return this
}
// 把req, res封装到ctx里面
createContext(req, res) {
const ctx = {
req,
res
}
ctx.query = req.query
return ctx
}
callback() {
const fn = compose(this.middlewareList)
return (req, res) => {
const ctx = this.createContext(req, res)
fn(ctx)
}
}
listen(...args) {
const server = http.createServer(this.callback())
server.listen(...args)
}
}
复制代码
下面通过一个例子来对中间件执行这一过程进行分析:
// 中间件 fn1 和 fn2
async function fn1(ctx, next) {
console.log('first: start')
await next()
console.log('first: end')
}
async function fn2(ctx, next) {
console.log('second: start')
await next()
console.log('second: end')
}
复制代码
打印结果:
first: start
second: start
second: end
first: end
复制代码
根据上面的描述我们就理解了express的中间件是线型模型,而 Koa 则为洋葱型模型。
两者的不同
可以看到,Koa2
的中间件机制和express
没啥区别,都是回调函数的嵌套,遇到next
或者 await next
就中断本中间件的代码执行,跳转到对应的下一个中间件执行期内的代码,一直到最后一个中间件,然后逆序回退到倒数第二个中间件await next 或者next下部分的代码执行,完成后继续回退,一直回退到第一个中间件await next或者next下部分的代码执行完成,中间件全部执行结束。
仔细看一下koa
除了调用next的时候前面加了一个await好像和express没有任何区别,都是函数嵌套,都是洋葱模型。但是koa
是在哪里响应的用户请求呢?koa中好型并没有cxt.send这样的函数,只有cxt.body,但是调用cxt.body并不是直接结束请求返回响应,和express的res.send有着本质上的不同。
所以,最关键的不是这些中间的执行顺序,而是响应的时机,Express 使用 res.end() 是立即返回(尽管响应结束了,但是后面的代码仍然会执行),这样想要做出些响应前的操作变得比较麻烦;而 Koa 是在所有中间件中使用 ctx.body 设置响应数据,但是并不立即响应,而是在所有中间件执行结束后,再调用 res.end(ctx.body) 进行响应,这样就为响应前的操作预留了空间,所以是请求与响应都在最外层,中间件处理是一层层进行,所以被理解成洋葱模型。
作者:小p
来源:稀土掘金