在讨论Express框架的路由风格前,先让我们解决一下 async/await
的问题。
在Express框架下的路由,通常写法如下:
1 | import * as express from 'express' |
对于一般的任务,如使用服务的渲染页面,重定向或者读取文件,都不会有问题。因为这些任务都有一个特征:它们都是同步的(或是提供同步的调用接口)
但是在服务器上,很多任务是异步的,而要命的是某些异步方法执行的结果,是另外一些任务的前置条件,例如下面这个例子,先需要获取User
对象,才能获取对应的user.token
:
1 |
|
如果这个时候 await User.find({id: 1})
抛出异常,那么整个node进程会退出。也即:由async/await
抛出的异常不会被errorHandler
(如果有的话)所捕获。
当然,直接在路由中try/catch
可以解决这个问题:
1 |
|
在捕获异常后,交给下一个中间件处理。不过,真的有必要在每个路由中重复一遍这个吗…?
将路由初始化之前,试着引入下面的代码,你会发现,在路由里可以自由使用 async/await
了!这是因为下方的代码直接对Router
对象进行了修改,相当于提前将try/catch
植入到实际的路由事件中… 等等,这种操作是不是很熟悉,是不是有点AOP(面向切面编程)
的感觉了?
1 | // typescript |
同样的,对于Express的路由来说,既有标准的中间件
风格,也有使用装饰器的AOP
风格
中间件
一个Http请求经过Express服务器时,就像水流经过处理厂,一开始可能有沉淀装置,然后可能有化学净水装置,再然后是物理净水装置,最后经过二次净化后流出。这些装置就像是Express中的各种组件一样,目标是通过这些组件的组装,对Http请求作出处理和响应。这些组件被称作中间件
, 一个典型例子如下:
1 | app.use(function (req, res, next) { |
app.use
标志着括号里的函数被作为一个中间件,来处理流经的Http请求;而第一个参数是string
的则表示,对应的路径的请求,交给后面的中间件处理。每个中间件接受三个参数,分别是:req: Request,res: Response,next: NextFunction
中间件通过调用 next()
参数来传递 Http请求,如果不调用 next()
,Http请求将由该中间件响应(Http Response)
如果现在需要一个身份认证的前置逻辑,对于中间件,这样写:
1 | async function Auth(req, res, next) { |
注意这里的next()
,这是一个中间件不可缺少的调用。使用中间件可以很轻易地实现诸如拦截器,身份验证的逻辑。
装饰器
如果说使用中间件是标准风格的话,那么装饰器风格无疑就很炫酷了。
1 |
|
它带来了声明式编程的体验,你总是能一眼通过装饰器洞察到它们的实际目的。
使用装饰器的路由原理也不同于使用中间件,装饰器更像是截断了原来的管道(而中间件是在原来的管道前/后添加新的管道),再用新工艺重新封装起来。
1 |
|
在装饰器的定义函数中,我们没有发现熟悉的next()
,甚至整个函数非常抽象,需要一点学习成本。因为@GET('/OK')
在程序启动时就运行了,所以在执行router.get(path, descriptor.value)
时,路由和Handler被绑定。当Express调用对应路由的handler时,将由这个被定义的ok
方法处理。而在代码层面上,我们并不实际调用APIHandler().ok(req, res)
。
实际上这个例子并不好,我们只是简单地进行了路由绑定而已。下面一个例子,我们为刚才的路由添加一个身份验证的装饰器:
1 |
|
看,在实际执行handler之前,我们偷偷做了点工作(把user
对象挂载到 res: Response
对象上)
这不就是中间件的作用吗?只不过这次,我不需要手动调用next()
了,定义Auth装饰器时,写法比定义一个中间件更复杂。好处就是带来了声明式这么直观、高效地使用方式。