{T}

Koa 中间件机制

中间件核心概念

Koa 的核心设计哲学是轻量、灵活,它将大部分功能都委托给中间件来完成。这种设计使得开发者可以根据项目需求,自由组合和扩展功能,构建出高效、可维护的 Node.js 应用。

洋葱模型

Koa 中间件采用独特的"洋葱模型",请求从外到内依次穿过各中间件,响应则从内到外反向穿出:

code
请求 ────────────────────────────────────▶
  │                                           │
  │   ┌─────────────────────────────────┐    │
  │   │        中间件 1                  │    │
  │   │  ┌───────────────────────────┐  │    │
  │   │  │      中间件 2              │  │    │
  │   │  │  ┌─────────────────────┐  │  │    │
  │   │  │  │    中间件 3          │  │  │    │
  │   │  │  │                     │  │  │    │
  │   │  │  │    await next()     │  │  │    │
  │   │  │  │                     │  │  │    │
  │   │  │  └─────────────────────┘  │  │    │
  │   │  │         响应               │  │    │
  │   │  └───────────────────────────┘  │    │
  │   └─────────────────────────────────┘    │  │
◀─────────────────────────────────────────────
                        响应

中间件执行流程

javascript
const Koa = require("koa")
const app = new Koa()

// 中间件 1
app.use(async (ctx, next) => {
  console.log("1. 中间件 1 - 请求开始")
  const start = Date.now()
  
  await next()  // 等待下游中间件执行完成
  
  const ms = Date.now() - start
  console.log(`4. 中间件 1 - 响应完成 (${ms}ms)`)
})

// 中间件 2
app.use(async (ctx, next) => {
  console.log("2. 中间件 2 - 请求开始")
  await next()
  console.log("3. 中间件 2 - 响应处理")
})

// 中间件 3
app.use(async (ctx) => {
  console.log("3. 中间件 3 - 处理请求")
  ctx.body = "Hello Koa"
})

// 执行顺序:
// 1. 中间件 1 - 请求开始
// 2. 中间件 2 - 请求开始
// 3. 中间件 3 - 处理请求
// 3. 中间件 2 - 响应处理
// 4. 中间件 1 - 响应完成

中间件分类

类型描述示例
应用级中间件应用到所有路由日志、错误处理
路由级中间件应用到特定路由认证、权限检查
错误处理中间件捕获和处理错误全局错误处理
第三方中间件社区提供的中间件@koa/router, koa-body

中间件注册顺序

中间件的注册顺序非常重要,通常遵循以下原则:

javascript
const Koa = require("koa")
const app = new Koa()

// 1. 错误处理中间件(最外层)
app.use(errorHandler)

// 2. 请求预处理中间件
app.use(requestLogger)
app.use(requestId)

// 3. 安全相关中间件
app.use(helmet())
app.use(cors())

// 4. Body 解析中间件
app.use(bodyParser())

// 5. Session/Cookie 中间件
app.use(session())

// 6. 路由中间件
app.use(router.routes())
app.use(router.allowedMethods())

// 7. 404 处理中间件(最内层)
app.use(notFoundHandler)

常用第三方中间件

路由管理:@koa/router

@koa/router 是 Koa 官方维护的路由中间件,提供强大而富有表现力的 API,轻松组织应用的路由逻辑,构建结构清晰的 RESTful API。

核心特性

  • HTTP 方法路由:支持所有标准的 HTTP 方法(GET、POST、PUT、DELETE 等)
  • 动态路由:通过命名参数捕获 URL 中的动态片段
  • 路由中间件:可以在单个路由上应用一个或多个中间件
  • 路由前缀:为一组路由添加统一的路径前缀
  • 嵌套路由:实现模块化路由管理
  • URL 生成:根据路由名称和参数动态生成 URL

安装

bash
# 使用 npm
npm install @koa/router

# 使用 pnpm
pnpm add @koa/router

基本使用

