Skip to content

一. koa 的基本使用


1. 认识 Koa

  • 前面我们已经学习了 express,另外一个非常流行的 Node Web 服务器框架就是 Koa

  • Koa官方的介绍:

    • koa: next generation web framework for node.js
    • koa: node.js 的下一代 web 框架
  • 事实上,koaexpress 同一个团队开发的一个新的 Web 框架:

    • 目前团队的核心开发者 TJ 的主要精力也在维护 Koaexpress 已经交给团队维护了
    • Koa 旨在为 Web 应用程序和 API 提供更小、更丰富和更强大的能力
    • koa 相对于 express 具有更强的异步处理能力
    • Koa 的核心代码只有 1600+ 行,是一个更加轻量级的框架
    • 我们可以根据需要安装和使用中间件
  • 事实上学习了 express 之后,学习 koa 的过程是很简单的

  • 安装 koa

    shell
    npm i koa

2. Koa 初体验

  • 我们来体验一下 koaWeb 服务器,创建一个接口

    • koa 也是通过注册中间件来完成请求操作的
  • koa 注册的中间件提供了两个参数:

  • ctx:上下文(Context)对象

    • koa 并没有像 express 一样,将 reqres 分开,而是将它们作为 ctx 的属性
    • ctx 代表一次请求的上下文对象
    • ctx.request:获取请求对象
    • ctx.response:获取响应对象
  • next:本质上是一个 dispatch,类似于之前的 next

    • 后续我们学习 Koa 的源码,来看一下它是一个怎么样的函数
    js
    const Koa = require('koa')
    // koa在没有提供对应的服务时,koa没有像express那样提供一个对应的页面
    const app = new Koa()
    
    // 注册中间件(middleware)
    // koa中间件有两个参数:ctx/next
    app.use((ctx, next) => {
      // 1.请求对象
      // ctx.request:koa封装的请求对象
      console.log('ctx.request: ', ctx.request)
      // ctx.req: node封装的请求对象(http.createServer中的请求对象)
      console.log('ctx.req: ', ctx.req)
    
      // 2.响应对象
      // ctx.response: koa封装的响应对象
      console.log('ctx.response: ', ctx.response)
      // ctx.res: node封装的响应对象(http.createServer中的响应对象)
      console.log('ctx.res: ', ctx.res)
    
      // 3.其他属性
      console.log('ctx.query: ', ctx.query)
      next()
    })
    
    app.use(() => {
      console.log('hello world~')
    })
    
    app.listen(6000, () => {
      console.log('koa server running~')
    })
    // koa在没有提供对应的服务时,koa没有像express那样提供一个对应的页面,而是返回Not Found

3. Koa 中间件

  • koa 通过创建的 app 对象,注册中间件只能通过 use 方法:

    • Koa 本身没有提供 methods 的方式来注册中间件
    • 也没有提供 path 中间件来匹配路径
  • 但是真实开发中我们如何将路径和 method 分离呢?

    • 方式一:根据 request 自己来判断

      js
      const Koa = require('koa')
      const app = new Koa()
      
      app.use((ctx, next) => {
        const { path, request, method } = ctx
        console.log(path === request.path) // true
        const mtd = method == 'GET' ? 'GET' : 'POST'
        switch (path) {
          case '/users':
            ctx.body = `${mtd} users`
            break
          case '/home':
            ctx.body = `${mtd} home`
            break
          case '/login':
            ctx.body = `${mtd} login`
            break
        }
      })
      
      app.listen(6000, () => {
        console.log('koa server running~')
      })
    • 方式二:使用第三方路由中间件(看下一节)

4. 路由的使用

  • koa 官方并没有给我们提供路由的库,我们可以选择第三方库@koa/router

    bash
    npm i @koa/router
  • 我们可以先封装一个 userRouter.js 的文件:

    js
    // ./router/userRouter.js
    const KoaRouter = require('@koa/router')
    
    // 1.创建路由对象
    const userRouter = new KoaRouter({
      prefix: '/users',
    })
    
    // 2.在路由中注册中间件:path/method
    userRouter.get('/', (ctx, next) => {
      ctx.body = 'users 01'
    })
    userRouter.get('/:id', (ctx, next) => {
      const { id } = ctx.params
      ctx.body = '获取某一个用户' + id
    })
    userRouter.post('/', (ctx, next) => {
      ctx.body = '创建用户成功'
    })
    userRouter.delete('/:id', (ctx, next) => {
      const { id } = ctx.params
      ctx.body = '删除某一个用户' + id
    })
    userRouter.patch('/:id', (ctx, next) => {
      const { id } = ctx.params
      ctx.body = '修改某一个用户' + id
    })
    
    module.exports = userRouter
  • app 中将 router.routes() 注册为中间件:

    js
    const Koa = require('koa')
    const userRouter = require('./router/userRouter')
    
    const app = new Koa()
    
    // 3.让路由中的中间件生效
    app.use(userRouter.routes())
    app.use(userRouter.allowedMethods())
    
    app.listen(6000, () => {
      console.log('koa server running~')
    })

