5.5 Cookie操作
Cookie是Web应用维持少量数据的一种手段,通过Cookie,服务端可以表示用户以及用户身份。本节我们将学习一下Koa如何操作Cookie。
5.5.1 Cookie签名
由于Cookie存放在浏览器端,存在篡改风险,因此Web应用一般会在存放数据的时候同时存放一个签名cookie,以保证Cookie内容不被篡改。
Koa中需要配置Cookie签名密钥才能使用Cookie功能,否则将报错。
const Koa = require('koa'); const app = new Koa(); app.keys = ['signedKey']; // 推荐使用随机字符串
5.5.2 写入Cookie
访问http://localhost:10000,查看浏览器的Cookie列表,结果如图5-1所示。
图5-1
可以看到除了我们设置的“logged”之外,多了个“logged.sig”,这就是用来签名的Cookie。服务端读取“logged”时,还会同时读取“logged.sig”,一旦发现签名不匹配,则读取到的cookie值为“undefined”。
5.5.3 读取Cookie
// 导入模块 const Koa = require('koa'); const app = new Koa(); app.keys = ['signedKey']; app.use(async (ctx) => { const logged = ctx.cookies.get('logged', { signed: true }); ctx.body = logged; }); // 监听 app.listen(10000, () => { console.log('listen on 10000'); });
访问http://localhost:10000,浏览器将显示“1”。
ctx.cookies.get()建议传递signed选项来验证签名,否则cookie将有篡改风险。
5.5.4 中间件
与Express类似,Koa的中间件也能访问请求对象、响应对象和next函数,通常用来执行以下任务:
· 执行逻辑代码。
· 更改请求和响应对象。
· 结束请求-响应周期。
· 调用下一个中间件。
· 错误处理。
如果一个请求流程中,任何中间件都没有输出响应,Koa中此次请求将返回404状态码(Express会将请求挂起)。
造成这种差别的原因是Express需要手动执行输出函数才可以结束请求流程,而Koa使用了async/await来进行异步编程,不需要执行回调函数,直接对ctx.body赋值即可。
Koa的中间件是一个标准的异步函数,函数签名如下:
async function middleware(ctx, next)
· ctx:上下文对象。
· next:下一个中间件。
运行完逻辑代码,将需要传递的数据挂载到ctx.state,并且调用await next()才能将请求交给下一个中间件处理。
为了更好地理解中间件的执行流程,下面使用图5-2所示的模型来说明。
图5-2
Koa的中间件模型称为“洋葱圈模型”,请求从左边进入,有序地经过中间件处理,最终从右边输出响应。
最先use的中间件在最外层,最后use的中间件在最内层。
一般的中间件会执行两次(下面会给出示例),调用next之前为第一次,也就是“洋葱左半边”这一部分,从外层向内层依次执行。当后续没有中间件时,就进入响应流程,也就是“洋葱右半边”这一部分,从内层向外层依次执行,这是第二次执行。
下面举一个例子以方便大家理解该模型。
// 导入模块 const Koa = require('koa'); // 实例化应用 const app = new Koa(); async function middleware1(ctx, next) { console.log('middleware1 start'); await next(); console.log('middlware1 end') } async function middleware2(ctx, next) { console.log('middleware2 start'); await next(); console.log('middlware2 end') } // 中间件 app.use(middleware1); app.use(middleware2); // 路由 app.use(async (ctx) => { console.log('router'); ctx.body = 'Hello World'; }); // 监听 app.listen(10000, () => { console.log('listen on 10000'); });
访问http://localhost:10000时,终端输出如下:
middleware1 start middleware2 start router middlware2 end middlware1 end
也就是说,执行流程如下:
请求→中间件1开始→中间件2开始→路由处理→中间件2结束→中间件1结束→响应。
5.5.5 请求日志中间件
由于每个中间件有两次执行机会,因此相比于Express监听finish事件的方式,Koa的中间件显得更加简单。
下面我们来开发一个日志中间件,该中间件会输出请求方法、请求路径、User-Agent和处理时长。
访问http://localhost:10000,输出如下:
GET / "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36" 2ms
在logger中,我们在调用next()之前记录当前请求时间,调用next()后logger将不再执行,直接执行下一个中间件。本例中下一个中间件就是我们的路由了,路由执行完毕后,再来执行logger中间件next()之后的代码。
5.5.6 可配置的中间件
与Express类似,Koa也可以通过“函数返回一个新函数”来编写可配置的中间件。
下面通过配置选项来控制日志输出的内容:
访问http://localhost:10000,由于未开启user-agent来显示网页,因此终端输出如下:
GET / 2 ms
5.5.7 Cookie解析中间件
与上面的中间件不同,本节编写的中间件解析完请求Cookie之后,需要将其挂载到ctx.state中,然后在路由中间件使用。
访问http://localhost:10000,输出如下:
{ "Idea-26226ea6": "760672c4-a540-4189-a6d4-8d71c8b48fdd", "grafana_session": "20e77e60cad3c900ad12cf8d6dc2c1cb" }
有些第三方中间件处理完数据并未挂载到state下,这不影响使用,但不是Koa推荐的做法,笔者建议各位读者还是将自定义的数据挂载到ctx.state下。
5.5.8 路由函数
Koa核心并没有提供路由功能,但是可以使用一个默认的路由函数来提供响应。所有的请求都会执行该默认的路由函数。
路由函数的定义如下:
async function(ctx, next)
· ctx:请求上下文。
· next:下一个路由函数。
如果路由函数内部未使用异步逻辑,async是可以省略的。
如下示例代码是合法且能执行的:
app.use((ctx) => { ctx.body = 'Hello World'; });
5.5.9 多个路由函数
一般来说,路由函数只有一个,设置响应数据到ctx.body,执行完中间件后,请求终止。但是Koa支持同一个路由来使用多个路由函数。
// 导入模块 const Koa = require('koa'); const Router = require('koa-router'); // 实例化应用 const app = new Koa(); app.use(async (ctx, next) => { //路由函数1 ctx.body = '1'; await next(); }); app.use((ctx) => { // 路由函数2 ctx.body = '2'; }); // 监听 app.listen(10000, () => { console.log('listen on 10000'); });
访问http://localhost:10000,将“2”输出到浏览器。
如果将路由函数1中的代码修改一下,将“1”输出到浏览器:
app.use(async (ctx, next) => { await next(); ctx.body = '1'; });
因为Koa的洋葱圈模型,所以先执行路由函数1,执行到next()后,进入路由函数2的执行,路由函数2设置了ctx.body为“2”,此时没有后续路由函数,执行流程将回到路由函数1,而此时路由函数1将ctx.body设置为“1”,所以浏览器最终显示“1”。
5.5.10 错误处理
Koa有着简单且优雅的中间件机制,因此编写错误处理中间件变得很简单。
和Express错误处理中间件需要放置在应用末尾不同,Koa采用了洋葱圈模型,所以Koa的错误处理中间件需要在应用的开始处挂载,这样才能将整个请求-响应周期涵盖,捕获其发生的错误。
访问http://localhost:1000时,将输出“System Error: Forbidden”。
如果调换一下路由与错误处理中间件之间的位置呢?
访问http://localhost:10000,将输出“Forbidden”,也就是说错误处理器放在后面是不生效的,最终采用了Koa自带的错误处理机制。
5.5.11 多个错误处理器
在生产环境的应用中,发生错误除了要显示错误信息给客户端之外,还需要上报错误、记录日志等操作,因此为了项目的可维护性,一般需要将错误处理中间件进行拆分,拆分为错误响应中间件、日志记录中间件等,每个中间件只负责一项工作。
访问http://localhost:10000,终端将显示“GET / Error: Forbidden”,之后客户端将收到“System Error: Forbidden”。