javascript
const Koa = require("koa")
const Router = require("@koa/router")

const app = new Koa()
const router = new Router()

// 定义路由
router.get("/", (ctx) => {
  ctx.body = "Hello World"
})

router.get("/about", (ctx) => {
  ctx.body = "About Page"
})

// 注册路由中间件
app.use(router.routes())
app.use(router.allowedMethods())

app.listen(3000, () => {
  console.log("Server is running at http://localhost:3000")
})

核心 API 示例

@koa/router 提供了多种方法来定义路由,以下是一些最常用的功能:

最佳实践

  1. 模块化路由:将不同功能的路由(如用户管理、产品管理)拆分到不同的文件中,再通过主文件统一导入和注册,保持代码的整洁和可维护性
  2. 使用路由前缀:为 API 添加版本号前缀(如 /api/v1),这为未来的 API 升级和兼容性维护提供了极大的灵活性
  3. 中间件组合:将通用的中间件(如认证、日志、限流)提取出来,在需要的地方按需应用,避免代码重复
  4. 统一错误处理:利用 Koa 的中间件模型,在所有路由之前设置一个全局的错误处理中间件,捕获并格式化所有路由中抛出的错误
  5. 参数验证:始终验证来自客户端的路由参数(ctx.params)和请求体(ctx.request.body),防止无效数据或恶意输入

koa-bodyparser:请求体解析

由于 Koa 自身没有解析 post 请求参数的功能,因此需要安装 Koa 中间件 koa-bodyparser

bash
npm install koa-bodyparser

基本用法:

javascript
const Koa = require("koa")
const app = new Koa()
const Router = require("koa-router")
const bodyParser = require("koa-bodyparser")
const router = new Router()

app.use(bodyParser())
router.post("/api/get/userInfo", async (ctx) => {
  let { name } = ctx.request.body
  ctx.body = `请求参数为 ${name}`
})

// 加载路由中间件
app.use(router.routes())

app.listen(4000, () => {
  console.log("server is running, port is 4000")
})

使用 koa-bodyparser 中间件后,post 请求的参数会被自动解析成 JSON 格式,这在实际项目中是非常实用的,如果用的是开源的 BFF 框架,那么该功能应该被集成到框架中

底层原理:如果不使用中间件,需要手动监听 ctx.req(Node.js 原生请求对象)的 dataend 事件来接收数据流,这非常繁琐且容易出错。koa-bodyparser 等中间件处理这一切

koa-body:请求体解析

koa-body 是功能强大的 Koa 中间件,用于解析 HTTP 请求体。它无缝集成了 koa-body-parserkoa-multer 的功能,支持 JSONform-urlencoded 以及 multipart/form-data 等多种格式的请求体, 是处理文件上传和复杂表单的不二之选

  • 多格式解析:支持 JSON、URL-encoded 表单和 multipart/form-data
  • 文件上传:内置强大的文件上传处理能力,基于 formidable
  • 精细化配置:允许对不同类型的请求体设置不同的大小限制
  • 严格模式:可配置为仅解析与 Content-Type 头匹配的请求
  • 自定义错误处理:提供 onError 钩子,用于捕获和处理解析过程中的错误

版本与兼容性:

中间件最新版本Koa 兼容性Node.js 版本要求
koa-bodyv6.0.1v2.x>= 14.x

重要提示:从 v5.x 升级到 v6.x 后,koa-body 将底层的 formidable 从 v1 升级到了 v2。这是一个重大变更,可能会影响文件上传的处理方式。请务必查阅官方文档以了解详细的迁移指南。

bash
# 使用 npm
npm install koa-body

# 使用 pnpm
pnpm add koa-body

基本用法

koa-body 的使用非常直观。只需在你的 Koa 应用中注册它,即可通过 ctx.request.body 访问解析后的请求体数据,通过 ctx.request.files 访问上传的文件

javascript
const Koa = require("koa")
const Router = require("@koa/router")
const koaBody = require("koa-body")