注意:

  • allowedMethods() 方法用于判断某一个 method 是否支持:

  • 如果我们请求 get,那么是正常的请求,因为我们有实现 get

  • 如果我们请求 putdeletepatch,会自动报错:Method Not Allowed,状态码:405

  • 如果我们请求 linkcopylock,会自动报错:Not Implemented,状态码:501

二. koa 的参数解析


  • 下面章节参数解析示例都是基于以下配置:

    js
    const Koa = require('koa')
    const KoaRouter = require('@koa/router')
    
    const app = new Koa()
    const userRouter = new KoaRouter({
      prefix: '/users',
    })
    
    /**
     * 1.get: params方式,例子:/id
     * 2.get: query方式,例子:?name=later-zc&age=1
     * 3.post: json方式,例子:{"name": "later-zc"}
     * 4.post: x-www-form-urlencode
     * 5.post: form-data
     */
    
    app.use(userRouter.routes())
    app.use(userRouter.allowedMethods())
    
    app.listen(6000, () => {
      console.log('koa server running~')
    })

1. 参数解析:params - query

2. 参数解析:json

  • 请求地址:http://localhost:6000/users

  • bodyjson 格式:{"name": "later-zc", "password": "123"}

  • 获取 json 数据:

    • 安装依赖:

      bash
      npm i koa-bodyparser
    • 使用 koa-bodyparser 的中间件

      js
      const bodyParser = require('koa-bodyparser')
      // ...
      app.use(bodyParser())
      
      // post/json
      userRouter.post('/', (ctx, next) => {
        const { name, age } = ctx.request.body
        ctx.body = name + ' ' + age
      })

    注意:

    • 不能从 ctx.body 中获取,ctx.body 用来返回数据

3. 参数解析:x-www-form-urlencoded

  • 请求地址:http://localhost:6000/users/urlencode

    • bodyx-www-form-urlencoded 格式:
    image-20230215003935643
  • 获取 json 数据:(和 json 是一致的)

    • 安装依赖:

      bash
      npm i koa-bodyparser
    • 使用 koa-bodyparser 的中间件

      js
      const bodyParser = require('koa-bodyparser')
      // ...
      app.use(bodyParser())
      
      // post/urlencode
      userRouter.post('/urlencode', (ctx, next) => {
        const { name } = ctx.request.body
        ctx.body = name
      })

4. 参数解析:form-data

  • 请求地址:http://localhost:8000/login

    • bodyform-data 格式:
    image-20230215004111354
  • 解析 body 中的数据,我们需要使用 multer

    • 安装依赖:

      bash
      npm i @koa/multer multer
    • 使用 multer 中间件

      js
      const multer = require('@koa/multer')
      // ...
      const formParser = multer()
      
      // form-data
      userRouter.post('/form', formParser.any(), (ctx, next) => {
        const { name } = ctx.request.body
        ctx.body = name
      })

5. Multer上传文件

js
const Koa = require('koa')
const KoaRouter = require('@koa/router')
const multer = require('@koa/multer')

const app = new Koa()
const upload = multer({
  // dest: './uploads',
  storage: multer.diskStorage({
    destination(req, file, cb) {
      cb(null, './uploads')
    },
    filename(req, file, cb) {
      cb(null, Date.now() + '_' + file.originalname)
    },
  }),
})
const uploadRouter = new KoaRouter({
  prefix: '/upload',
})

// 单文件
uploadRouter.post('/avatar', upload.single('avatar'), (ctx, next) => {
  console.log('ctx.request.file: ', ctx.request.file)
  ctx.body = '文件上传成功'
})
// 多文件
uploadRouter.post('/photos', upload.array('photos'), (ctx, next) => {
  console.log('ctx.request.files: ', ctx.request.files)
  ctx.body = '文件上传成功'
})

app.use(uploadRouter.routes())
app.use(uploadRouter.allowedMethods())

app.listen(6000, () => {
  console.log('koa server running~')
})

三. koa 响应和错误


