Skip to content

一. Koa 的基本使用

引言

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

认识

官方介绍:

英:next generation web framework for node.js
中:node.js 的下一代 web 框架

事实上,Koa 是 express 同一个团队开发的一个新的 Web 框架:

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

事实上学习了 express 之后,学习 Koa 的过程是很简单的。

安装 Koa:

bash
npm i koa

初体验

我们来体验一下 Koa 的 Web 服务器,创建一个接口:

Koa 也是通过注册中间件来完成请求操作的。

Koa 注册的中间件提供了两个参数:

  • ctx:上下文对象

    Koa 并没有像 express 一样,将 reqres 分开,而是将它们作为 ctx 的属性。
    ctx 代表一次请求的上下文对象。
    ctx.request:获取请求对象。
    ctx.response:获取响应对象。

  • next:本质上是一个 dispatch,类似于之前的 next

    后续我们学习 Koa 的源码,来看一下它是一个怎么样的函数。

示例:

js
const Koa = require('koa')
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。

中间件

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~')
    })
  • 方式二:使用第三方路由中间件(看下一节)

路由

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

bash
npm i @koa/router

我们可以先封装一个 userRouter.js 的文件:

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~')
})

参数解析:path / query

  • 路径参数

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

js
// params
userRouter.get('/:userID', (ctx, next) => {
  const { userID } = ctx.params
  ctx.body = 'params ' + userID // params 123
})
  • 查询参数

请求地址:http://localhost:6000/users?username=later-zc&age=18

js
// query
userRouter.get('/', (ctx, next) => {
  const { name, age } = ctx.query
  ctx.body = name + ' ' + age // later-zc 18
})

参数解析:body - json

请求地址:http://localhost:6000/users
bodyjson 格式:{"name": "later-zc", "password": "123"}

获取 JSON 数据:

  • 安装 koa-bodyparser 依赖:

    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 用来返回数据。

参数解析:body - x-www-form-urlencoded

请求地址:http://localhost:6000/users/urlencode
bodyx-www-form-urlencoded 格式:

image-20230215003935643

获取 JSON 数据:(和解析 json body 是一致的):

  • 安装 koa-bodyparser 依赖:

    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
    })

参数解析:body - form-data

请求地址:http://localhost:8000/login
bodyform-data 格式:

image-20230215004111354

解析 body 中的数据,我们需要使用 multer

  • 安装 @koa/multermulter 依赖:

    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
    })

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 响应和错误

响应数据

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

  • string:字符串数据
  • Buffer:Buffer 数据
  • 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~')
})

错误处理

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 的源码解析

创建 Koa 的过程

image-20230216220056394

开启监听

image-20230216220155936

注册中间件

image-20230216220244806

监听回调

image-20230216220349542

compose 方法

image-20230216220712347

六. 和 express 对比

架构设计对比

在学习了两个框架之后,我们应该已经可以发现 Koaexpress 的区别:

从架构设计上来说:

express 是完整和强大的,其中帮助我们内置了非常多好用的功能

Koa 是简洁和自由的,它只包含最核心的功能,并不会对我们使用其他中间件进行任何的限制,甚至是在 app 中连最基本的 getpost 都没有提供。 我们需要通过自己或者路由来判断请求方式或者其他功能。

因为 express 和 Koa 框架他们的核心其实都是中间件:

但是他们的中间件事实上,它们的中间件的执行机制是不同的,特别是针对某个中间件中包含异步操作时。 所以,接下来我们再来研究一下 expressKoa 中间件的执行顺序问题。

案例对比

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

  • 假如有三个中间件会在一次请求中匹配到,并且按照顺序执行
  • 我希望最终实现的方案是:
    • 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 其内部源码实现应该做了边界处理,从而对这类非异步的表达式当成同步操作立马返回结果。

Koa 洋葱模型

洋葱模型 官方并无此概念,而是社区中有人提出的,两层理解含义:

  • 中间件处理代码的过程
  • Response 返回 body 执行
image-20230216193018674

如有转载或CV请标注本站原文地址