const app = new Koa()
const router = new Router()

// 1. 注册 koa-body 中间件
// 必须在路由中间件之前注册
app.use(
  koaBody({
    multipart: true, // 启用 multipart/form-data 解析,用于文件上传
    formidable: {
      uploadDir: __dirname + "/uploads", // 设置文件上传目录
      keepExtensions: true // 保留文件扩展名
    }
  })
)

// 2. 定义处理 POST 请求的路由
router.post("/users", (ctx) => {
  // ctx.request.body 中包含了 JSON 或表单数据
  console.log("Request Body:", ctx.request.body)
  ctx.body = { message: "Data received", data: ctx.request.body }
})

// 3. 定义处理文件上传的路由
router.post("/upload", (ctx) => {
  // ctx.request.files 中包含了上传的文件信息
  console.log("Uploaded Files:", ctx.request.files)
  ctx.body = { message: "File uploaded successfully", files: ctx.request.files }
})

// 4. 注册路由
app.use(router.routes())
app.use(router.allowedMethods())

// 5. 启动服务器
app.listen(3000, () => {
  console.log("✅ Server is running at http://localhost:3000")
})

核心 API 与示例

最佳实践

文件上传安全

  • 限制文件类型:通过检查文件的 mimetype,只允许白名单中的文件类型上传
  • 限制文件大小:在 formidable 配置中设置 maxFileSize,防止超大文件耗尽服务器资源
  • 重命名上传文件:不要直接使用用户提供的原始文件名 生成一个唯一的、安全的文件名(如使用 UUID),以防止目录遍历攻击和文件名冲突
  • 使用临时目录:将文件先上传到操作系统的临时目录,验证通过后再移动到最终的存储位置

错误处理

  • 使用 onError:配置 onError 钩子来捕获解析阶段的错误(如请求体过大),并返回统一、友好的错误信息
  • 结合 try/catch:在路由处理器中使用 try/catch 来处理业务逻辑中的错误(如文件类型不匹配),确保所有异常都被妥善管理

koa-static 静态资源服务

koa-static 是 Koa 官方提供的中间件,专门用于高效地提供静态资源服务。无论是网站的图片、CSS、JavaScript 文件,还是单页应用(SPA)的入口文件,都可以通过它轻松地暴露给客户端访问。

  • 高效文件传输:利用 send 模块,支持缓存头(ETag, Last-Modified),减少不必要的网络传输
  • 可配置选项:提供丰富的选项,如设置缓存时间、默认文件、隐藏文件处理等
  • 多目录服务:可以多次使用 koa-static 来服务多个不同的静态资源目录
  • 索引文件支持:自动服务目录下的 index.html
bash
# 使用 npm
npm install koa-static

# 使用 pnpm
pnpm add koa-static

基本用法

假设项目根目录 public 文件夹,存放着所有静态资源

text
/
|-- public/
|   |-- index.html
|   |-- styles.css
|   |-- main.js
|-- app.js

只需一行代码,即可让 public 目录下的所有文件通过 HTTP 访问:

javascript
const Koa = require("koa")
const serve = require("koa-static")
const path = require("path")

const app = new Koa()

// 1. 设置静态资源目录
const staticDir = path.join(__dirname, "public")
app.use(serve(staticDir))

// 2. 启动服务器
app.listen(3000, () => {
  console.log("✅ Server is running at http://localhost:3000")
  console.log("Static files are served from:", staticDir)
})

// 现在你可以通过以下 URL 访问静态文件:
// - http://localhost:3000/index.html
// - http://localhost:3000/styles.css
// - http://localhost:3000/main.js

高级用法

添加配置