1. 数据的响应

  • 输出结果:body 将响应主体设置为以下之一:

    • string:字符串数据
    • BufferBuffer 数据
    • Stream:流数据
    • Object||Array:对象或者数组
    • null:不输出任何内容
    • 如果 response.status 尚未设置,Koa 会自动将状态设置为 200 204
  • 请求状态:status

    js
    const fs = require('fs')
    const Koa = require('koa')
    const KoaRouter = require('@koa/router')
    const bodyParser = require('koa-bodyparser')
    
    const app = new Koa()
    
    app.use(bodyParser())
    
    const userRouter = new KoaRouter({
      prefix: '/users',
    })
    
    userRouter.get('/', (ctx, next) => {
      // 1.body类型是string
      // ctx.body = 'hello world'
    
      // 2.body类型是Buffer
      // ctx.body = Buffer.from('hello world')
    
      // 3.body的类型是Stream
      // const readStream = fs.createReadStream('./uploads/01.png')
      // ctx.type = 'image/png'
      // ctx.body = readStream
    
      // 4.body的类型是数据(array/object)
      // ctx.body = {
      //   code: 0,
      //   data: [
      //     { id: 111, price: 99 },
      //     { id: 112, price: 89 },
      //   ],
      // }
    
      // 5.body的类型是null, 自动设置http状态码为204 no content
      ctx.body = null
    })
    
    app.use(userRouter.routes())
    app.use(userRouter.allowedMethods())
    
    app.listen(8000, () => {
      console.log('koa server running~')
    })

2. 错误处理

js
const Koa = require('koa')
const KoaRouter = require('@koa/router')

const app = new Koa()

const userRouter = new KoaRouter({
  prefix: '/users',
})

userRouter.get('/', (ctx, next) => {
  const isAuth = false
  if (isAuth) {
    ctx.body = {
      username: 'later-zc',
      age: 18,
    }
  } else {
    // EventEmitter
    ctx.app.emit('error', -1003, ctx)
  }
})

app.use(userRouter.routes())
app.use(userRouter.allowedMethods())

// 独立的文件:error-handle.js
app.on('error', (code, ctx) => {
  let message = '未知错误'
  switch (code) {
    case -1001:
      message = '账户或密码错误'
      break
    case -1002:
      message = '请求参数不正确'
      break
    case -1003:
      message = 'token未授权'
      break
  }
  ctx.body = {
    code,
    message,
  }
})

app.listen(8000, () => {
  console.log('koa server running~')
})

四. koa 静态服务器


  • koa 并没有内置部署相关的功能,所以我们需要使用第三方库:

    bash
    npm i koa-static
  • 部署的过程类似于 express

    js
    const Koa = require('koa')
    const static = require('koa-static')
    
    const app = new Koa()
    
    app.use(static('./uploads')) // 静态资源所在目录,访问时:http://127.0.0.1:8000/01.png
    app.use(static('./build'))
    
    app.listen(8000, () => {
      console.log('koa server running~')
    })

五. koa 的源码解析


1. 创建 Koa 的过程

image-20230216220056394

2. 开启监听

image-20230216220155936

3. 注册中间件

image-20230216220244806

4. 监听回调

image-20230216220349542

5. compose 方法

image-20230216220712347

六. 和 express 对比


1. 架构设计对比

  • 在学习了两个框架之后,我们应该已经可以发现 koaexpress 的区别:
  • 从架构设计上来说:
    • express 是完整和强大的,其中帮助我们内置了非常多好用的功能
    • koa 是简洁和自由的,它只包含最核心的功能,并不会对我们使用其他中间件进行任何的限制
    • 甚至是在 app 中连最基本的 getpost 都没有给我们提供
    • 我们需要通过自己或者路由来判断请求方式或者其他功能
  • 因为 expresskoa 框架他们的核心其实都是中间件:
    • 但是他们的中间件事实上,它们的中间件的执行机制是不同的,特别是针对某个中间件中包含异步操作时
    • 所以,接下来,我们再来研究一下 expresskoa 中间件的执行顺序问题

