Express的两种路由风格
在讨论Express框架的路由风格前,先让我们解决一下 async/await 的问题。
在Express框架下的路由,通常写法如下:
import * as express from 'express'
const router = express.Router()
router.get('/OK', (req, res) => {
res.send('OK')
})
对于一般的任务,如使用服务的渲染页面,重定向或者读取文件,都不会有问题。因为这些任务都有一个特征:它们都是同步的(或是提供同步的调用接口)
但是在服务器上,很多任务是异步的,而要命的是某些异步方法执行的结果,是另外一些任务的前置条件,例如下面这个例子,先需要获取User对象,才能获取对应的user.token:
router.get('/OK', async (req, res) => {
const user = await User.find({id: 1})
const data = await AsyncRedis.get(user.token)
res.resify(data)
})
如果这个时候 await User.find({id: 1}) 抛出异常,那么整个node进程会退出。也即:由async/await抛出的异常不会被errorHandler(如果有的话)所捕获。
当然,直接在路由中try/catch可以解决这个问题:
router.get('/OK', async (req, res, next) => {
try {
const user = await User.find({id: 1})
const data = await AsyncRedis.get(user.token)
res.resify(data)
} catch (e) {
next(e)
}
})
在捕获异常后,交给下一个中间件处理。不过,真的有必要在每个路由中重复一遍这个吗…?
将路由初始化之前,试着引入下面的代码,你会发现,在路由里可以自由使用 async/await 了!这是因为下方的代码直接对Router对象进行了修改,相当于提前将try/catch植入到实际的路由事件中… 等等,这种操作是不是很熟悉,是不是有点AOP(面向切面编程)的感觉了?
// typescript
/**
* Copy from https://github.com/davidbanham/express-async-errors
*
*/
import { Router } from 'express'
// @ts-ignore
import Layer = require('express/lib/router/layer')
const last = (arr = []) => arr[arr.length - 1]
const noop = Function.prototype
function copyFnProps (oldFn: { [x: string]: any }, newFn: { (...args: any[]): any;[x: string]: any }) {
Object.keys(oldFn).forEach((key) => {
newFn[key] = oldFn[key]
})
return newFn
}
function wrap (fn: { [x: string]: any; apply?: any; length?: any }) {
const newFn = function newFn (...args: any[]) {
// @ts-ignore
const ret = fn.apply(this, args)
const next = (args.length === 5 ? args[2] : last(args as any)) || noop
if (ret && ret.catch) ret.catch((err:Error) => next(err))
return ret
}
Object.defineProperty(newFn, 'length', {
value: fn.length,
writable: false,
})
return copyFnProps(fn, newFn)
}
function patchRouterParam () {
const originalParam = Router.prototype.constructor.param
Router.prototype.constructor.param = function param (name: any,
fn: { (...args: any[]): any;[x: string]: any; apply?: any; length?: any }
) {
fn = wrap(fn)
return originalParam.call(this, name, fn)
}
}
Object.defineProperty(Layer.prototype, 'handle', {
enumerable: true,
get () {
return this.__handle
},
set (fn) {
fn = wrap(fn)
this.__handle = fn
},
})
patchRouterParam()
同样的,对于Express的路由来说,既有标准的中间件风格,也有使用装饰器的AOP风格
中间件
一个Http请求经过Express服务器时,就像水流经过处理厂,一开始可能有沉淀装置,然后可能有化学净水装置,再然后是物理净水装置,最后经过二次净化后流出。这些装置就像是Express中的各种组件一样,目标是通过这些组件的组装,对Http请求作出处理和响应。这些组件被称作中间件, 一个典型例子如下:
app.use(function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', 'Content-Type, X-Refresh-Token, Authorization')
res.header('Access-Control-Allow-Methods', 'GET,POST,DELETE')
next()
})
...
const requestInterceptor = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
...
next()
}
app.use(requestInterceptor)
app.use('/', router)
app.use('/test', TestHandler)
app.use(errorHandler)
...
app.use标志着括号里的函数被作为一个中间件,来处理流经的Http请求;而第一个参数是string的则表示,对应的路径的请求,交给后面的中间件处理。每个中间件接受三个参数,分别是:req: Request,res: Response,next: NextFunction
中间件通过调用 next() 参数来传递 Http请求,如果不调用 next(),Http请求将由该中间件响应(Http Response)
如果现在需要一个身份认证的前置逻辑,对于中间件,这样写:
async function Auth(req, res, next) {
... // do something
next()
}
app.use('/', Auth, router)
// 或者
router.get('/OK', Auth, (req, res) => {
... // do something
})
注意这里的next(),这是一个中间件不可缺少的调用。使用中间件可以很轻易地实现诸如拦截器,身份验证的逻辑。
装饰器
如果说使用中间件是标准风格的话,那么装饰器风格无疑就很炫酷了。
class APIHandler {
@GET('/OK')
ok (req: express.Request, res: express.Response) {
res.resify.success()
}
}
它带来了声明式编程的体验,你总是能一眼通过装饰器洞察到它们的实际目的。
使用装饰器的路由原理也不同于使用中间件,装饰器更像是截断了原来的管道(而中间件是在原来的管道前/后添加新的管道),再用新工艺重新封装起来。
import * as express from 'express'
export const router = express.Router()
export function GET (path: string): MethodDecorator {
return function ( target: any, propertyKey: string | symbol, fn: PropertyDescriptor) {
router.get(path, fn.value)
return fn
}
}
在装饰器的定义函数中,我们没有发现熟悉的next(),甚至整个函数非常抽象,需要一点学习成本。因为@GET('/OK')在程序启动时就运行了,所以在执行router.get(path, descriptor.value)时,路由和Handler被绑定。当Express调用对应路由的handler时,将由这个被定义的ok方法处理。而在代码层面上,我们并不实际调用APIHandler().ok(req, res)。
实际上这个例子并不好,我们只是简单地进行了路由绑定而已。下面一个例子,我们为刚才的路由添加一个身份验证的装饰器:
export function Auth (): MethodDecorator {
return function (target: any, key: string, fn: PropertyDescriptor) {
const origin = fn.value
fn.value = async function (req: Request, res: Response, next: NextFunction) {
... // do something
const user = await UserSerivce.find({id: req.uid})
res.user = user
const result = await origin.apply(this, [req, res, next])
return result
}
return fn
}
}
class APIHandler {
@GET('/OK')
@Auth()
ok (req: express.Request, res: express.Response) {
res.resify.success()
}
}
看,在实际执行handler之前,我们偷偷做了点工作(把user对象挂载到 res: Response 对象上)
这不就是中间件的作用吗?只不过这次,我不需要手动调用next()了,定义Auth装饰器时,写法比定义一个中间件更复杂。好处就是带来了声明式这么直观、高效地使用方式。