js
app.use(
  serve(path.join(__dirname, "public"), {
    // 浏览器缓存最大时长(毫秒)
    maxage: 30 * 24 * 60 * 60 * 1000, // 缓存 30 天

    // 是否传输隐藏文件(以 `.` 开头的文件)
    hidden: false,

    // 默认文件名,当请求一个目录时返回
    index: "index.html",

    // 是否在文件传输后推迟到下一个中间件
    defer: false,

    // 是否启用 Gzip 压缩(需要 `koa-compress` 配合)
    gzip: true,

    // 是否为响应头添加 `br` 压缩标识(需要 `koa-compress` 配合
    brotli: true,

    // 自定义响应头的函数
    setHeaders: (res, path, stats) => {
      // 为所有 CSS 文件添加自定义头
      if (path.endsWith(".css")) {
        res.setHeader("X-Custom-CSS-Header", "true")
      }
    }
  })
)

服务多个目录

js
// 可以多次调用 `app.use(serve(...))` 来服务多个目录,请求会按照注册顺序依次在目录中查找文件

const assetsDir = path.join(__dirname, "assets")
const publicDir = path.join(__dirname, "public")

// 先在 `assets` 目录查找
app.use(serve(assetsDir))
// 如果找不到,再到 `public` 目录查找
app.use(serve(publicDir))

koa-views 模板引擎

Koa 本身不包含模板引擎,但通过 koa-views 中间件,可以方便地集成各种流行的模板引擎,如 EJS、Pug、Nunjucks 等

bash
pnpm install koa-views ejs pug

@koa/cors:跨域资源共享

@koa/cors 中间件通过设置一系列 HTTP 响应头,使得服务器能够安全地允许跨域请求:

  • 简单易用:只需一行代码即可启用基本的 CORS 功能
  • 高度可配:支持配置允许的源、HTTP 方法、请求头以及是否携带凭证
  • 动态配置:可以根据请求的上下文动态决定 CORS 策略
bash
# 使用 npm
npm install @koa/cors

# 使用 pnpm
pnpm add @koa/cors

基本使用

最简单的用法是允许所有来源的跨域请求:

javascript
const Koa = require("koa")
const cors = require("@koa/cors")

const app = new Koa()

// 在所有路由之前使用 cors 中间件
app.use(cors())

app.use((ctx) => {
  ctx.body = "Hello World with CORS!"
})

app.listen(3000, () => {
  console.log("✅ Server with CORS is running at http://localhost:3000")
})

高级配置

核心选项:

  • origin: 配置 Access-Control-Allow-Origin
    • String: 设置为特定的源,如 'http://example.com'
    • Function: 一个函数 (ctx) => string | false,根据请求上下文动态返回允许的源
    • *: 允许所有源(默认值),但在需要凭证时无效
  • credentials: 配置 Access-Control-Allow-Credentials。布尔值,默认为 false。如果设置为 trueorigin 不能为 *,必须是具体的源
  • allowMethods: 配置 Access-Control-Allow-Methods。字符串或数组,如 ['GET', 'POST']
  • allowHeaders: 配置 Access-Control-Allow-Headers。字符串或数组,指定允许的自定义请求头
  • exposeHeaders: 配置 Access-Control-Expose-Headers。字符串或数组,让浏览器能够访问响应中的指定头部
  • maxAge: 配置 Access-Control-Max-Age。数字(秒),设置预检请求结果的缓存时间

在生产环境中,通常需要更精细的控制,例如只允许特定的源进行访问:

javascript
const Koa = require("koa")
const cors = require("@koa/cors")
const Router = require("@koa/router")

const app = new Koa()
const router = new Router()

// 白名单,只允许这些源进行跨域访问
const whitelist = ["http://localhost:8080", "https://my-frontend.com"]

const corsOptions = {
  origin: (ctx) => {
    const origin = ctx.get("Origin")
    if (whitelist.includes(origin)) {
      return origin
    }
    // 如果源不在白名单中,则不允许跨域
    // 你也可以返回一个默认值,或者不返回任何内容来拒绝请求
    return false
  },
  // 允许携带凭证(如 Cookie)
  credentials: true,
  // 允许的 HTTP 方法
  allowMethods: ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
  // 允许的请求头
  allowHeaders: ["Content-Type", "Authorization", "Accept"],
  // 预检请求(OPTIONS)的缓存时间(秒)
  maxAge: 5 * 60 // 5 分钟
}