2. 案例对比

  • 我通过一个需求来演示所有的过程:

    • 假如有三个中间件会在一次请求中匹配到,并且按照顺序执行
    • 我希望最终实现的方案是:
      • middleware1 中,在 req.message 中添加一个字符串 aaa
      • middleware2 中,在 req.message 中添加一个 字符串 bbb
      • middleware3 中,在 req.message 中添加一个 字符串 ccc
      • 当所有内容添加结束后,在 middleware1 中,通过 res 返回最终的结果
  • 实现方案:

    • Express 同步数据的实现

      js
      const express = require('express')
      
      const app = express()
      app.use((req, res, next) => {
        req.msg = 'aaa'
        console.log('express middleware 01')
        next()
        console.log('first')
        res.end(req.msg)
      })
      
      app.use((req, res, next) => {
        req.msg += 'bbb'
        console.log('express middleware 02')
        next()
      })
      
      app.use((req, res, next) => {
        req.msg += 'ccc'
        console.log('express middleware 03')
      })
      
      // output:
      // express middleware 01
      // express middleware 02
      // express middleware 03
      // first
      
      // 服务器响应结果:aaabbbccc
      
      app.listen(8000, () => {
        console.log('express server running~')
      })
    • Koa 同步数据的实现

      js
      const Koa = require('Koa')
      
      const app = new Koa()
      app.use((ctx, next) => {
        console.log('koa middleware 01')
        ctx.msg = 'aaa'
        next()
        console.log('first')
        ctx.body = ctx.msg
      })
      
      app.use((ctx, next) => {
        ctx.msg += 'bbb'
        console.log('koa middleware 02')
        next()
      })
      
      app.use((ctx, next) => {
        ctx.msg += 'ccc'
        console.log('koa middleware 03')
      })
      
      // output:
      // koa middleware 01
      // koa middleware 02
      // koa middleware 03
      // first
      
      // 服务器响应结果:aaabbbccc
      
      app.listen(8000, () => {
        console.log('koa server running~')
      })
    • Express 异步数据的实现

      js
      const express = require('express')
      const axios = require('axios')
      
      const app = express()
      
      app.use(async (req, res, next) => {
        console.log('express middleware 01')
        req.msg = 'aaa'
        await next()
        console.log('first')
        res.end(req.msg)
      })
      
      app.use(async (req, res, next) => {
        console.log('express middleware 02')
        req.msg += 'bbb'
        await next()
      })
      
      app.use(async (req, res, next) => {
        console.log('express middleware 03')
        const result = await '---' // 这里比较特殊,如果是await表达式是一个普通值,next会继续等待后续代码执行完成
        req.msg += result
        console.log('111')
        // 如果遇到await表达式是网络请求返回promise的形式,next不会等待后面的代码执行完成,即使是await next()
        const result2 = await axios.get('http://123.207.32.32:8000/home/multidata')
        req.msg += result2.data.data.banner.list[0].title
        console.log('222')
      })
      
      // output:
      // express server running~
      // express middleware 01
      // express middleware 02
      // express middleware 03
      // 111
      // first
      // 222
      
      // 服务器返回结果:aaabbb---
      
      app.listen(8000, () => {
        console.log('express server running~')
      })
    • Koa 异步数据的实现

      js
      const Koa = require('Koa')
      const axios = require('axios')
      
      const app = new Koa()
      
      app.use(async (ctx, next) => {
        console.log('koa middleware 01')
        ctx.msg = 'aaa'
        await next()
        console.log('first')
        ctx.body = ctx.msg
      })
      
      app.use(async (ctx, next) => {
        console.log('koa middleware 02')
        ctx.msg += 'bbb'
        // 如果执行的下一个中间件是一个异步函数,那么next默认不会等到中间件的结果,就会执行下一步操作
        // 如果希望等待下一个异步函数的执行结果,则需要在next函数前面加await
        await next()
        console.log('立即执行')
      })
      
      app.use(async (ctx, next) => {
        console.log('koa middleware 03')
        const res = await '---' // 模拟网络请求
        ctx.msg += res
      })
      
      // output:
      // koa middleware 01
      // koa middleware 02
      // koa middleware 03
      // 立即执行
      // first
      
      // 服务器响应结果:aaabbb---
      
      app.listen(8000, () => {
        console.log('koa server running~')
      })

总结:

  • 默认 next 会等待下一个中间件函数执行完成,才会继续执行当前中间件中的代码,如果 next 执行的下一个中间件中有异步操作,那么 next 默认是不会等待该中间件的异步处理完成,就会执行当前中间件函数中的后续代码
  • 没有 awaitnext,会根据同步代码中的响应数据返回给客户端,如果同步中没有响应数据给客户端,则挂起(即404),如果有的话,则根据同步响应数据中的先后顺序,后面的响应覆盖前面的
  • 如果希望等待下一个其中带有异步操作的中间件函数的执行结果,koa 则需要在 next 函数前面加 await,而 express 即使加上 await 也不行

注意:

  • 经测试,在express中,如果希望等待下一个异步中间件函数的执行结果,上面我们说过即使加上 await 也不行,但是有一种特殊情况,就是如下情况:

    js
    const result = await '---' // 这里比较特殊,如果await表达式是一个普通值,不影响后续代码
    req.msg += result
    console.log('111')
    // 如果await表达式是网络请求返回promise的形式,这后面的代码会影响,next不会等待
    const result2 = await axios.get('http://123.207.32.32:8000/home/multidata')
    req.msg += result2.data.data.banner.list[0].title
    console.log('222')
  • 猜测:应该是如果 await 后面表达式是一个非异步处理,await 其内部源码实现应该做了边界处理,从而对这类非异步的表达式当成同步操作立马返回结果

3. koa 洋葱模型

  • 洋葱模型官方并无此概念,而是社区中有人提出的,两层理解含义:
    • 中间件处理代码的过程
    • Response 返回 body 执行
image-20230216193018674

Released under the MIT License.