Koa 知识总结(基础 + 进阶 + 实战)
Koa 是 Express 原班团队打造的下一代 Node.js 框架,基于 async/await 与洋葱模型中间件,轻量灵活。比 Express 现代、比 NestJS 轻,适合中小项目。
第一部分:基础
一、Koa 简介
1.1 什么是 Koa
- 由 Express 原班团队(TJ Holowaychuk 等)开发,2014 年发布
- 更轻量——核心只有 ~2000 行代码,不捆绑中间件
- 基于 async/await,原生支持异步流程控制
- 洋葱模型中间件机制,比 Express 的线性模型更灵活
1.2 Koa vs Express vs NestJS
| 对比项 | Koa | Express | NestJS |
|---|---|---|---|
| 出品方 | Express 原班团队 | Node.js 基金会 | 独立(借鉴 Angular) |
| 语言 | JS / TS 均可 | JS 为主 | TypeScript 优先 |
| 异步模型 | async/await | 回调 / Promise | async/await |
| 中间件模型 | 洋葱模型(双向) | 线性模型(单向) | 洋葱模型 + DI |
| 内置功能 | 极少(只有 HTTP 基础) | 路由、静态文件等 | 路由、DI、模块化、Swagger 等 |
| 学习曲线 | ★★★☆☆ | ★★☆☆☆ | ★★★★☆ |
| 适合项目 | 中小型 API、轻量服务 | 小型项目 | 中大型企业级 |
| 生态 | 需要自己组装中间件 | 中间件最多 | 官方模块丰富 |
1.3 Koa 的核心特点
1. 极简核心 —— 不捆绑路由、模板、静态文件等,按需引入2. 洋葱模型 —— 中间件可以在请求前后都执行代码3. async/await —— 原生异步,没有回调地狱4. Context 对象 —— ctx 统一封装 request 和 response5. 不内置中间件 —— 一切皆中间件,自由组合1.4 Koa1 vs Koa2
| 版本 | 异步方案 | 状态 |
|---|---|---|
| Koa1 | generator + co | 已废弃 |
| Koa2 | async/await | 当前主流 |
| Koa3 | async/await(优化) | 最新版 |
本文以 Koa2 为准。
二、快速开始
2.1 安装
# 初始化项目mkdir my-koa-app && cd my-koa-appnpm init -y# 安装 Koanpm install koa# 安装常用中间件(按需)npm install @koa/router koa-bodyparser koa-static koa2-cors2.2 Hello World
// app.jsconst Koa = require('koa');const app = new Koa();// 中间件:每次请求都会执行app.use(async (ctx) => { ctx.body = 'Hello, Koa!';});app.listen(3000, () => { console.log('Koa server running on http://localhost:3000');});node app.js# 访问 http://localhost:3000 → Hello, Koa!2.3 TypeScript 版
npm install koanpm install -D typescript @types/node @types/koa ts-node// app.tsimport Koa from 'koa';import { Context } from 'koa';const app = new Koa();app.use(async (ctx: Context) => { ctx.body = { message: 'Hello, Koa + TypeScript!' };});app.listen(3000);# 运行npx ts-node app.ts# 或编译后运行npx tsc && node dist/app.js2.4 推荐目录结构
my-koa-app/├── src/│ ├── app.js # 入口文件│ ├── config/ # 配置│ │ └── index.js│ ├── middlewares/ # 自定义中间件│ │ ├── error.js│ │ └── auth.js│ ├── routes/ # 路由│ │ ├── index.js│ │ ├── user.js│ │ └── order.js│ ├── controllers/ # 控制器│ │ └── user.controller.js│ ├── services/ # 业务逻辑│ │ └── user.service.js│ ├── models/ # 数据模型│ │ └── user.model.js│ └── utils/ # 工具函数│ └── response.js├── package.json└── .env三、Context 对象
3.1 ctx 是什么
Koa 把 Node.js 原生的 req 和 res 封装到了一个 ctx(Context)对象中,并在上面挂载了很多便捷属性。
app.use(async (ctx) => { // ctx.request —— 封装的请求对象 // ctx.response —— 封装的响应对象 // ctx.req —— Node.js 原生请求对象 // ctx.res —— Node.js 原生响应对象 ctx.body = 'Hello'; // 设置响应体(最常用)});3.2 请求相关
app.use(async (ctx) => { // 请求方法 ctx.method; // 'GET' / 'POST' / ... ctx.path; // '/users/123' ctx.url; // '/users/123?name=alice' // 请求头 ctx.header; // 所有请求头对象 ctx.get('Authorization'); // 获取单个请求头(推荐) ctx.headers.host; // 直接访问 // 查询参数 ctx.query; // { name: 'alice', age: '25' } ctx.querystring; // 'name=alice&age=25' // 路径参数(需配合路由中间件) ctx.params; // { id: '123' } // 请求体(需配合 koa-bodyparser) ctx.request.body; // { username: 'alice', password: '123456' } // IP ctx.ip; // 客户端 IP ctx.ips; // 代理链 IP 数组 // Cookie ctx.cookies.get('name'); // 获取 Cookie ctx.cookies.set('name', 'value', { httpOnly: true, maxAge: 7 * 24 * 3600 * 1000, // 7 天 });});3.3 响应相关
app.use(async (ctx) => { // 响应体(最常用) ctx.body = 'Hello'; // 字符串 ctx.body = { code: 200, data: {} }; // JSON(自动设置 Content-Type) ctx.body = Buffer.from('...'); // Buffer ctx.body = fs.createReadStream('file.pdf'); // Stream // 状态码 ctx.status = 200; // OK ctx.status = 404; // Not Found ctx.status = 500; // Server Error // 响应头 ctx.set('X-Custom-Header', 'value'); ctx.set({ 'Cache-Control': 'no-cache', 'X-Total-Count': '100', }); // 重定向 ctx.redirect('/login'); ctx.redirect('https://example.com'); // 附加数据(不会发送给客户端,中间件间传递) ctx.state.user = { id: 1, name: 'Alice' };});3.4 ctx 别名速查
| Koa 封装 | 对应原生 | 说明 |
|---|---|---|
ctx.method |
ctx.req.method |
请求方法 |
ctx.path |
ctx.req.url 解析 |
请求路径 |
ctx.header |
ctx.req.headers |
请求头 |
ctx.query |
URL 解析 | 查询参数对象 |
ctx.body |
ctx.res.end() |
响应体 |
ctx.status |
ctx.res.statusCode |
状态码 |
ctx.set() |
ctx.res.setHeader() |
设置响应头 |
四、中间件机制
4.1 洋葱模型
Koa 的中间件是洋葱模型——请求像穿洋葱一样,一层层进去,再一层层出来:
请求 → 中间件A(前) → 中间件B(前) → 中间件C(前) → 路由处理 ↓响应 ← 中间件A(后) ← 中间件B(后) ← 中间件C(后) ← 返回结果const Koa = require('koa');const app = new Koa();// 中间件 Aapp.use(async (ctx, next) => { console.log('A - 请求前'); await next(); // 把控制权交给下一个中间件 console.log('A - 响应后');});// 中间件 Bapp.use(async (ctx, next) => { console.log('B - 请求前'); await next(); console.log('B - 响应后');});// 中间件 C(实际处理)app.use(async (ctx) => { console.log('C - 处理请求'); ctx.body = 'Hello';});// 输出顺序:// A - 请求前// B - 请求前// C - 处理请求// B - 响应后// A - 响应后4.2 next() 的作用
await next() 是洋葱模型的关键——它把控制权交给下一个中间件,等下游所有中间件执行完后再回来继续执行后续代码。
// 计时中间件app.use(async (ctx, next) => { const start = Date.now(); await next(); // 等待下游中间件执行完毕 const ms = Date.now() - start; ctx.set('X-Response-Time', `${ms}ms`); console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);});4.3 忘记 await next() 的后果
// ❌ 错误:忘记 awaitapp.use(async (ctx, next) => { console.log('前'); next(); // 没有 await! console.log('后'); // 会在下游中间件还没执行完就打印});// ✅ 正确app.use(async (ctx, next) => { console.log('前'); await next(); // 等待下游完成 console.log('后');});铁律:Koa 中间件必须
await next(),否则洋葱模型会断裂,执行顺序错乱。
4.4 常用中间件示例
// 1. 日志中间件app.use(async (ctx, next) => { const start = new Date(); await next(); const ms = new Date() - start; console.log(`${ctx.method} ${ctx.url} ${ctx.status} - ${ms}ms`);});// 2. 统一响应格式app.use(async (ctx, next) => { await next(); if (ctx.body && typeof ctx.body === 'object') { ctx.body = { code: ctx.status === 200 ? 0 : ctx.status, message: ctx.message || 'success', data: ctx.body, }; }});// 3. CORS 跨域app.use(async (ctx, next) => { ctx.set('Access-Control-Allow-Origin', '*'); ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (ctx.method === 'OPTIONS') { ctx.status = 204; return; } await next();});五、路由
5.1 安装 @koa/router
npm install @koa/router5.2 基本路由
const Koa = require('koa');const Router = require('@koa/router');const app = new Koa();const router = new Router();// GET 请求router.get('/', (ctx) => { ctx.body = '首页';});router.get('/users', (ctx) => { ctx.body = '用户列表';});// POST 请求(需配合 koa-bodyparser 荣获取 body)router.post('/users', (ctx) => { const body = ctx.request.body; ctx.body = { message: '创建成功', data: body };});// 动态路由router.get('/users/:id', (ctx) => { const id = ctx.params.id; ctx.body = `用户 ${id}`;});// PUT / DELETErouter.put('/users/:id', (ctx) => { ctx.body = `更新用户 ${ctx.params.id}`;});router.delete('/users/:id', (ctx) => { ctx.body = `删除用户 ${ctx.params.id}`;});// 使用路由app.use(router.routes());app.use(router.allowedMethods()); // 自动响应 OPTIONS,处理 405/501app.listen(3000);5.3 路由前缀
// 所有路由自动加 /api/v1 前缀const router = new Router({ prefix: '/api/v1',});router.get('/users', ...); // 实际路径:/api/v1/usersrouter.get('/orders', ...); // 实际路径:/api/v1/orders5.4 路由模块化
// routes/user.routes.jsconst Router = require('@koa/router');const router = new Router({ prefix: '/users' });router.get('/', (ctx) => { ctx.body = '用户列表'; });router.get('/:id', (ctx) => { ctx.body = `用户 ${ctx.params.id}`; });router.post('/', (ctx) => { ctx.body = '创建用户'; });module.exports = router;// routes/order.routes.jsconst Router = require('@koa/router');const router = new Router({ prefix: '/orders' });router.get('/', (ctx) => { ctx.body = '订单列表'; });module.exports = router;// app.jsconst Koa = require('koa');const app = new Koa();const userRoutes = require('./routes/user.routes');const orderRoutes = require('./routes/order.routes');app.use(userRoutes.routes()).use(userRoutes.allowedMethods());app.use(orderRoutes.routes()).use(orderRoutes.allowedMethods());app.listen(3000);5.5 路由重定向
router.redirect('/old-path', '/new-path', 301);router.get('/old', (ctx) => { ctx.redirect('/new');});第二部分:进阶
六、请求与响应
6.1 获取请求参数
// 安装 bodyparser// npm install koa-bodyparserconst bodyParser = require('koa-bodyparser');app.use(bodyParser());router.post('/users', (ctx) => { // 1. 路径参数 // GET /users/123 ctx.params.id; // '123' // 2. 查询参数 // GET /users?page=1&size=10&keyword=alice ctx.query.page; // '1' ctx.query.size; // '10' ctx.query.keyword; // 'alice' // 3. 请求体(需 koa-bodyparser) // POST /users Body: { "name": "Alice", "age": 25 } ctx.request.body.name; // 'Alice' ctx.request.body.age; // 25 // 4. 请求头 ctx.get('Authorization'); // 'Bearer xxx' ctx.get('Content-Type'); // 'application/json' // 5. 表单数据(application/x-www-form-urlencoded) // koa-bodyparser 也支持 ctx.request.body.username; ctx.body = { message: '参数获取成功' };});6.2 文件上传
npm install @koa/multer multerconst Koa = require('koa');const Router = require('@koa/router');const multer = require('@koa/multer');const path = require('path');const app = new Koa();const router = new Router();// 配置 multerconst storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, 'uploads/'); }, filename: (req, file, cb) => { const ext = path.extname(file.originalname); const filename = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`; cb(null, filename); },});const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 }, // 5MB fileFilter: (req, file, cb) => { const allowed = /jpeg|jpg|png|gif|webp/; const ok = allowed.test(path.extname(file.originalname).toLowerCase()); if (ok) cb(null, true); else cb(new Error('只支持图片格式')); },});// 单文件上传router.post('/upload', upload.single('file'), (ctx) => { const file = ctx.file; ctx.body = { url: `/uploads/${file.filename}`, filename: file.filename, size: file.size, };});// 多文件上传router.post('/uploads', upload.array('files', 10), (ctx) => { const files = ctx.files; ctx.body = files.map(f => ({ url: `/uploads/${f.filename}`, filename: f.filename, }));});app.use(router.routes());app.listen(3000);6.3 Cookie 与 Session
// Cookierouter.get('/set-cookie', (ctx) => { ctx.cookies.set('name', 'Alice', { httpOnly: true, // JS 不可读 maxAge: 7 * 24 * 3600 * 1000, // 7 天 sameSite: 'strict', secure: false, // 生产环境用 true(HTTPS) }); ctx.body = 'Cookie 已设置';});router.get('/get-cookie', (ctx) => { const name = ctx.cookies.get('name'); ctx.body = `Cookie: ${name}`;});// Session(需安装 koa-session)// npm install koa-sessionconst session = require('koa-session');app.keys = ['your-secret-key']; // 必须设置app.use(session({ maxAge: 7 * 24 * 3600 * 1000, httpOnly: true,}, app));router.post('/login', (ctx) => { // 验证用户... ctx.session.userId = 1; ctx.session.username = 'Alice'; ctx.body = '登录成功';});router.get('/profile', (ctx) => { if (!ctx.session.userId) { ctx.status = 401; ctx.body = '请先登录'; return; } ctx.body = { userId: ctx.session.userId, username: ctx.session.username };});router.post('/logout', (ctx) => { ctx.session = null; ctx.body = '已退出';});6.4 静态文件服务
npm install koa-staticconst serve = require('koa-static');const path = require('path');// 访问 http://localhost:3000/uploads/xxx.pngapp.use(serve(path.join(__dirname, 'uploads')));// 多个目录app.use(serve(path.join(__dirname, 'public')));app.use(serve(path.join(__dirname, 'dist')));七、常用中间件
7.1 中间件速查表
| 中间件 | 安装包 | 作用 |
|---|---|---|
| koa-bodyparser | koa-bodyparser |
解析 JSON/表单请求体 |
| @koa/router | @koa/router |
路由 |
| koa-static | koa-static |
静态文件服务 |
| koa2-cors | koa2-cors |
CORS 跨域 |
| koa-jwt | koa-jwt |
JWT 认证 |
| koa-logger | koa-logger |
请求日志 |
| koa-helmet | koa-helmet |
安全头 |
| koa-compress | koa-compress |
Gzip 压缩 |
| koa-session | koa-session |
Session |
| @koa/multer | @koa/multer |
文件上传 |
| koa-rate-limit | koa-ratelimit |
接口限流 |
| koa-swagger-decorator | koa-swagger-decorator |
Swagger 文档 |
7.2 常用配置
const Koa = require('koa');const bodyParser = require('koa-bodyparser');const cors = require('koa2-cors');const logger = require('koa-logger');const helmet = require('koa-helmet');const compress = require('koa-compress');const app = new Koa();// 安全头app.use(helmet());// 请求日志app.use(logger());// Gzip 压缩app.use(compress({ threshold: 1024, // 大于 1KB 才压缩}));// CORS 跨域app.use(cors({ origin: (ctx) => { // 白名单 const allowedOrigins = ['https://example.com', 'http://localhost:5173']; const origin = ctx.header.origin; if (allowedOrigins.includes(origin)) { return origin; } return null; }, allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowHeaders: ['Content-Type', 'Authorization'], credentials: true,}));// 请求体解析app.use(bodyParser({ jsonLimit: '10mb', formLimit: '10mb', textLimit: '10mb',}));7.3 接口限流
npm install koa-ratelimitconst rateLimit = require('koa-ratelimit');const Redis = require('ioredis');app.use(rateLimit({ db: new Redis(), duration: 60000, // 60 秒 max: 100, // 每个 IP 最多 100 次 id: (ctx) => ctx.ip, errorMessage: '请求过于频繁,请稍后再试', headers: { remaining: 'Rate-Limit-Remaining', reset: 'Rate-Limit-Reset', total: 'Rate-Limit-Total', },}));八、数据库集成
8.1 MySQL(mysql2)
npm install mysql2// config/database.jsconst mysql = require('mysql2/promise');const pool = mysql.createPool({ host: 'localhost', port: 3306, user: 'root', password: '123456', database: 'travel_db', waitForConnections: true, connectionLimit: 10, queueLimit: 0,});module.exports = pool;// services/user.service.jsconst pool = require('../config/database');class UserService { async findAll(page = 1, size = 10) { const offset = (page - 1) * size; const [rows] = await pool.query( 'SELECT id, username, email, status FROM users ORDER BY id DESC LIMIT ? OFFSET ?', [Number(size), Number(offset)] ); const [[{ total }]] = await pool.query('SELECT COUNT(*) as total FROM users'); return { list: rows, total, page, size }; } async findById(id) { const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [id]); return rows[0]; } async create(data) { const { username, email, password } = data; const [result] = await pool.query( 'INSERT INTO users (username, email, password) VALUES (?, ?, ?)', [username, email, password] ); return { id: result.insertId, username, email }; } async update(id, data) { const fields = []; const values = []; for (const [key, value] of Object.entries(data)) { fields.push(`${key} = ?`); values.push(value); } values.push(id); await pool.query(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`, values); return this.findById(id); } async delete(id) { await pool.query('DELETE FROM users WHERE id = ?', [id]); }}module.exports = new UserService();8.2 Redis(ioredis)
npm install ioredis// config/redis.jsconst Redis = require('ioredis');const redis = new Redis({ host: '127.0.0.1', port: 6379, password: 'YourRedisPass', db: 0,});module.exports = redis;// 使用:缓存用户信息const redis = require('../config/redis');async function getUser(id) { const cacheKey = `user:${id}`; // 先查缓存 const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } // 查数据库 const user = await User.findById(id); if (user) { // 写入缓存,过期 10 分钟 await redis.setex(cacheKey, 600, JSON.stringify(user)); } return user;}8.3 Sequelize ORM
npm install sequelize mysql2const { Sequelize, DataTypes } = require('sequelize');const sequelize = new Sequelize('travel_db', 'root', '123456', { host: 'localhost', dialect: 'mysql', logging: false,});// 定义模型const User = sequelize.define('User', { username: { type: DataTypes.STRING(50), allowNull: false, unique: true }, email: { type: DataTypes.STRING(100), allowNull: false }, password: { type: DataTypes.STRING(255), allowNull: false }, status: { type: DataTypes.ENUM('active', 'inactive', 'banned'), defaultValue: 'active' },}, { tableName: 'users', timestamps: true, underscored: true,});// 使用async function findAll() { return await User.findAll({ attributes: ['id', 'username', 'email', 'status'], order: [['created_at', 'DESC']], });}九、错误处理
9.1 全局错误中间件
// 中间件注册顺序很重要!错误处理要放在最前面app.use(async (ctx, next) => { try { await next(); } catch (err) { ctx.status = err.status || 500; ctx.body = { code: ctx.status, message: err.message || '服务器内部错误', data: null, }; // 记录错误日志 console.error(`[${new Date().toISOString()}] ${ctx.method} ${ctx.url}`); console.error(err.stack); }});9.2 自定义错误类
// utils/errors.jsclass BusinessException extends Error { constructor(code, message) { super(message); this.status = 200; // HTTP 状态码 200 this.code = code; // 业务错误码 }}class ValidationError extends Error { constructor(message) { super(message); this.status = 400; }}class NotFoundError extends Error { constructor(resource) { super(`${resource} 不存在`); this.status = 404; }}class UnauthorizedError extends Error { constructor(message = '未授权') { super(message); this.status = 401; }}module.exports = { BusinessException, ValidationError, NotFoundError, UnauthorizedError };// 使用const { NotFoundError, ValidationError } = require('../utils/errors');router.get('/users/:id', async (ctx) => { const user = await userService.findById(ctx.params.id); if (!user) { throw new NotFoundError('用户'); // 会被全局错误中间件捕获 } ctx.body = user;});9.3 统一响应格式
// middlewares/response.jsfunction success(ctx, data, message = 'success') { ctx.body = { code: 0, message, data, };}function error(ctx, code, message) { ctx.body = { code, message, data: null, };}function paginate(ctx, list, total, page, size) { ctx.body = { code: 0, message: 'success', data: { list, total, page: Number(page), size: Number(size), totalPages: Math.ceil(total / size), }, };}module.exports = { success, error, paginate };// 使用const { success, paginate } = require('../middlewares/response');router.get('/users', async (ctx) => { const { page = 1, size = 10 } = ctx.query; const result = await userService.findAll(page, size); paginate(ctx, result.list, result.total, page, size);});router.post('/users', async (ctx) => { const user = await userService.create(ctx.request.body); success(ctx, user, '创建成功');});9.4 404 处理
// 放在所有路由之后app.use(async (ctx) => { ctx.status = 404; ctx.body = { code: 404, message: `路径 ${ctx.method} ${ctx.path} 不存在`, data: null, };});十、JWT 认证
10.1 安装
npm install koa-jwt jsonwebtoken bcryptjs10.2 登录注册
const jwt = require('jsonwebtoken');const bcrypt = require('bcryptjs');const SECRET = 'your-secret-key';router.post('/auth/register', async (ctx) => { const { username, password, email } = ctx.request.body; // 检查用户名是否已存在 const existing = await userService.findByUsername(username); if (existing) { ctx.status = 409; ctx.body = { code: 409, message: '用户名已存在' }; return; } // 加密密码 const hashedPassword = await bcrypt.hash(password, 10); // 创建用户 const user = await userService.create({ username, email, password: hashedPassword, }); // 生成 token const token = jwt.sign( { sub: user.id, username: user.username }, SECRET, { expiresIn: '7d' } ); ctx.body = { code: 0, message: '注册成功', data: { user: { id: user.id, username: user.username }, token }, };});router.post('/auth/login', async (ctx) => { const { username, password } = ctx.request.body; const user = await userService.findByUsername(username); if (!user) { ctx.status = 401; ctx.body = { code: 401, message: '用户名或密码错误' }; return; } const valid = await bcrypt.compare(password, user.password); if (!valid) { ctx.status = 401; ctx.body = { code: 401, message: '用户名或密码错误' }; return; } const token = jwt.sign( { sub: user.id, username: user.username }, SECRET, { expiresIn: '7d' } ); ctx.body = { code: 0, message: '登录成功', data: { user: { id: user.id, username: user.username }, token }, };});10.3 JWT 中间件
// middlewares/auth.jsconst jwt = require('jsonwebtoken');const SECRET = 'your-secret-key';// 不需要认证的路由const publicPaths = [ /^\/api\/auth\/register/, /^\/api\/auth\/login/,];function isPublic(path) { return publicPaths.some(pattern => pattern.test(path));}module.exports = async function auth(ctx, next) { // 公开路由跳过 if (isPublic(ctx.path)) { return await next(); } const authHeader = ctx.header.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { ctx.status = 401; ctx.body = { code: 401, message: '请提供认证 token' }; return; } const token = authHeader.replace('Bearer ', ''); try { const decoded = jwt.verify(token, SECRET); ctx.state.user = decoded; // 把用户信息挂到 ctx.state await next(); } catch (err) { ctx.status = 401; ctx.body = { code: 401, message: 'token 无效或已过期' }; }};// 使用app.use(auth); // 在路由之前app.use(router.routes());10.4 角色权限
// middlewares/role.jsmodule.exports = (...roles) => { return async (ctx, next) => { if (!ctx.state.user) { ctx.status = 401; ctx.body = { code: 401, message: '未登录' }; return; } if (!roles.includes(ctx.state.user.role)) { ctx.status = 403; ctx.body = { code: 403, message: '权限不足' }; return; } await next(); };};// 使用const requireRole = require('../middlewares/role');router.get('/admin/users', requireRole('admin'), async (ctx) => { // 只有 admin 能访问 ctx.body = await userService.findAll();});第三部分:实战
十一、项目结构
src/├── app.js # 入口├── config/│ ├── index.js # 环境配置│ ├── database.js # 数据库配置│ └── redis.js # Redis 配置├── middlewares/│ ├── error.js # 全局错误处理│ ├── auth.js # JWT 认证│ ├── role.js # 角色权限│ └── response.js # 统一响应├── routes/│ ├── index.js # 路由汇总│ ├── auth.routes.js│ ├── user.routes.js│ └── order.routes.js├── controllers/│ ├── auth.controller.js│ ├── user.controller.js│ └── order.controller.js├── services/│ ├── auth.service.js│ ├── user.service.js│ └── order.service.js├── models/│ └── user.model.js├── utils/│ ├── errors.js # 自定义错误│ └── validator.js # 参数验证└── validators/ └── user.validator.js # DTO 验证十二、实战:用户模块
12.1 入口文件
// app.jsconst Koa = require('koa');const bodyParser = require('koa-bodyparser');const cors = require('koa2-cors');const logger = require('koa-logger');const helmet = require('koa-helmet');const errorHandler = require('./middlewares/error');const authMiddleware = require('./middlewares/auth');const routes = require('./routes');const app = new Koa();// 1. 安全头app.use(helmet());// 2. 请求日志app.use(logger());// 3. CORSapp.use(cors({ origin: '*' }));// 4. 请求体解析app.use(bodyParser());// 5. 全局错误处理(必须放最前面)app.use(errorHandler);// 6. JWT 认证app.use(authMiddleware);// 7. 路由app.use(routes.routes()).use(routes.allowedMethods());// 8. 404app.use(async (ctx) => { ctx.status = 404; ctx.body = { code: 404, message: 'Not Found' };});const PORT = process.env.PORT || 3000;app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`);});12.2 路由
// routes/user.routes.jsconst Router = require('@koa/router');const router = new Router({ prefix: '/api/users' });const userController = require('../controllers/user.controller');const auth = require('../middlewares/auth');const requireRole = require('../middlewares/role');// 公开router.get('/', userController.findAll);router.get('/:id', userController.findById);// 需要登录router.post('/', auth, userController.create);router.put('/:id', auth, userController.update);router.delete('/:id', auth, requireRole('admin'), userController.delete);module.exports = router;12.3 控制器
// controllers/user.controller.jsconst userService = require('../services/user.service');const { success, paginate } = require('../middlewares/response');const { NotFoundError, ValidationError } = require('../utils/errors');module.exports = { async findAll(ctx) { const { page = 1, size = 10, keyword } = ctx.query; const result = await userService.findAll({ page, size, keyword }); paginate(ctx, result.list, result.total, page, size); }, async findById(ctx) { const user = await userService.findById(ctx.params.id); if (!user) throw new NotFoundError('用户'); success(ctx, user); }, async create(ctx) { const { username, email, password } = ctx.request.body; // 参数验证 if (!username || !email || !password) { throw new ValidationError('用户名、邮箱、密码不能为空'); } if (password.length < 6) { throw new ValidationError('密码至少 6 位'); } const user = await userService.create(ctx.request.body); success(ctx, user, '创建成功'); }, async update(ctx) { const user = await userService.update(ctx.params.id, ctx.request.body); success(ctx, user, '更新成功'); }, async delete(ctx) { await userService.delete(ctx.params.id); success(ctx, null, '删除成功'); },};12.4 服务层
// services/user.service.jsconst pool = require('../config/database');const bcrypt = require('bcryptjs');const { ConflictError, NotFoundError } = require('../utils/errors');class UserService { async findAll({ page = 1, size = 10, keyword }) { const offset = (page - 1) * size; let sql = 'SELECT id, username, email, status, created_at FROM users'; let countSql = 'SELECT COUNT(*) as total FROM users'; const params = []; if (keyword) { sql += ' WHERE username LIKE ?'; countSql += ' WHERE username LIKE ?'; params.push(`%${keyword}%`); } sql += ' ORDER BY id DESC LIMIT ? OFFSET ?'; const [rows] = await pool.query(sql, [Number(size), Number(offset)]); const [[{ total }]] = await pool.query(countSql, params); return { list: rows, total }; } async findById(id) { const [rows] = await pool.query( 'SELECT id, username, email, status, created_at FROM users WHERE id = ?', [id] ); return rows[0]; } async findByUsername(username) { const [rows] = await pool.query('SELECT * FROM users WHERE username = ?', [username]); return rows[0]; } async create(data) { const existing = await this.findByUsername(data.username); if (existing) throw new ConflictError('用户名已存在'); const hashedPassword = await bcrypt.hash(data.password, 10); const [result] = await pool.query( 'INSERT INTO users (username, email, password) VALUES (?, ?, ?)', [data.username, data.email, hashedPassword] ); return { id: result.insertId, username: data.username, email: data.email }; } async update(id, data) { const user = await this.findById(id); if (!user) throw new NotFoundError('用户'); const fields = []; const values = []; if (data.email) { fields.push('email = ?'); values.push(data.email); } if (data.status) { fields.push('status = ?'); values.push(data.status); } if (data.password) { const hashed = await bcrypt.hash(data.password, 10); fields.push('password = ?'); values.push(hashed); } if (fields.length > 0) { values.push(id); await pool.query(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`, values); } return this.findById(id); } async delete(id) { const user = await this.findById(id); if (!user) throw new NotFoundError('用户'); await pool.query('DELETE FROM users WHERE id = ?', [id]); }}module.exports = new UserService();十三、文件上传与静态资源
// routes/upload.routes.jsconst Router = require('@koa/router');const multer = require('@koa/multer');const path = require('path');const fs = require('fs');const router = new Router({ prefix: '/api/upload' });// 确保上传目录存在const uploadDir = path.join(__dirname, '../../uploads');if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true });}const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, uploadDir), filename: (req, file, cb) => { const ext = path.extname(file.originalname); cb(null, `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`); },});const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 }, fileFilter: (req, file, cb) => { const allowed = /jpeg|jpg|png|gif|webp/; if (allowed.test(path.extname(file.originalname).toLowerCase())) { cb(null, true); } else { cb(new Error('只支持图片格式')); } },});router.post('/single', upload.single('file'), (ctx) => { ctx.body = { code: 0, data: { url: `/uploads/${ctx.file.filename}`, filename: ctx.file.filename, size: ctx.file.size, }, };});router.post('/multiple', upload.array('files', 10), (ctx) => { ctx.body = { code: 0, data: ctx.files.map(f => ({ url: `/uploads/${f.filename}`, filename: f.filename, })), };});module.exports = router;// app.js 中添加静态文件服务const serve = require('koa-static');app.use(serve(path.join(__dirname, 'uploads')));十四、部署上线
14.1 PM2 部署
# 安装npm install -g pm2# 启动pm2 start src/app.js --name travel-api# 带环境变量启动pm2 start src/app.js --name travel-api -- --env production# 常用命令pm2 listpm2 logs travel-apipm2 restart travel-apipm2 stop travel-apipm2 delete travel-api# 开机自启pm2 startuppm2 save// ecosystem.config.js(PM2 配置文件)module.exports = { apps: [{ name: 'travel-api', script: 'src/app.js', instances: 2, // 2 个进程(负载均衡) exec_mode: 'cluster', max_memory_restart: '500M', env: { NODE_ENV: 'production', PORT: 3000, }, error_file: './logs/error.log', out_file: './logs/output.log', log_date_format: 'YYYY-MM-DD HH:mm:ss', }],};pm2 start ecosystem.config.js14.2 Docker 部署
# DockerfileFROM node:18-alpineWORKDIR /appCOPY package*.json ./RUN npm install --productionCOPY . .EXPOSE 3000CMD ["node", "src/app.js"]# docker-compose.ymlversion: '3.8'services: api: build: . ports: - "3000:3000" environment: - NODE_ENV=production - DB_HOST=mysql - REDIS_HOST=redis depends_on: - mysql - redis restart: always mysql: image: mysql:8 environment: MYSQL_ROOT_PASSWORD: rootpassword MYSQL_DATABASE: travel_db volumes: - mysql-data:/var/lib/mysql redis: image: redis:7-alpine volumes: - redis-data:/datavolumes: mysql-data: redis-data:14.3 Nginx 反向代理
server { listen 80; server_name api.example.com; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /uploads/ { alias /www/wwwroot/api/uploads/; }}十五、Koa vs Express vs NestJS
| 维度 | Koa | Express | NestJS |
|---|---|---|---|
| 定位 | 轻量框架 | 经典框架 | 企业级框架 |
| 代码风格 | async/await | 回调/Promise | 装饰器 + class |
| 中间件 | 洋葱模型 | 线性模型 | 洋葱 + DI |
| 路由 | 需装 @koa/router | 内置 | 装饰器声明 |
| 类型安全 | 需自行加 TS | 弱 | 原生 TS |
| 学习成本 | 中 | 低 | 高 |
| 开发效率 | 中(需组装) | 高(开箱即用) | 高(约定式) |
| 适合项目 | 中小型 API | 快速原型 | 大型企业级 |
| 生态 | 中间件需自己找 | 最丰富 | 官方模块多 |
| 性能 | 好 | 好 | 略低(DI 开销) |
选型建议:
| 场景 | 推荐 |
|---|---|
| 快速写个小 API | Express |
| 想用 async/await + 轻量框架 | Koa |
| 大型项目、团队协作 | NestJS |
| 已有 Express 项目想迁移 | Koa(语法相近) |
| 微服务、Serverless | Koa / Express |
十六、常见面试题
Q1:Koa 和 Express 的区别?
- 中间件模型不同:Koa 洋葱模型(双向),Express 线性模型(单向)
- 异步方案不同:Koa 用 async/await,Express 用回调
- Koa 不内置中间件,Express 内置了路由、静态文件等
Q2:什么是洋葱模型?
- 中间件像洋葱一样一层层包裹
- 请求时从外到内执行
next()前的代码 - 响应时从内到外执行
next()后的代码 - 每个中间件可以在请求前后都做事
Q3:为什么 Koa 中间件要 await next()?
next()返回的是 Promise- 不 await 的话,后续代码会在下游中间件还没执行完就运行
- 导致洋葱模型断裂,执行顺序错乱
Q4:Koa 的 ctx 是什么?
- Context 对象,封装了 request 和 response
- 挂载了便捷属性:
ctx.body、ctx.query、ctx.params、ctx.method等 ctx.state用于中间件间传递数据
Q5:Koa 如何处理错误?
- 用 try-catch 包裹
await next() - 抛出的错误会被外层的错误处理中间件捕获
- 也可以监听
app.on('error', handler)事件
Q6:Koa 如何获取 POST 请求体?
- Koa 原生不支持,需要
koa-bodyparser中间件 - 配置后通过
ctx.request.body获取
Q7:Koa 如何实现路由?
- Koa 不内置路由,需要
@koa/router中间件 - 支持路由前缀、动态参数、模块化拆分
Q8:Koa 和 Express 中间件的区别?
- Express 中间件:
(req, res, next) => {},操作 req/res - Koa 中间件:
(ctx, next) => {},操作 ctx,可以 await next() - Koa 可以在响应后做处理,Express 不行(已经 res.end() 了)
Q9:Koa 的洋葱模型有什么优势?
- 可以在响应后执行代码(如日志、计时)
- 更清晰的请求/响应处理分离
- 更适合组合复杂中间件链
Q10:Koa 项目如何做参数验证?
- 用
joi、zod、class-validator等库 - 在中间件或控制器中验证
- 验证失败抛出 ValidationError,被全局错误处理捕获
十七、最佳实践
- 错误处理放最前面:第一个注册的中间件就应该是全局错误处理
- 统一响应格式:封装
success()/error()/paginate()工具函数 - 分层架构:Controller → Service → Model,职责分离
- 路由模块化:每个业务一个 routes 文件,统一在 index.js 汇总
- 环境变量管理:用
dotenv加载.env,不硬编码 - 密码加密:用
bcryptjs,不要用 MD5 - SQL 防注入:用参数化查询,不要字符串拼接
- 日志规范:用
koa-logger或winston,不要用console.log - 限流防护:用
koa-ratelimit+ Redis 防接口刷量 - PM2 集群模式:
exec_mode: 'cluster'利用多核 CPU
总结
Koa 的核心就三件事:
- ctx 对象 —— 请求和响应都通过 ctx 操作,简洁统一
- 洋葱模型中间件 ——
async (ctx, next) => { await next(); }一切皆中间件 - 自由组装 —— 路由、bodyparser、CORS、JWT 全部按需引入
Koa 不像 NestJS 那样"什么都帮你做好了",也不像 Express 那样"回调满天飞"。 它是中间最好的平衡——轻量但不简陋,自由但不混乱。 适合想要掌控代码细节、又不想写回调地狱的开发者。