Koa 框架
Koa 核心概念、中间件机制、路由与最佳实践
Koa 概述
特点
- 异步优先,由 Express 原班人马打造
- 极简设计,无内置路由和中间件
- 洋葱模型中间件
- 使用 async/await
- 错误处理更优雅
与 Express 对比
| 特性 | Koa | Express |
|---|---|---|
| 中间件模型 | 洋葱模型 | 线性模型 |
| 异步处理 | 原生 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 的区别?
| 特性 | Koa | Express |
|---|---|---|
| 中间件 | 洋葱模型 | 线性模型 |
| 异步 | 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() 后是返回阶段。后注册的中间件先返回。