Koa 中间件机制
中间件核心概念
Koa 的核心设计哲学是轻量、灵活,它将大部分功能都委托给中间件来完成。这种设计使得开发者可以根据项目需求,自由组合和扩展功能,构建出高效、可维护的 Node.js 应用。
洋葱模型
Koa 中间件采用独特的"洋葱模型",请求从外到内依次穿过各中间件,响应则从内到外反向穿出:
请求 ────────────────────────────────────▶
│ │
│ ┌─────────────────────────────────┐ │
│ │ 中间件 1 │ │
│ │ ┌───────────────────────────┐ │ │
│ │ │ 中间件 2 │ │ │
│ │ │ ┌─────────────────────┐ │ │ │
│ │ │ │ 中间件 3 │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ await next() │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ └─────────────────────┘ │ │ │
│ │ │ 响应 │ │ │
│ │ └───────────────────────────┘ │ │
│ └─────────────────────────────────┘ │ │
◀─────────────────────────────────────────────
响应中间件执行流程
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 |
中间件注册顺序
中间件的注册顺序非常重要,通常遵循以下原则:
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
安装
# 使用 npm
npm install @koa/router
# 使用 pnpm
pnpm add @koa/router基本使用
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 提供了多种方法来定义路由,以下是一些最常用的功能:
最佳实践
- 模块化路由:将不同功能的路由(如用户管理、产品管理)拆分到不同的文件中,再通过主文件统一导入和注册,保持代码的整洁和可维护性
- 使用路由前缀:为 API 添加版本号前缀(如
/api/v1),这为未来的 API 升级和兼容性维护提供了极大的灵活性 - 中间件组合:将通用的中间件(如认证、日志、限流)提取出来,在需要的地方按需应用,避免代码重复
- 统一错误处理:利用 Koa 的中间件模型,在所有路由之前设置一个全局的错误处理中间件,捕获并格式化所有路由中抛出的错误
- 参数验证:始终验证来自客户端的路由参数(
ctx.params)和请求体(ctx.request.body),防止无效数据或恶意输入
koa-bodyparser:请求体解析
由于 Koa 自身没有解析 post 请求参数的功能,因此需要安装 Koa 中间件 koa-bodyparser
npm install koa-bodyparser基本用法:
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 原生请求对象)的data和end事件来接收数据流,这非常繁琐且容易出错。koa-bodyparser等中间件处理这一切
koa-body:请求体解析
koa-body 是功能强大的 Koa 中间件,用于解析 HTTP 请求体。它无缝集成了 koa-body-parser、koa-multer 的功能,支持 JSON、form-urlencoded 以及 multipart/form-data 等多种格式的请求体,
是处理文件上传和复杂表单的不二之选
- 多格式解析:支持 JSON、URL-encoded 表单和 multipart/form-data
- 文件上传:内置强大的文件上传处理能力,基于
formidable - 精细化配置:允许对不同类型的请求体设置不同的大小限制
- 严格模式:可配置为仅解析与
Content-Type头匹配的请求 - 自定义错误处理:提供
onError钩子,用于捕获和处理解析过程中的错误
版本与兼容性:
| 中间件 | 最新版本 | Koa 兼容性 | Node.js 版本要求 |
|---|---|---|---|
koa-body | v6.0.1 | v2.x | >= 14.x |
重要提示:从
v5.x升级到v6.x后,koa-body将底层的formidable从 v1 升级到了 v2。这是一个重大变更,可能会影响文件上传的处理方式。请务必查阅官方文档以了解详细的迁移指南。
# 使用 npm
npm install koa-body
# 使用 pnpm
pnpm add koa-body基本用法
koa-body 的使用非常直观。只需在你的 Koa 应用中注册它,即可通过 ctx.request.body 访问解析后的请求体数据,通过 ctx.request.files 访问上传的文件
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
# 使用 npm
npm install koa-static
# 使用 pnpm
pnpm add koa-static基本用法
假设项目根目录 public 文件夹,存放着所有静态资源
/
|-- public/
| |-- index.html
| |-- styles.css
| |-- main.js
|-- app.js只需一行代码,即可让 public 目录下的所有文件通过 HTTP 访问:
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高级用法
添加配置
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")
}
}
})
)服务多个目录
// 可以多次调用 `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 等
pnpm install koa-views ejs pug@koa/cors:跨域资源共享
@koa/cors 中间件通过设置一系列 HTTP 响应头,使得服务器能够安全地允许跨域请求:
- 简单易用:只需一行代码即可启用基本的 CORS 功能
- 高度可配:支持配置允许的源、HTTP 方法、请求头以及是否携带凭证
- 动态配置:可以根据请求的上下文动态决定 CORS 策略
# 使用 npm
npm install @koa/cors
# 使用 pnpm
pnpm add @koa/cors基本使用
最简单的用法是允许所有来源的跨域请求:
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。如果设置为true,origin不能为*,必须是具体的源allowMethods: 配置Access-Control-Allow-Methods。字符串或数组,如['GET', 'POST']allowHeaders: 配置Access-Control-Allow-Headers。字符串或数组,指定允许的自定义请求头exposeHeaders: 配置Access-Control-Expose-Headers。字符串或数组,让浏览器能够访问响应中的指定头部maxAge: 配置Access-Control-Max-Age。数字(秒),设置预检请求结果的缓存时间
在生产环境中,通常需要更精细的控制,例如只允许特定的源进行访问:
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 应用组合成一个单一的、更大型的应用
- 模块化:有助于将大型应用拆分为多个职责单一的小型应用,提高代码的可维护性
# 使用 npm
npm install koa-mount
# 使用 pnpm
pnpm add koa-mount基本用法
假设有个提供静态文件的应用和一个提供 API 的应用,可以使用 koa-mount 将它们组合起来:
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 与示例
最佳实践
- 微服务架构:将不同业务领域实现为独立的 Koa 应用,使用
koa-mount组合成统一网关 - 版本化 API:通过挂载不同应用实例实现 API 版本控制
javascript
app.use(mount('/v1', apiV1)) app.use(mount('/v2', apiV2)) - 隔离中间件:精确地将中间件应用到需要的路径,避免全局污染
其他常用中间件
koa-logger:请求日志
npm install koa-loggerconst logger = require("koa-logger")
app.use(logger())
// 自定义日志格式
app.use(logger((str, args) => {
console.log(`${new Date().toISOString()} ${str}`)
}))koa-compress:响应压缩
npm install koa-compressconst 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:安全头设置
npm install koa-helmetconst helmet = require("koa-helmet")
app.use(helmet())
// 自定义配置
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"]
}
}
}))koa-jwt:JWT 认证
npm install koa-jwtconst jwt = require("koa-jwt")
// 公开路由
app.use(publicRoutes.routes())
// JWT 保护的路由
app.use(jwt({ secret: "your-secret-key" }))
app.use(protectedRoutes.routes())koa-ratelimit:请求限流
npm install koa-ratelimitconst 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 管理
npm install koa-sessionconst 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/cors | CORS 跨域 | API 服务 |
koa-mount | 中间件挂载 | 模块化应用 |
koa-logger | 请求日志 | 开发调试 |
koa-compress | 响应压缩 | 性能优化 |
koa-helmet | 安全头 | 生产环境 |
koa-jwt | JWT 认证 | API 认证 |
koa-ratelimit | 请求限流 | 防止滥用 |
koa-session | Session 管理 | 传统登录 |
自定义中间件开发
基本结构
// 基本中间件结构
async function myMiddleware(ctx, next) {
// 前置处理(请求阶段)
console.log("请求开始")
// 等待下游中间件执行
await next()
// 后置处理(响应阶段)
console.log("请求结束")
}
// 使用
app.use(myMiddleware)带配置的中间件
// 创建可配置的中间件
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
}))实用自定义中间件示例
请求计时中间件
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 中间件
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())认证中间件
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 }
})权限检查中间件
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) => {
// 只有管理员可以访问
}
)缓存中间件
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()
})请求验证中间件
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
})错误处理中间件
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. 中间件顺序
遵循正确的中间件注册顺序:
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. 错误处理
统一错误处理机制:
// 自定义错误类
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. 性能优化
// 响应压缩
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. 中间件复用
// 中间件工厂函数
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. 异步处理
// 正确处理异步错误
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: 中间件执行顺序为什么很重要?
中间件按照注册顺序执行,形成洋葱模型。错误的顺序可能导致:
// ❌ 错误:路由在错误处理之前
app.use(router.routes())
app.use(errorHandler()) // 永远不会被触发
// ✅ 正确:错误处理在最外层
app.use(errorHandler())
app.use(router.routes())Q2: 如何调试中间件执行流程?
// 添加调试日志
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: 中间件中如何共享数据?
// 使用 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: 如何在中间件中提前结束请求?
// 认证失败时提前返回
app.use(async (ctx, next) => {
if (!ctx.headers.authorization) {
ctx.status = 401
ctx.body = { error: "Unauthorized" }
return // 不调用 next(),提前结束
}
await next()
})Q5: 如何处理中间件中的异步错误?
// 方式 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-body | koa-bodyparser |
|---|---|---|
| 文件上传 | ✅ 支持 | ❌ 不支持 |
| JSON | ✅ | ✅ |
| 表单 | ✅ | ✅ |
| 配置复杂度 | 中等 | 简单 |
| 适用场景 | 需要文件上传 | 仅文本数据 |
// 需要文件上传:使用 koa-body
app.use(koaBody({ multipart: true }))
// 仅处理 JSON/表单:使用 koa-bodyparser
app.use(bodyParser())Q7: 如何实现中间件的单元测试?
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 中间件机制的核心要点:
- 洋葱模型:请求和响应双向处理,中间件形成洋葱结构
- async/await:全面支持异步流程控制
- 轻量灵活:核心极简,功能由中间件组合实现
- 模块化设计:按需组合中间件,构建复杂应用
- 丰富的生态:社区提供大量高质量中间件
下一步学习
推荐资源
官方资源
常用中间件仓库
最后更新:2026年2月