Middleware(中间件)

Middleware

在接触 Node.js 后,发现一些第三方框架使用了 Middleware 的机制,如 Express 中的app.use。简单来说就是在进入具体的业务处理之前,可以使用第三方提供的中间件进行其他细节的处理。

简介

在使用 Monk 这个第三方 MongoDB 库时,看到其 Middlewares 这篇文章用几个很简单但很常见的问题来说明了为什么使用 Middleware。

下面,我就用这篇文章提供的例子,解释一下 Middleware。

理解 Middleware

问题:记录日志(Logging)

记录日志是每个程序都应该具备的,它可以在出现问题时帮我我们进行定位以及监视程序的一些运行状态。

解决方案 1:手动添加

最简单的方法是在调用方法的前后,手动添加日志,比如我们进行了如下调用:

1
db.get('todos').insert({text: 'Use Monk'}))

我们可以在调用的前后,增加日志打印:

1
2
3
4
5
6
7
let todo = {text: 'Use Monk'}

console.log('inserting', todo)
db.get('todos').insert(todo).then((res) => {
console.log('inserting result', res)
return res
})

但是如果需要在每个调用的地方都添加日志,将会很繁琐,将会存在很多重复编写。

解决方案 2:封装接口

有解决方案 1很容易就可以想到,我们应该对这样的接口进行封装:

1
2
3
4
5
6
7
function queryAndLog(collection, method, ...args) {
console.log(method, ...args)
collection[method](...args).then((res) => {
console.log(method + ' result', res)
return res
})
}

在需要调用db.get(collection).method()的地方替换为我们自己实现的方法,例如:

1
queryAndLog(db.get('todos'), 'insert', {text: 'Use Monk'})

但每个接口都需要重新封装一遍,还有没有其他便利的方法呢?

解决方案 3:猴子补丁(Monkey patch

我们可以替换原来的insert的实现,听上去是不是有点类似 Objective-C 的 Runtime:

1
2
3
4
5
6
7
8
let next = db.get('todos').insert
db.get('todos').insert = function insertAndLog(...args) {
console.log('insert', ...args)
return next(...args).then((res) => {
console.log('insert result', res)
return res
})
}

这样在使用时跟不添加日志没有任何区别,但同样的,我们需要为每个方法都进行该替换的操作。

新的问题:崩溃报告收集

如果我们需要在insert时插入多个类似“日志记录”这种操作呢?另一个常用的是程序在出现错误时,抛出异常,我们需要将这些异常记录到服务器。通常奔溃报告收集跟日志记录是两个独立的模块:

1
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
function patchMethodToAddLogging(db, collection, method) {
let next = db.get(collection)[method]
db.get(collection)[method] = function methodAndLog(...args) {
console.log(method, ...args)
return next(...args).then((res) => {
console.log(method + ' result', res)
return res
})
}
}

function patchMethodToAddCrashReporting(db, collection, method) {
let next = db.get(collection)[method]
db.get(collection)[method] = function methodAndReportErrors(...args) {
console.log(method, ...args)
return next(...args).catch((err) => {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
method,
args
}
})
throw err
})
}
}

解决方案 4:隐藏猴子补丁

之前我们是直接替换了原有的insert方法,如果我们返回一个新方法而不是直接替换呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function logger(db, collection, method) {
let next = db.get(collection)[method]

// Previously:
// db.get(collection)[method] = function methodAndLog(...args) {

return function methodAndLog(...args) {
console.log(method, ...args)
return next(...args).then((res) => {
console.log(method + ' result', res)
return res
})
}
}

同时我们提供一个新的方法,来实现猴子补丁:

1
2
3
4
5
6
7
8
9
function applyMiddlewareByMonkeypatching(db, collection, method, middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()

// Transform dispatch function with each middleware.
middlewares.forEach(middleware =>
db.get(collection)[method] = middleware(db, collection, method)
)
}

这样我们可以在一个方法里指定多个 middleware,而且可以指定需要替换的方法:

1
applyMiddlewareByMonkeypatching(db, 'todos', 'insert', [logger, crashReporter])

但说到底还是使用了 Monkey patch。

解决方案 5:移除猴子补丁

1
let next = db.get(collection)[method]

这个赋值,最终目的是替换原始方法让我们使用。另外还有一个作用是我们可以链式的调用中间件。我们可以增加一个参数,将方法传递进来,达到同样的效果:

1
2
3
4
5
6
7
8
9
10
11
function logger(context) {
return function wrapMethodToAddLogging(next) {
return function methodAndLog(args, method) {
console.log(method, ...args)
return next(args, method).then((res) => {
console.log(method + ' result', res)
return res
})
}
}
}

柯里化之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const logger = context => next => (...args) => {
console.log(method, ...args)
return next(...args).then((res) => {
console.log(method + ' result', res)
return res
})
}

const crashReporter = context => next => (...args) => {
return next(...args).catch((err) => {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
method,
args
}
})
throw err
})
}

实现类似如下的方法,可以应用所有中间件:

1
2
3
4
5
6
7
8
9
10
11
// Warning: Naïve implementation!
// That's *not* Monk API.
function applyMiddlewares(db, collection, method, middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()
let next = collection[method]
middlewares.forEach(middleware =>
next = middleware({monkInstance: db, collection})(next)
)
return next
}

需要在每种方法上都调用该接口应用中间件链。

最终效果

我们编写如下中间件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const logger = context => next => (args, method) => {
console.log(method, args)
return next(args, method).then((res) => {
console.log(method + ' result', res)
return res
})
}

const crashReporter = context => next => (args, method) => {
return next(args, method).catch((err) => {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
method,
args
}
})
throw err
})
}

通过调用接口添加中间件:

1
2
db.addMiddleware(logger)
db.addMiddleware(crashReporter)

当我们调用如下方法时,内部实现将自动调用applyMiddlewares触发中间件的调用链:

1
2
// Will flow through both logger and crashReporter middleware!
db.get('todos').insert({text: 'Use Monk'}))
Cotin Yang wechat
欢迎订阅我的微信公众号 CotinDev
小小地鼓励一下吧~😘