Skip to content

Koa 框架

Koa 核心概念、中间件机制、路由与最佳实践

Koa 概述

特点

  • 异步优先,由 Express 原班人马打造
  • 极简设计,无内置路由和中间件
  • 洋葱模型中间件
  • 使用 async/await
  • 错误处理更优雅

与 Express 对比

特性KoaExpress
中间件模型洋葱模型线性模型
异步处理原生 async/await回调/Promise
路由需单独安装内置
体积更轻量相对较大
学习曲线稍陡较平缓

核心概念

应用创建

javascript
import Koa from 'koa'
import bodyParser from 'koa-bodyparser'

const app = new Koa()

// 全局错误处理
app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    ctx.status = err.status || 500
    ctx.body = { error: err.message }
    ctx.app.emit('error', err, ctx)
  }
})

// 请求日志
app.use(async (ctx, next) => {
  const start = Date.now()
  await next()
  const ms = Date.now() - start
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})

// 响应
app.use(async ctx => {
  ctx.body = { message: 'Hello Koa' }
})

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000')
})

Context(上下文)

javascript
// ctx 请求信息
ctx.request          // Koa Request 对象
ctx.response         // Koa Response 对象
ctx.req              // 原生 Request
ctx.res              // 原生 Response
ctx.method           // HTTP 方法
ctx.url              // 请求 URL
ctx.path             // 路径
ctx.query            // 查询参数对象
ctx.querystring      // 查询字符串
ctx.params           // 路由参数
ctx.headers          // 请求头
ctx.ip               // 客户端 IP

// ctx 响应信息
ctx.body             // 响应体
ctx.status           // 状态码
ctx.message          // 状态消息
ctx.set(field, value) // 设置响应头
ctx.redirect(url)    // 重定向
ctx.throw(status, message)  // 抛出错误

洋葱模型

请求 →  M1 进入  →  M2 进入  →  M3 进入  →  M3 离开  →  M2 离开  →  M1 离开  →  响应
              ↓              ↓              ↓
           await          await          await
              ↑              ↑              ↑
           next()         next()         next()
javascript
// 中间件执行顺序示例
app.use(async (ctx, next) => {
  console.log('1. middleware A - before next')
  await next()
  console.log('2. middleware A - after next')
})

app.use(async (ctx, next) => {
  console.log('3. middleware B - before next')
  await next()
  console.log('4. middleware B - after next')
})

app.use(async ctx => {
  console.log('5. response')
  ctx.body = 'Hello'
})

// 输出顺序:
// 1. middleware A - before next
// 3. middleware B - before next
// 5. response
// 4. middleware B - after next
// 2. middleware A - after next

中间件

中间件分类

javascript
// 1. 应用级中间件(全局)
app.use(async (ctx, next) => {
  ctx.state.user = await getUser(ctx)
  await next()
})

// 2. 路由级中间件(特定路由)
router.get('/admin', requireAuth, async ctx => {
  ctx.body = 'Admin page'
})

// 3. 错误处理中间件
app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    ctx.status = err.status || 500
    ctx.body = err.expose ? err.message : 'Internal error'
  }
})

// 4. 第三方中间件
import bodyParser from 'koa-bodyparser'
import staticFiles from 'koa-static'
import cors from '@koa/cors'
import helmet from 'koa-helmet'

app.use(bodyParser())
app.use(staticFiles('public'))
app.use(cors())
app.use(helmet())

compose 原理

javascript
// compose 函数实现
function compose(middlewares) {
  return function(ctx, next) {
    let index = -1

    function dispatch(i) {
      if (i <= index) {
        return Promise.reject(new Error('next() called multiple times'))
      }
      index = i

      let fn = middlewares[i]
      if (i === middlewares.length) {
        fn = next
      }

      try {
        return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }

    return dispatch(0)
  }
}

路由

koa-router

javascript
import Router from '@koa/router'

const router = new Router({ prefix: '/api' })

// RESTful 路由
router.get('/users', async ctx => {
  ctx.body = await getUsers()
})

router.get('/users/:id', async ctx => {
  const user = await getUser(ctx.params.id)
  if (!user) {
    ctx.throw(404, 'User not found')
  }
  ctx.body = user
})

router.post('/users', async ctx => {
  const user = await createUser(ctx.request.body)
  ctx.status = 201
  ctx.body = user
})

router.put('/users/:id', async ctx => {
  const user = await updateUser(ctx.params.id, ctx.request.body)
  ctx.body = user
})

router.delete('/users/:id', async ctx => {
  await deleteUser(ctx.params.id)
  ctx.status = 204
})

// 路由嵌套
const postsRouter = new Router({ prefix: '/posts' })
postsRouter.get('/', async ctx => {
  ctx.body = await getPosts()
})
postsRouter.get('/:id', async ctx => {
  ctx.body = await getPost(ctx.params.id)
})

// 使用路由
app.use(postsRouter.routes())
app.use(postsRouter.allowedMethods())

路由分组与中间件

javascript
const api = new Router({ prefix: '/api' })

// 认证中间件
const auth = async (ctx, next) => {
  const token = ctx.headers.authorization?.split(' ')[1]
  if (!token) {
    ctx.throw(401, 'Unauthorized')
  }
  const user = await verifyToken(token)
  ctx.state.user = user
  await next()
}

// v1 路由组
const v1 = api.prefix('/v1').use(auth)

v1.get('/users', async ctx => {
  ctx.body = await getUsers(ctx.state.user)
})

v1.post('/orders', async ctx => {
  ctx.body = await createOrder(ctx.state.user, ctx.request.body)
})

// 错误处理中间件
const errorHandler = async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    ctx.status = err.status || 500
    ctx.body = {
      success: false,
      message: err.message
    }
  }
}