app.use(cors(corsOptions))

router.get("/data", (ctx) => {
  // 如果需要携带 cookie,前端请求需要设置 withCredentials: true
  ctx.cookies.set("my-cookie", "hello from server", { httpOnly: false })
  ctx.body = { message: "This is sensitive data from a CORS-enabled server." }
})

app.use(router.routes()).use(router.allowedMethods())

app.listen(3000, () => {
  console.log("✅ Advanced CORS server is running at http://localhost:3000")
})

koa-mount:中间件挂载

koa-mount 用于将 Koa 中间件或整个 Koa 应用挂载到特定 URL 前缀下的工具。它在构建模块化应用、组合多个独立服务或为特定路径应用专用逻辑时非常有用

  • 路径前缀挂载:将一个中间件或 Koa 应用限制在指定的 URL 前缀下运行
  • 应用组合:可以将多个独立的 Koa 应用组合成一个单一的、更大型的应用
  • 模块化:有助于将大型应用拆分为多个职责单一的小型应用,提高代码的可维护性
bash
# 使用 npm
npm install koa-mount

# 使用 pnpm
pnpm add koa-mount

基本用法

假设有个提供静态文件的应用和一个提供 API 的应用,可以使用 koa-mount 将它们组合起来:

javascript
const Koa = require("koa")
const mount = require("koa-mount")
const serve = require("koa-static")
const path = require("path")

const app = new Koa()

// 1. 创建一个专门提供静态文件的 Koa 实例
const staticApp = new Koa()
staticApp.use(serve(path.join(__dirname, "public")))

// 2. 创建一个提供 API 的 Koa 实例
const apiApp = new Koa()
apiApp.use(async (ctx, next) => {
  ctx.body = "This is the API response."
  await next()
})

// 3. 使用 koa-mount 将它们挂载到主应用上,访问 /static/* 的请求将由 staticApp 处理
app.use(mount("/static", staticApp))

// 访问 /api/* 的请求将由 apiApp 处理
app.use(mount("/api", apiApp))

// 4. 启动主应用
app.listen(3000, () => {
  console.log("✅ Server is running at http://localhost:3000")
  console.log("Access static files at http://localhost:3000/static/")
  console.log("Access API at http://localhost:3000/api/")
})

核心 API 与示例

最佳实践

  1. 微服务架构:将不同业务领域实现为独立的 Koa 应用,使用 koa-mount 组合成统一网关
  2. 版本化 API:通过挂载不同应用实例实现 API 版本控制
    javascript
    app.use(mount('/v1', apiV1))
    app.use(mount('/v2', apiV2))
  3. 隔离中间件:精确地将中间件应用到需要的路径,避免全局污染

其他常用中间件

koa-logger:请求日志

bash
npm install koa-logger
javascript
const logger = require("koa-logger")

app.use(logger())

// 自定义日志格式
app.use(logger((str, args) => {
  console.log(`${new Date().toISOString()} ${str}`)
}))

koa-compress:响应压缩

bash
npm install koa-compress
javascript
const compress = require("koa-compress")

app.use(compress({
  filter: (contentType) => {
    return /text|json|javascript|css/i.test(contentType)
  },
  threshold: 1024,  // 超过 1KB 才压缩
  gzip: {
    flush: require("zlib").constants.Z_SYNC_FLUSH
  }
}))

koa-helmet:安全头设置

bash
npm install koa-helmet
javascript
const helmet = require("koa-helmet")

app.use(helmet())

// 自定义配置
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"]
    }
  }
}))

koa-jwt:JWT 认证

bash
npm install koa-jwt
javascript
const jwt = require("koa-jwt")

