对比express

  • 编程模型不同
    • express的中间件是线型的
    • Koa的中间件是U型的
  • 对语言特性的使用不同
    • express使用回调函数next()
    • Koa1 使用generator语法
    • Koa2 使用async/await语法

回顾一下之前的express的中间件模型
image.png

Koa的中间件模型

image.png

  • Koa的中间件模型是一个U型
  1. 老王发一个请求->开始->f1->f2>f3>…..>结尾 (这是往下的过程)
  2. 从结尾又回去(u型的另外一边) 结尾->…..->f3->f2->f1->响应给老王
  • 开始阶段
    • 主要用来构造一个叫context的变量
  • f1,f2,f3等中间件
    • 访问开始阶段构造的context
    • 调用await next() 进入下一个函数

工具安装

  • npm i -g node-dev ts-node-dev || yarn global add node-dev ts-node-dev
  • 如果要用ts,需要运行tsc —init 初始化tsconfig.
  • 还需要安装 @types/koa

koa-demo-1

安装:
$ yarn add koa
$ yarn add —dev @types/koa
创建server.ts
$ tsc —init

server.ts

  1. import Koa from 'koa'
  2. const app = new Koa()
  3. app.use(async(ctx)=>{
  4. ctx.body = 'hello'
  5. })
  6. app.listen(3000)

$ ts-node-dev server.ts
打开localhost:3000
image.png

改进一下

  1. import Koa from 'koa'
  2. const app = new Koa()
  3. app.use(async(ctx,next)=>{
  4. ctx.body = ' hello'
  5. await next()
  6. ctx.body += '\n await next string'
  7. })
  8. app.use(async (ctx)=>{
  9. ctx.body += ' world'
  10. })
  11. app.listen(3000)

image.png
从这个demo可以很好的看出这个u型的模型是怎么运行的

  1. 在第一个app.use()里 ctx.body写入 hello
  2. 在第二个app.use() 增加world
  3. 上面两步是u型往下的部分,第二个app.use()已经到底了。开始u型往上的部分
  4. 网上又开始执行第二个app.use(),再执行第一个app.use(),这时候ctx.body加上了string,这点我们可以从网页上推导到的,一五年awaite next string是出现在第二行的
  5. 所以await next()这个方法之后的js,会在u型往上的阶段执行

koa-demo-2

需求:记录一个请求从开始到写完hello world 所需要的时间

  1. import Koa from 'koa'
  2. const app = new Koa()
  3. app.use(async (ctx, next) => {
  4. // 空
  5. await next();
  6. const time = ctx.response.get('X-Response-Time'); //读取response header
  7. console.log(`${ctx.url} - ${time}`); // 打印出来
  8. });
  9. app.use(async (ctx, next) => {
  10. const start = Date.now(); //记录开始时间
  11. await next();
  12. const time = Date.now() - start; //记录总耗时 = 结束时间 - start
  13. ctx.set('X-Response-Time', `${time}ms`); //写到header里
  14. });
  15. app.use(async ctx => {
  16. ctx.body = 'Hello World';
  17. // await next() 最后一个中间件,可以不写await next()
  18. });
  19. app.listen(3000)
  20. //记录这个请求,从开始到写完hello world整个过程的用时
  1. // 代码行数
  1. 开始fn1什么都不做,next了 // 6
  2. fn2 记录开始时间 // 12
  3. fn3往ctx.body写入hello world // 18
  4. 到底了,开始从下往上走
  5. fn2 记录总耗时,并写到response header里 // 14-15
  6. fn1 读取response header , 然后打印出来 // 8-9

从代码行数也可以看出来是一个先从上往下,到底部再从下往上的一个过程。
koa2的代码,一开始是看不出来的,必须要大脑里有这个u型模型,带着这个u去看,才能看出来。

用u型的模型去做这种需求,是比较好做的,那么用线型的express呢?
比较麻烦。
要在fn1记录开始时间,然后在最后插入一个fn4来计算时间,还要保证fn2和fn3的运行时间必须要在它们之间。
做起来很别扭

洋葱模型

看回koa-demo-2的代码,看一下它的运行顺序
image.png

这个运行顺序,有人把它描述为洋葱模型
image.png
怎么理解呢:

  • 你写f1的时候,这个f1对应的是最外层的洋葱皮,f2是第二层洋葱皮,f3第三层,f4第四层,一直到f8就是最里层。
  • 看图中的request,粗黑色箭头和response
    • 每次Request过来的时候,就会先经过洋葱的最外层f1,然后f2 …最后到最里层f8,最后到达洋葱的中心
    • 然后从洋葱的中心穿出去,又经过f8,f7,f6…..到f2,f1,最后把response发出去
  • 所以是request 贯穿了洋葱 变成response
  • 但是这个图不是Koa的原型图,实际上这个图是Python Web框架Pylons的框架图(想了解可以点击链接)

但是我觉得u型图更好理解一点。

await next()

  1. app.use(async(ctx,next)=>{
  2. const start = Date.now()
  3. await next() //等待 下一个中间件执行
  4. const time = Date.now - start
  5. ctx.set(`X-Request-Time`, `${time}ms`)
  6. })
  • next() 表示进入下一个函数
  • 下一个函数会返回一个Promise对象 ,P
    • 当前的函数也会返回一个promise,在哪里返回的呢,只要你写了async,他就会返回一个promise。
  • 下一个函数所有代码执行完毕后,将p设置为成功
  • await会等待P成功后,再回头执行剩余的代码 (4、5行)

await next() 改成promise写法

  1. app.use(async(ctx,next)=>{
  2. const start = Date.now()
  3. return next().then(()=>{
  4. const time = Date.now - start
  5. ctx.set(`X-Request-Time`, `${time}ms`)
  6. }) //等待 下一个中间件执行
  7. })

如上面所提到的,next().then会返回一个promise对象,所以next.then之后,就是下一个函数执行成功了,在执行then里面的代码。
return是必须要写的,因为每一个函数都要返回一个promise对象才行(当前的函数就是上个函数的下一个函数,下一个函数都要返回一个promise对象)
因为当前返回了一个promise对象之后,上一个函数才会知道“你成功了,我要执行了”。

  • 一定要写return,因为中间件必返回Promise对象
  • 错误处理在这里有点反模式,用app.on(‘error’)更方便(因为有可能你不知道下一个中间件做了什么,不知道的话是很难捕获错误的)