title | layout | thread | date | author | categories | tags | excerpt | ||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
\[译\] 掌握 Koa 中间件 | post | 181 | 2017-11-10 | Joe Jiang | documents |
|
随着 Node 默默的实现了 async-await 的用法,Koa2 也在最近发布了。Express 似乎还占领着这场人气比赛的上风,但自 Koa2 发布以来我一直愉快的使用着,并且总是害怕回到老项目中去使用 Express... |
随着 Node 默默的实现了 async-await 的用法,Koa2 也在最近发布了。Express 似乎还占领着这场人气比赛的上风,但自 Koa2 发布以来我一直愉快的使用着,并且总是害怕回到老项目中去使用 Express.
我偶尔出没在 Koa Gitter 为大家答疑解惑,而我回答最多的是与魔法般的Koa中间件系统有关的问题,所以我居然定在这个问题上好好写写。
有很多 Koa 新手曾经使用过 Express,所以我会在两者之间进行大量的比较。
这篇文章针对的是 Koa 新人,以及正在考虑在他们下一个项目中使用 Koa 的人。
让我们从最重要的开始。在 Koa 和 Express 中,所有关于 HTTP 请求的事情都是在中间件内部完成的,最重要的则是理解中间件延续传递的概念。这听起来很奇特,但事实并非如此。它的思想是,一旦中间件完成了它的事情,它可以选择调用链中的下一个中间件。
- const express = require('express')
- const app = express()
- // Middleware 1
- app.use((req, res, next) => {
- res.status(200)
- console.log('Setting status')
- // Call the next middleware
- next()
- })
- // Middleware 2
- app.use((req, res) => {
- console.log('Setting body')
- res.send(`Hello from Express`)
- })
- app.listen(3001, () => console.log('Express app listening on 3001'))
- const Koa = require('koa')
- const app = new Koa()
- // Middleware 1
- app.use(async (ctx, next) => {
- ctx.status = 200
- console.log('Setting status')
- // Call the next middleware, wait for it to complete
- await next()
- })
- // Middleware 2
- app.use((ctx) => {
- console.log('Setting body')
- ctx.body = 'Hello from Koa'
- })
- app.listen(3002, () => console.log('Koa app listening on 3002'))
让我们用
命令来测试下两者:
- curl
- $ curl http://localhost:3001
- Hello from Express
- $ curl http://localhost:3002
- Hello from Koa
两个例子都做了同样的事情,并且都在终端上打印了相同的输出信息:
- Setting status
- Setting body
这表明在两种情况下,中间件都是自上而下的。
这里最大的区别在于 Express 中间件链是基于回调的,而 Koa 是基于 Promise 的。
让我们看看如果我们在两个例子中都省略了
,会发生什么。
- next()
- $ curl http://localhost:3001
...它永不结束。这是因为在 Express 中,你必须在调用
和发送 response 两个做法中二选一——否则请求永远不会完成。
- next()
- $ curl http: //localhost:3002
- OK
啊,所有 Koa 会完成请求,它虽然有状态码信息,但是没有任何 body 信息。所以第二个中间件是没有被调用的。
但是对于 Koa 来说还有一件事情至关重要。如果你调用
,你必须等待它!
- next()
下面就是最好的例子:
- // Simple Promise delay
- function delay (ms) {
- return new Promise((resolve) => {
- setTimeout(resolve, ms)
- })
- }
- app.use(async (ctx, next) => {
- ctx.status = 200
- console.log('Setting status')
- next() // forgot await!
- })
- app.use(async (ctx) => {
- await delay(1000) // simulate actual async behavior
- console.log('Setting body')
- ctx.body = 'Hello from Koa'
- })
让我们看看发生了什么。
- $ curl http: //localhost:3002
- OK
嗯,我们调用了
,但是没有任何 body 信息传递?这是因为 Koa 在中间件 Promise 链被解析了之后就结束了请求。这意味着在我们设置
- next()
之前,response 就已经被发送给了客户端!
- ctx.body
另一个需要明白的是如果你在使用纯粹的
替代
- Promise.then()
时,你的中间件需要返回一个 promise。当返回的 promise 被解析时,Koa 会在此时恢复之前的中间件。
- async-await
- app.use((ctx, next) => {
- ctx.status = 200
- console.log('Setting status')
- // need to return here, not using async-await
- return next()
- })
一个更好的使用纯粹 promise 的例子:
- // We don't call `next()` because
- // we don't want anything else to happen.
- app.use((ctx) => {
- return delay(1000).then(() => {
- console.log('Setting body')
- ctx.body = 'Hello from Koa'
- })
- })
在之前的章节我写到:
Koa 会在此时恢复之前的中间件
这可能会让你有些失望,请允许我解释一下。
在 Express,一个中间件只能在调用
之前做相关操作,而不是之后。一旦你调用
- next()
,请求将永远不再接触到这个中间件。这可能会有些失望,人们(包括 Express 作者自己)已经找到了一些聪明的解决方法,比如当 header 获得写入时观察响应流,但是对于普通用户来说只会感觉到尴尬。
- next()
举个例子,要实现一个记录完成请求所需的时间并将其发送到 X-ResponseTime 头部的中间件,需要一个“在下一个调用之前”的代码点和一个“在下一个调用之后”的代码点。在 Express 中,它使用流观察的技术来实现。
让我们在 Koa 中来实现它。
- async function responseTime (ctx, next) {
- console.log('Started tracking response time')
- const started = Date.now()
- await next()
- // once all middleware below completes, this continues
- const ellapsed = (Date.now() - started) + 'ms'
- console.log('Response time is:', ellapsed)
- ctx.set('X-ResponseTime', ellapsed)
- }
- app.use(responseTime)
- app.use(async (ctx, next) => {
- ctx.status = 200
- console.log('Setting status')
- await next()
- })
- app.use(async (ctx) => {
- await delay(1000)
- console.log('Setting body')
- ctx.body = 'Hello from Koa'
- })
8行,只需要这么多。没有 tricky 的流嗅探,只是看起来非常棒的 async-await 代码。让我们运行一下!这里的
标记是告诉 curl 同时也为我们显示响应的头部信息。
- -i
- $ curl -i http://localhost:3002
- HTTP/1.1 200 OK
- Content-Type: text/plain; charset=utf-8
- Content-Length: 14
- X-ResponseTime: 1001ms
- Date: Thu, 30 Mar 2017 12:52:48 GMT
- Connection: keep-alive
- Hello from Koa
非常棒!我们在 HTTP 头部中找到了响应时间。让我们再看看终端上的日志吧,看看日志是以什么顺序输出的。
- Started tracking response time
- Setting status
- Setting body
- Response time is: 1001ms
看,就是这样。Koa 让我们完全控制了中间件流程。实现类似身份验证以及错误处理的事情将会变得非常简单!
这是我最喜欢的关于 Koa 的地方,它是由上面详述的强大的中间件 Promise 链所支持的。
为了做好度量,让我们看看我们如何在 Express 中做到这一点。
错误处理是在一个特殊签名的中间件中完成的,他必须被添加到链的最后才能工作。
- app.use((req, res) = >{
- if (req.query.greet !== 'world') {
- throw new Error('can only greet "world"')
- }
- res.status(200) res.send(`Hello $ {
- req.query.greet
- }
- from Express`)
- })
- // Error handler
- app.use((err, req, res, next) = >{
- if (!err) {
- next() return
- }
- console.log('Error handler:', err.message) res.status(400) res.send('Uh-oh: ' + err.message)
- })
这是一个最好的例子。如果您正在处理来自回调或 Promise 的异步错误,则会变得非常冗长。 例如:
- app.use((req, res, next) = >{
- loadCurrentWeather(req.query.city, (err, weather) = >{
- if (err) {
- return next(err)
- }
- loadForecast(req.query.city, (err, forecast) = >{
- if (err) {
- return next(err)
- }
- res.status(200).send({
- weather: weather,
- forecast: forecast
- })
- })
- })
- next()
- })
我完全知道使用模块来处理回调地狱更容易,这里仅仅是为了证明在 Express 中简单的错误处理变得笨拙,更不用说你还需要考虑异步错误与同步错误等。
错误处理也是使用 promise 完成的。Koa 总是为我们将
包装在一个 promise 中,所以我们甚至不必担心异步与同步错误。
- next()
错误处理中间件在最顶端运行,因为它“绕过”了每一个后续的中间件。这意味着在错误处理之后添加的任何错误都会被捕获(是的,感受一下!)
- app.use(async(ctx, next) = >{
- try {
- await next()
- } catch(err) {
- ctx.status = 400 ctx.body = `Uh - oh: $ {
- err.message
- }`console.log('Error handler:', err.message)
- }
- }) app.use(async(ctx) = >{
- if (ctx.query.greet !== 'world') {
- throw new Error('can only greet "world"')
- }
- console.log('Sending response') ctx.status = 200 ctx.body = `Hello $ {
- ctx.query.greet
- }
- from Koa`
- })
是的,一个
。对于错误处理,*这是多么合适!*非
- try-catch
的方式如下所示:
- async-await
- app.use((ctx, next) => {
- return next().catch(err => {
- ctx.status = 400
- ctx.body = `Uh-oh: ${err.message}`
- console.log('Error handler:', err.message)
- })
- })
让我们出发一个错误:
- $ curl http: //localhost:3002?greet=jeff
- Uh - oh: can only greet "world"
控制台如预期般般输出:
- Error handler: can only greet "world"
与 Express 不同,Koa 几乎没有任何东西可用。没有 bodyparser,也没有路由。
在 Koa 有许多路由的选择,例如
和
- koa-route
。我更喜欢后者。
- koa-router
Express 中的路由是内置的。
- app.get('/todos', (req, res) => {
- res.status(200).send([{
- id: 1,
- text: 'Switch to Koa'
- }, {
- id: 2,
- text: '???'
- }, {
- id: 3,
- text: 'Profit'
- }])
- })
在这个例子中我选择
因为它是我正在使用的路由。
- koa-router
- const Router = require('koa-router')
- const router = new Router()
- router.get('/todos', (ctx) => {
- ctx.status = 200
- ctx.body = [{
- id: 1,
- text: 'Switch to Koa',
- completed: true
- }, {
- id: 2,
- text: '???',
- completed: true
- }, {
- id: 3,
- text: 'Profit',
- completed: true
- }]
- })
- app.use(router.routes())
- // makes sure a 405 Method Not Allowed is sent
- app.use(router.allowedMethods())
Koa 很酷。基于中间件链的完全控制,并且基于 Promise 的事实使得一切都变得容易操作起来。不再是到处的
而只有 promise.
- if (err) return next(err)
通过超强大的错误处理程序,我们可以抛出错误,更加优雅地脱离我们的代码的运行路径(想想验证错误,业务逻辑违规)。
下面是我经常使用的中间件列表(没有特定的顺序):
最好知道:并不是所有的中间件都是基于 Koa 2 可用的,然后他们可以在运行时通过
进行转换,所以不用担心。
- koa-convert
Mastering Koa Middleware, 作者 Twitter @Jeffijoe
来源: https://juejin.im/entry/5a056284f265da43085d7de3