// 公开路由
app.use(publicRoutes.routes())

// JWT 保护的路由
app.use(jwt({ secret: "your-secret-key" }))
app.use(protectedRoutes.routes())

koa-ratelimit:请求限流

bash
npm install koa-ratelimit
javascript
const rateLimit = require("koa-ratelimit")

app.use(rateLimit({
  db: new Map(),  // 生产环境使用 Redis
  duration: 60000,  // 60 秒
  max: 100,  // 最大请求数
  id: (ctx) => ctx.ip,
  errorMessage: "Too many requests",
  disableHeader: false
}))

koa-session:Session 管理

bash
npm install koa-session
javascript
const session = require("koa-session")

app.keys = ["secret-key"]

app.use(session({
  key: "koa.sess",
  maxAge: 86400000,
  autoCommit: true,
  overwrite: true,
  httpOnly: true,
  signed: true,
  rolling: false,
  renew: false
}, app))

中间件速查表

中间件功能使用场景
@koa/router路由管理所有项目
koa-body请求体解析处理 POST 请求、文件上传
koa-bodyparser请求体解析处理 JSON、表单
koa-static静态文件服务前端资源托管
koa-views模板引擎服务端渲染
@koa/corsCORS 跨域API 服务
koa-mount中间件挂载模块化应用
koa-logger请求日志开发调试
koa-compress响应压缩性能优化
koa-helmet安全头生产环境
koa-jwtJWT 认证API 认证
koa-ratelimit请求限流防止滥用
koa-sessionSession 管理传统登录

自定义中间件开发

基本结构

javascript
// 基本中间件结构
async function myMiddleware(ctx, next) {
  // 前置处理(请求阶段)
  console.log("请求开始")
  
  // 等待下游中间件执行
  await next()
  
  // 后置处理(响应阶段)
  console.log("请求结束")
}

// 使用
app.use(myMiddleware)

带配置的中间件

javascript
// 创建可配置的中间件
function myMiddleware(options = {}) {
  const { 
    prefix = "[MyMiddleware]",
    logResponse = true 
  } = options
  
  return async (ctx, next) => {
    console.log(`${prefix} 请求: ${ctx.method} ${ctx.url}`)
    
    await next()
    
    if (logResponse) {
      console.log(`${prefix} 响应: ${ctx.status}`)
    }
  }
}

// 使用
app.use(myMiddleware({ 
  prefix: "[API]",
  logResponse: false 
}))

实用自定义中间件示例

请求计时中间件

javascript
function responseTime() {
  return async (ctx, next) => {
    const start = Date.now()
    
    await next()
    
    const ms = Date.now() - start
    ctx.set("X-Response-Time", `${ms}ms`)
  }
}

app.use(responseTime())

请求 ID 中间件

javascript
const crypto = require("crypto")

function requestId() {
  return async (ctx, next) => {
    const id = crypto.randomBytes(16).toString("hex")
    ctx.state.requestId = id
    ctx.set("X-Request-Id", id)
    
    await next()
  }
}

app.use(requestId())

认证中间件

javascript
function authenticate(options = {}) {
  const { 
    tokenHeader = "authorization",
    prefix = "Bearer" 
  } = options
  
  return async (ctx, next) => {
    const authHeader = ctx.get(tokenHeader)
    
    if (!authHeader) {
      ctx.throw(401, "No token provided")
    }
    
    const token = authHeader.replace(`${prefix} `, "")
    
    try {
      const user = await verifyToken(token)
      ctx.state.user = user
      await next()
    } catch (err) {
      ctx.throw(401, "Invalid token")
    }
  }
}

// 使用
router.get("/protected", authenticate(), (ctx) => {
  ctx.body = { user: ctx.state.user }
})

权限检查中间件

javascript
function requireRole(role) {
  return async (ctx, next) => {
    const user = ctx.state.user
    
    if (!user || user.role !== role) {
      ctx.throw(403, "Forbidden")
    }
    
    await next()
  }
}

