全栈手账 订阅 RSS
Koa

Koa 知识总结(基础 + 进阶 + 实战)

Koa 是 Express 原班团队打造的下一代 Node.js 框架,基于 async/await 与洋葱模型中间件,轻量灵活。比 Express 现代、比 NestJS 轻,适合中小项目。

读了 71 分钟 更新于 8 月 18 日

第一部分:基础

一、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 安装

bash
# 初始化项目mkdir my-koa-app && cd my-koa-appnpm init -y# 安装 Koanpm install koa# 安装常用中间件(按需)npm install @koa/router koa-bodyparser koa-static koa2-cors

2.2 Hello World

javascript
// 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');});
bash
node app.js# 访问 http://localhost:3000 → Hello, Koa!

2.3 TypeScript 版

bash
npm install koanpm install -D typescript @types/node @types/koa ts-node
typescript
// 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);
bash
# 运行npx ts-node app.ts# 或编译后运行npx tsc && node dist/app.js

2.4 推荐目录结构

code
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)对象中,并在上面挂载了很多便捷属性。

javascript
app.use(async (ctx) => {  // ctx.request —— 封装的请求对象  // ctx.response —— 封装的响应对象  // ctx.req —— Node.js 原生请求对象  // ctx.res —— Node.js 原生响应对象    ctx.body = 'Hello';  // 设置响应体(最常用)});

3.2 请求相关

javascript
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 响应相关

javascript
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(后)  ←  返回结果
javascript
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() 是洋葱模型的关键——它把控制权交给下一个中间件,等下游所有中间件执行完后再回来继续执行后续代码。

javascript
// 计时中间件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() 的后果

javascript
// ❌ 错误:忘记 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 常用中间件示例

javascript
// 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

bash
npm install @koa/router

5.2 基本路由

javascript
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 路由前缀

javascript
// 所有路由自动加 /api/v1 前缀const router = new Router({  prefix: '/api/v1',});router.get('/users', ...);    // 实际路径:/api/v1/usersrouter.get('/orders', ...);   // 实际路径:/api/v1/orders

5.4 路由模块化

javascript
// 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 路由重定向

javascript
router.redirect('/old-path', '/new-path', 301);router.get('/old', (ctx) => {  ctx.redirect('/new');});

第二部分:进阶

六、请求与响应

6.1 获取请求参数

javascript
// 安装 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 文件上传

bash
npm install @koa/multer multer
javascript
const 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

javascript
// 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 静态文件服务

bash
npm install koa-static
javascript
const 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 常用配置

javascript
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 接口限流

bash
npm install koa-ratelimit
javascript
const 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)

bash
npm install mysql2
javascript
// 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)

bash
npm install ioredis
javascript
// 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

bash
npm install sequelize mysql2
javascript
const { 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 全局错误中间件

javascript
// 中间件注册顺序很重要!错误处理要放在最前面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 自定义错误类

javascript
// 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 统一响应格式

javascript
// 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 处理

javascript
// 放在所有路由之后app.use(async (ctx) => {  ctx.status = 404;  ctx.body = {    code: 404,    message: `路径 ${ctx.method} ${ctx.path} 不存在`,    data: null,  };});

十、JWT 认证

10.1 安装

bash
npm install koa-jwt jsonwebtoken bcryptjs

10.2 登录注册

javascript
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 中间件

javascript
// 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 角色权限

javascript
// 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();});

第三部分:实战

十一、项目结构

code
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 入口文件

javascript
// 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 路由

javascript
// 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 控制器

javascript
// 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 服务层

javascript
// 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();

十三、文件上传与静态资源

javascript
// 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 部署

bash
# 安装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
javascript
// 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',  }],};
bash
pm2 start ecosystem.config.js

14.2 Docker 部署

dockerfile
# DockerfileFROM node:18-alpineWORKDIR /appCOPY package*.json ./RUN npm install --productionCOPY . .EXPOSE 3000CMD ["node", "src/app.js"]
yaml
# 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 反向代理

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,被全局错误处理捕获

十七、最佳实践

  1. 错误处理放最前面:第一个注册的中间件就应该是全局错误处理
  2. 统一响应格式:封装 success() / error() / paginate() 工具函数
  3. 分层架构:Controller → Service → Model,职责分离
  4. 路由模块化:每个业务一个 routes 文件,统一在 index.js 汇总
  5. 环境变量管理:用 dotenv 加载 .env,不硬编码
  6. 密码加密:用 bcryptjs,不要用 MD5
  7. SQL 防注入:用参数化查询,不要字符串拼接
  8. 日志规范:用 koa-logger 或 winston,不要用 console.log
  9. 限流防护:用 koa-ratelimit + Redis 防接口刷量
  10. PM2 集群模式:exec_mode: 'cluster' 利用多核 CPU

总结

Koa 的核心就三件事:

  1. ctx 对象 —— 请求和响应都通过 ctx 操作,简洁统一
  2. 洋葱模型中间件 —— async (ctx, next) => { await next(); } 一切皆中间件
  3. 自由组装 —— 路由、bodyparser、CORS、JWT 全部按需引入

Koa 不像 NestJS 那样"什么都帮你做好了",也不像 Express 那样"回调满天飞"。 它是中间最好的平衡——轻量但不简陋,自由但不混乱。 适合想要掌控代码细节、又不想写回调地狱的开发者。

前端、后端、部署、客服都是我一个人。写这里的东西都是真踩过的坑,不是抄来的教程。