初识洋葱模型,分析中间件执行过程,浅析koa中间件源码
# 前言
作为洋葱模型的第一篇文章,这里仅介绍了一些入门级知识,比如
- 了解洋葱模型执行顺序
- 分析部分 koa 中间件的源码来加深对中间件的认识
为第二篇文章:分析洋葱模型实现原理,在自己项目中接入洋葱模型 打下基础
# 什么是洋葱模型
洋葱模型 一图胜前言
图中的一层一层的“方法”称为 中间件
,比如 Cache Middleware ,Routers Middleware 就是一个个中间件
他们像流水线一样,按指定顺序执行中间件的方法
下面的案例都以 koa
作为演示的基础
# 搭建一个 koa 环境
由于只是简单的一个环境,所以就不用脚手架了,直接自己需要什么就安装什么
- 新建文件夹 koa-middleware(按自己喜好命名就好)
- 进入新建的文件夹,打开终端执行
npm init -y
- 再次运行
npm install koa koa-router nodemon
- 新建
src/app.js
文件 - 打开
package.json
文件,在 “script” 中添加一句启动命令"dev": "nodemon src/app.js"
运行 npm run dev 看到上图的效果,就算是 OK 了
然后在 app.js 中写入 koa 开启服务的基础代码
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
router.get('/', async function(ctx) {
ctx.body = 'hello world'
})
app.use(router.routes())
app.listen('3000', function() {
console.log(`koa 服务已启动: http://localhost:3000`)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
可以看到 nodemon 已经起作用了,我们无须重启终端也可以立刻看到代码修改后的运行效果~
由于 koa 开启的是一个网络服务,我们每次都得打开http://localhost:3000
才能触发相关的事件,太麻烦了
所以接下来在安装一个 axios
用于自动触发任务,方便我们探究中间件运行过程
- 安装 axios
npm install axios
- 修改 app.js 代码如下:
const Koa = require('koa')
const axios = require('axios')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
router.get('/', async function(ctx) {
ctx.body = 'hello world'
})
app.use(router.routes())
app.listen('3000', function() {
console.log(`koa 服务已启动: http://localhost:3000`)
axios('http://localhost:3000/')
.then(res => {
console.log('服务端返回的内容:', res.data)
})
.catch(res => {
// catch 方法在这里是很有必要的,
// 因为 node 程序如果有报错可能整个程序都会挂掉
// 及时catch 住错误,不要影响主进程的运行
console.log('服务端报错~', res.message)
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
就这样一个简单的中间件探索运行环境都搭建好了
# 先了解中间件的写法
下面的写法仅针对 koa
,当然我也会介绍一下 express 的写法,不过都是大同小异的
- 相同的地方:
koa 和 express 都是通过 app.use()
方法传入回调函数来挂载中间件
都有 next 方法用于执行下一个中间件
- 不同的地方
在 use
方法中 koa 回调函数参数有 2 个 ctx
和 next
,而 express 有 3 个 req
,res
,next
koa 的 ctx
可以理解为 req
和 res
的结合体了
- koa 写法
app.use(async function(ctx, next) {
// ...
})
2
3
- express 写法
app.use(function(req, res, next) {
// ...
})
2
3
很多中间件插件的写法,其实效果都是一样的,用一个闭合函数的目的是为了可以为中间件传入自己需要的参数
// 定义 log 中间件函数
function logMiddleare() {
return async function(ctx, next) {
// ...
}
}
// 使用的时候
app.use(logMiddleare())
2
3
4
5
6
7
8
9
# 中间件执行顺序
以 app.use 调用顺序为准:越早调用执行越早,包尾越晚
怎么理解?
const Koa = require('koa')
const axios = require('axios')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
router.get('/', async function(ctx) {
ctx.body = 'hello world'
})
app.use(async (ctx, next) => {
console.log('1. 第一个执行')
await next()
console.log('1. 包尾执行')
})
app.use(async (ctx, next) => {
console.log('2. 第2个执行')
await next()
console.log('2. 2执行结束')
})
app.use(router.routes())
app.listen('3000', function() {
console.log(`koa 服务已启动: http://localhost:3000`)
axios('http://localhost:3000/')
.then(res => {
console.log('服务端返回的内容:', res.data)
})
.catch(res => {
console.log('服务端报错~', res.message)
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
终端输出内容如下:
可以看到 1 方法最早执行
,包尾结束
。结合开头的洋葱图,能理解何为“洋葱模型”了把
如果留意到 router.get
方法,其实他也是一个中间件,也遵循上面说的规则,他是由 app.use(router.routes())
位于第三位注册进去的,虽然没打印内容,不过可以试下他应该是第三个打印内容的中间件
# 中间件一定会执行吗?
不一定,这得看中间件 上家
给不给执行,如果上家一家独大包揽了,不调用 next
那“洋葱”就闭环结束了
如何理解?上代码(就是把 await next()
注释掉):
// ...
app.use(async (ctx, next) => {
console.log('1. 第一个执行')
// await next()
console.log('1. 包尾执行')
})
// ...
2
3
4
5
6
7
看到返回值:
接口直接 404;方法 2 也没被执行了。为什么?
因为接口的路由注册app.use(router.routes())
位于第三位才注册,方法 2 也位于第二位。只要有一个 app.use 的方法不调用 next 后面的方法都不能执行
同理,如果把 方法 1 中的 next 放开了,把方法 2 的注释掉,那么得到的结果就是:
# 中间件能相互传递数据吗?
在 koa 框架中,所有东西都挂载在 ctx/app
上,如果是公共的方法,参数,那就挂载在 app 上,如果只是和这次请求相关的内容,挂载在 ctx 上即可
当然也可以通过 return 来返回数据,不过前提是上一个中间件有接收你的值
比如下面的代码
- 方法 1 中,挂载了一个
ctx.name
;同时想获取ctx.desc
拿到的是 undefined - 在方法 2 中才挂载
ctx.desc
;并且接收next
执行的返回值;方法 2 没有返回值 - 在最后的方法中,成功拿到了
${ctx.name} ${ctx.desc}
,同时 return 了一个{age:18}
回去
const Koa = require('koa')
const axios = require('axios')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
router.get('/', async function(ctx, next) {
console.log(`${ctx.name} ${ctx.desc}`)
ctx.body = 'hello world'
await next()
return { age: 18 }
})
app.use(async (ctx, next) => {
console.log('1. 第一个执行')
ctx.name = 'Jioho'
console.log('方法1中获取desc:', ctx.desc)
let res = await next()
console.log('1. 包尾执行', res)
})
app.use(async (ctx, next) => {
console.log('2. 第2个执行')
ctx.desc = '前端小菜鸡'
let res = await next()
console.log('2. 2执行结束', res)
})
app.use(router.routes())
app.listen('3000', function() {
console.log(`koa 服务已启动: http://localhost:3000`)
axios('http://localhost:3000/')
.then(res => {
console.log('服务端返回的内容:', res.data)
})
.catch(res => {
console.log('服务端报错~', res.message)
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
执行结果如下:
这也说明了,中间件之间传递参数,最好还是通过 ctx 挂载,因为如果单纯的靠 return 来返回内容,万一中间某一个中间件没返回,那参数就丢失了
# koa 节选中间件源码分析
前面也有说到,koa 中间件写法有 2 种,上面一直都是简单的 console 代码观察中间件运行顺序,下面来几个实际的中间件看看中间件到底能做些什么~
下面的均为 koajs (opens new window) 官方的中间库,我挑了几个一个简单的库来分析源码(实在是找不到简单,又有代表性的库了)
# koa-response-time
koa-response-time (opens new window) 统计接口响应时间的中间件
用法和简单的描述:
这是一个统计接口响应时间的库。最下面还有一句没截图到就是
Best to .use() at the top before any other middleware, to wrap all subsequent middleware. 最好在任何其他中间件之前的顶部 .use() ,以包装所有后续中间件。
这样才能更好的统计接口整个响应的时间
可以看到这个库源码非常简短 https://github.com/koajs/response-time/blob/master/index.js (opens new window)
/**
* Expose `responseTime()`.
*/
module.exports = responseTime
/**
* Add X-Response-Time header field.
* @param {Dictionary} options options dictionary. { hrtime }
*
* hrtime: boolean.
* `true` to use time in nanoseconds.
* `false` to use time in milliseconds.
* Default is `false` to keep back compatible.
* @return {Function}
* @api public
*/
function responseTime(options) {
let hrtime = options && options.hrtime
return function responseTime(ctx, next) {
let start = ctx[Symbol.for('request-received.startAt')]
? ctx[Symbol.for('request-received.startAt')]
: process.hrtime()
return next().then(() => {
let delta = process.hrtime(start)
// Format to high resolution time with nano time
delta = delta[0] * 1000 + delta[1] / 1000000
if (!hrtime) {
// truncate to milliseconds.
delta = Math.round(delta)
}
ctx.set('X-Response-Time', delta + 'ms')
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
可以看到 responseTime
函数里面接收了 options
参数,就是为了接收 hrtime
,用于判断时候要输入小数时间
闭包方法和我们写的一样
唯一的区别就是他并没有用 async
关键字,因为他在 next()
方法后面使用了 .then()
完全使用 Promise
的方式,就不需要 async 和 await 了。
剩下的代码就是
- 先记录请求开始时间
- 然后调用 next 方法,执行后面所有的中间件
- 执行完成后在记录一次时间,并且设置到到返回值的 header 的 'X-Response-Time' 中
# 小结
- 中间件执行顺序取决于 app.use 调用时间
- 越早调用,执行越早,收尾越晚
- 传参得靠 ctx
- 必须调用 next 方法后面的方法才会执行,否则洋葱模型就结束了
摸清楚中间件挂载和执行顺序后,就可以继续下一篇:分析洋葱模型实现原理,在自己项目中接入洋葱模型