// 使用
router.delete("/users/:id", 
  authenticate(), 
  requireRole("admin"), 
  (ctx) => {
    // 只有管理员可以访问
  }
)

缓存中间件

javascript
function cache(duration = 3600) {
  return async (ctx, next) => {
    await next()
    
    if (ctx.status === 200) {
      ctx.set("Cache-Control", `public, max-age=${duration}`)
    }
  }
}

// 使用
router.get("/articles", cache(600), async (ctx) => {
  ctx.body = await Article.findAll()
})

请求验证中间件

javascript
function validate(schema) {
  return async (ctx, next) => {
    const data = ctx.method === "GET" ? ctx.query : ctx.request.body
    
    try {
      const validated = await schema.validateAsync(data)
      
      // 将验证后的数据存储到 state
      if (ctx.method === "GET") {
        ctx.query = validated
      } else {
        ctx.request.body = validated
      }
      
      await next()
    } catch (err) {
      ctx.throw(400, err.message)
    }
  }
}

// 使用
const Joi = require("joi")

const userSchema = Joi.object({
  name: Joi.string().required(),
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(0)
})

router.post("/users", validate(userSchema), async (ctx) => {
  const user = await User.create(ctx.request.body)
  ctx.body = user
})

错误处理中间件

javascript
function errorHandler() {
  return async (ctx, next) => {
    try {
      await next()
      
      // 404 处理
      if (ctx.status === 404 && !ctx.body) {
        ctx.status = 404
        ctx.body = {
          success: false,
          message: "Not Found"
        }
      }
    } catch (err) {
      // 记录错误
      console.error("Error:", err)
      
      // 设置状态码
      ctx.status = err.status || err.statusCode || 500
      
      // 设置响应体
      ctx.body = {
        success: false,
        message: err.message,
        code: err.code || "ERROR",
        ...(process.env.NODE_ENV === "development" && {
          stack: err.stack
        })
      }
      
      // 触发错误事件
      ctx.app.emit("error", err, ctx)
    }
  }
}

// 使用(必须放在最前面)
app.use(errorHandler())

中间件最佳实践

1. 中间件顺序

遵循正确的中间件注册顺序:

javascript
const Koa = require("koa")
const app = new Koa()

// 1. 错误处理(最外层)
app.use(errorHandler())

// 2. 日志记录
app.use(logger())

// 3. 安全相关
app.use(helmet())
app.use(cors())

// 4. 请求体解析
app.use(koaBody())

// 5. Session/认证
app.use(session(app))
app.use(passport.initialize())

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

// 7. 404 处理(最内层)
app.use(notFound())

2. 错误处理

统一错误处理机制:

javascript
// 自定义错误类
class AppError extends Error {
  constructor(message, code, status = 400) {
    super(message)
    this.code = code
    this.status = status
  }
}

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

// 错误事件监听
app.on("error", (err, ctx) => {
  console.error("Server Error:", err)
  
  // 发送错误通知
  if (process.env.NODE_ENV === "production") {
    // Sentry.captureException(err)
  }
})

3. 性能优化

javascript
// 响应压缩
app.use(compress({
  threshold: 1024
}))

// 缓存控制
app.use(async (ctx, next) => {
  await next()
  
  if (ctx.fresh) {
    ctx.status = 304
    return
  }
})

// 静态资源缓存
app.use(serve("./public", {
  maxage: 365 * 24 * 60 * 60 * 1000
}))

4. 中间件复用

javascript
// 中间件工厂函数
function createAuthMiddleware(options) {
  const { role, permission } = options
  
  return async (ctx, next) => {
    const user = ctx.state.user
    
    if (role && user.role !== role) {
      ctx.throw(403, "Forbidden")
    }
    
    if (permission && !user.permissions.includes(permission)) {
      ctx.throw(403, "No permission")
    }
    
    await next()
  }
}