api.use(errorHandler)

请求处理

请求体解析

javascript
import koaBody from 'koa-body'

app.use(koaBody({
  multipart: true,
  formidable: {
    uploadDir: './uploads',
    keepExtensions: true,
    maxFileSize: 10 * 1024 * 1024  // 10MB
  }
}))

// 访问请求体
app.use(async ctx => {
  // JSON body
  ctx.request.body

  // Form body
  ctx.request.body.username

  // Files
  ctx.request.files.myFile
})

文件上传

javascript
import koaBody from 'koa-body'
import fs from 'fs'
import path from 'path'

app.use(koaBody({
  multipart: true,
  formidable: {
    uploadDir: path.join(process.cwd(), 'uploads'),
    keepExtensions: true
  }
}))

router.post('/upload', async ctx => {
  const file = ctx.request.files.file

  if (!file) {
    ctx.throw(400, 'No file uploaded')
  }

  ctx.body = {
    filename: file.newFilename,
    originalName: file.originalFilename,
    size: file.size,
    mimetype: file.mimetype,
    url: `/uploads/${file.newFilename}`
  }
})

文件下载

javascript
router.get('/download/:filename', async ctx => {
  const { filename } = ctx.params
  const filePath = path.join(process.cwd(), 'uploads', filename)

  if (!fs.existsSync(filePath)) {
    ctx.throw(404, 'File not found')
  }

  ctx.set('Content-Disposition', `attachment; filename="${filename}"`)
  ctx.set('Content-Type', 'application/octet-stream')

  ctx.body = fs.createReadStream(filePath)
})

错误处理

javascript
// 基础错误
ctx.throw(400, 'Invalid parameters')
ctx.throw(401)
ctx.throw(403, 'Forbidden')
ctx.throw(404, 'Not found')
ctx.throw(500, 'Server error')

// 自定义错误类
class AppError extends Error {
  constructor(message, status = 500) {
    super(message)
    this.status = status
    this.expose = true
  }
}

throw new AppError('Custom error', 400)

// 全局错误处理
app.on('error', (err, ctx) => {
  console.error('Server error:', err)
  // 上报错误到监控系统
})

// 中间件中的错误处理
app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    ctx.status = err.status || 500

    // 生产环境不暴露错误详情
    const message = process.env.NODE_ENV === 'production'
      ? 'Internal server error'
      : err.message

    ctx.body = { error: message }

    ctx.app.emit('error', err, ctx)
  }
})

项目结构

src/
├── app.js              # 应用入口
├── index.js            # 启动文件
├── router/             # 路由
│   ├── index.js
│   ├── user.js
│   └── post.js
├── controller/         # 控制器
│   ├── user.js
│   └── post.js
├── service/            # 业务逻辑
│   ├── user.js
│   └── post.js
├── middleware/         # 中间件
│   ├── auth.js
│   ├── error.js
│   └── logger.js
├── model/              # 数据模型
│   └── user.js
├── config/             # 配置
│   └── index.js
└── utils/              # 工具函数
    └── helper.js

完整应用示例

javascript
// src/app.js
import Koa from 'koa'
import bodyParser from 'koa-bodyparser'
import cors from '@koa/cors'
import router from './router/index.js'
import errorHandler from './middleware/error.js'

const app = new Koa()

// 错误处理
app.use(errorHandler)

// CORS
app.use(cors())

// Body 解析
app.use(bodyParser())

// 路由
app.use(router.routes())
app.use(router.allowedMethods())

export default app

// src/index.js
import app from './app.js'

const PORT = process.env.PORT || 3000

app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`)
})

// src/middleware/error.js
export default async function errorHandler(ctx, next) {
  try {
    await next()
  } catch (err) {
    ctx.status = err.status || 500
    ctx.body = {
      success: false,
      message: process.env.NODE_ENV === 'production'
        ? 'Internal server error'
        : err.message
    }
    ctx.app.emit('error', err, ctx)
  }
}

// src/controller/user.js
import UserService from '../service/user.js'

export default class UserController {
  static async getUsers(ctx) {
    const { page = 1, limit = 10 } = ctx.query
    ctx.body = await UserService.findAll({ page, limit })
  }

  static async getUser(ctx) {
    const user = await UserService.findById(ctx.params.id)
    if (!user) {
      ctx.throw(404, 'User not found')
    }
    ctx.body = user
  }

  static async createUser(ctx) {
    const user = await UserService.create(ctx.request.body)
    ctx.status = 201
    ctx.body = user
  }
}

面试高频题

Q1: Koa 的洋葱模型是什么?

答案: Koa 中间件的执行方式类似洋葱,外层中间件先进入,依次执行到内层,处理完后内层先返回,依次返回到外层。每个中间件调用 next() 等待下一个中间件执行。

Q2: Koa 和 Express 的区别?

特性KoaExpress
中间件洋葱模型线性模型
异步async/await回调
路由需安装 koa-router内置
错误处理try/catch回调最后一个参数
体积更轻量功能更全

Q3: 如何处理 Koa 中的错误?

答案:

  • 使用 ctx.throw(status, message) 抛出错误
  • 在最外层中间件用 try/catch 捕获
  • 监听 app.on('error') 事件
  • 设置 err.expose 控制是否暴露错误信息

Q4: Koa 中间件的执行顺序?

答案: 先注册先执行,await next() 前是进入阶段,await next() 后是返回阶段。后注册的中间件先返回。

前端面试知识库