// 复用中间件
const adminOnly = createAuthMiddleware({ role: "admin" })
const canDelete = createAuthMiddleware({ permission: "delete" })

router.delete("/users/:id", authenticate(), canDelete, deleteUser)
router.put("/settings", authenticate(), adminOnly, updateSettings)

5. 异步处理

javascript
// 正确处理异步错误
app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    // 捕获异步错误
    ctx.status = err.status || 500
    ctx.body = { error: err.message }
  }
})

// 确保所有中间件都使用 async/await
app.use(async (ctx, next) => {
  // 正确
  await someAsyncOperation()
  await next()
  
  // 错误 - 可能导致错误未被捕获
  // someAsyncOperation()
  // next()
})

常见问题

Q1: 中间件执行顺序为什么很重要?

中间件按照注册顺序执行,形成洋葱模型。错误的顺序可能导致:

javascript
// ❌ 错误:路由在错误处理之前
app.use(router.routes())
app.use(errorHandler())  // 永远不会被触发

// ✅ 正确:错误处理在最外层
app.use(errorHandler())
app.use(router.routes())

Q2: 如何调试中间件执行流程?

javascript
// 添加调试日志
app.use(async (ctx, next) => {
  console.log(`>>> ${ctx.method} ${ctx.url}`)
  await next()
  console.log(`<<< ${ctx.status}`)
})

// 或使用 koa-logger
const logger = require("koa-logger")
app.use(logger())

Q3: 中间件中如何共享数据?

javascript
// 使用 ctx.state
app.use(async (ctx, next) => {
  ctx.state.user = await getUser()
  ctx.state.startTime = Date.now()
  await next()
})

app.use(async (ctx) => {
  console.log(ctx.state.user)      // 可访问
  console.log(ctx.state.startTime) // 可访问
})

Q4: 如何在中间件中提前结束请求?

javascript
// 认证失败时提前返回
app.use(async (ctx, next) => {
  if (!ctx.headers.authorization) {
    ctx.status = 401
    ctx.body = { error: "Unauthorized" }
    return  // 不调用 next(),提前结束
  }
  
  await next()
})

Q5: 如何处理中间件中的异步错误?

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

// 方式 2:中间件内部捕获
app.use(async (ctx, next) => {
  try {
    await someAsyncOperation()
  } catch (err) {
    ctx.throw(500, err.message)
  }
  
  await next()
})

Q6: koa-body 和 koa-bodyparser 有什么区别?

对比项koa-bodykoa-bodyparser
文件上传✅ 支持❌ 不支持
JSON
表单
配置复杂度中等简单
适用场景需要文件上传仅文本数据
javascript
// 需要文件上传:使用 koa-body
app.use(koaBody({ multipart: true }))

// 仅处理 JSON/表单:使用 koa-bodyparser
app.use(bodyParser())

Q7: 如何实现中间件的单元测试?

javascript
const request = require("supertest")
const Koa = require("koa")

describe("Auth Middleware", () => {
  const app = new Koa()
  const auth = require("./auth-middleware")
  
  app.use(auth())
  app.use((ctx) => {
    ctx.body = "Protected"
  })
  
  it("should reject without token", async () => {
    await request(app.callback())
      .get("/")
      .expect(401)
  })
  
  it("should allow with valid token", async () => {
    await request(app.callback())
      .get("/")
      .set("Authorization", "Bearer valid-token")
      .expect(200)
  })
})

总结

Koa 中间件机制的核心要点:

  1. 洋葱模型:请求和响应双向处理,中间件形成洋葱结构
  2. async/await:全面支持异步流程控制
  3. 轻量灵活:核心极简,功能由中间件组合实现
  4. 模块化设计:按需组合中间件,构建复杂应用
  5. 丰富的生态:社区提供大量高质量中间件

下一步学习

推荐资源

官方资源

常用中间件仓库


最后更新:2026年2月