Koa框架

Koa 是由 Express 原班人马打造的下一代 Web 框架,更轻量、更现代,充分利用了 async/await 语法。

Koa 简介

Koa vs Express

特性 Koa Express
核心大小 极简,约 500 行 较大,内置更多功能
中间件模型 洋葱模型 线性模型
异步处理 async/await 回调/Promise
上下文对象 ctx(合并 req/res) req/res 分离
错误处理 try/catch 错误中间件
灵活性 更高 较低

安装与初始化

# 创建项目
mkdir dongba-koa
cd dongba-koa
npm init -y

# 安装 Koa
npm install koa

# 安装常用中间件
npm install @koa/router koa-body koa-static koa-logger

最简应用

const Koa = require('koa')

const app = new Koa()
const PORT = 3000

// 中间件
app.use(async (ctx) => {
  ctx.body = '东巴文欢迎你'
})

// 启动服务器
app.listen(PORT, () => {
  console.log(`服务器运行在 http://localhost:${PORT}`)
})

上下文对象(Context)

Koa 将 Node.js 的 request 和 response 对象封装到一个 ctx 对象中。

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx) => {
  // 请求信息
  console.log(ctx.request.method)      // 请求方法
  console.log(ctx.request.url)         // 请求 URL
  console.log(ctx.request.header)      // 请求头
  console.log(ctx.request.query)       // 查询参数
  console.log(ctx.request.body)        // 请求体(需要中间件)
  
  // 简写形式
  console.log(ctx.method)              // 等同于 ctx.request.method
  console.log(ctx.url)                 // 等同于 ctx.request.url
  console.log(ctx.query)               // 等同于 ctx.request.query
  
  // 响应设置
  ctx.status = 200                     // 状态码
  ctx.message = 'OK'                   // 状态消息
  ctx.type = 'application/json'        // Content-Type
  ctx.set('X-Custom', 'Dongba')        // 自定义响应头
  
  // 响应体
  ctx.body = {                         // 自动序列化 JSON
    name: '东巴文',
    message: 'Hello Koa'
  }
  
  // ctx.body = '文本响应'              // 文本
  // ctx.body = Buffer.from('buffer')  // Buffer
  // ctx.body = fs.createReadStream()  // 流
})

app.listen(3000)

Context API 详解

app.use(async (ctx) => {
  // 请求相关
  ctx.href              // 完整 URL
  ctx.origin            // 协议 + 主机
  ctx.protocol          // 协议(http/https)
  ctx.host              // 主机名 + 端口
  ctx.hostname          // 主机名
  ctx.port              // 端口
  ctx.path              // 路径
  ctx.querystring       // 查询字符串
  ctx.query             // 解析后的查询对象
  ctx.search            // 带 ? 的查询字符串
  ctx.hash              // URL hash
  ctx.ip                // 客户端 IP
  ctx.ips               // 代理 IP 列表
  ctx.subdomains        // 子域名列表
  
  // 请求头
  ctx.header            // 所有请求头
  ctx.headers           // 同上
  ctx.get('User-Agent') // 获取特定请求头
  
  // 请求方法判断
  ctx.is('json')        // 检查 Content-Type
  ctx.accepts('json', 'html')  // 内容协商
  
  // 响应相关
  ctx.status            // 状态码
  ctx.message           // 状态消息
  ctx.body              // 响应体
  ctx.length            // Content-Length
  ctx.type              // Content-Type
  ctx.headerSent        // 是否已发送响应头
  
  // 响应方法
  ctx.set('X-Custom', 'value')    // 设置响应头
  ctx.append('Set-Cookie', '...') // 追加响应头
  ctx.remove('X-Custom')          // 移除响应头
  ctx.redirect('/new-path')       // 重定向
  ctx.attachment('file.txt')      // 附件下载
  
  // Cookie
  ctx.cookies.get('name')
  ctx.cookies.set('name', 'value', {
    maxAge: 86400000,
    httpOnly: true,
    secure: true,
    signed: true
  })
})

洋葱模型中间件

Koa 的中间件采用洋葱模型,请求依次穿过各层中间件,响应则反向返回。

中间件执行顺序

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx, next) => {
  console.log('1. 第一层中间件 - 开始')
  await next()
  console.log('1. 第一层中间件 - 结束')
})

app.use(async (ctx, next) => {
  console.log('2. 第二层中间件 - 开始')
  await next()
  console.log('2. 第二层中间件 - 结束')
})

app.use(async (ctx, next) => {
  console.log('3. 第三层中间件 - 开始')
  await next()
  console.log('3. 第三层中间件 - 结束')
})

app.use(async (ctx) => {
  console.log('4. 处理请求')
  ctx.body = '东巴文'
})

// 执行顺序:
// 1. 第一层中间件 - 开始
// 2. 第二层中间件 - 开始
// 3. 第三层中间件 - 开始
// 4. 处理请求
// 3. 第三层中间件 - 结束
// 2. 第二层中间件 - 结束
// 1. 第一层中间件 - 结束

app.listen(3000)

计时中间件

app.use(async (ctx, next) => {
  const start = Date.now()
  
  await next()
  
  const duration = Date.now() - start
  ctx.set('X-Response-Time', `${duration}ms`)
  console.log(`${ctx.method} ${ctx.url} - ${duration}ms`)
})

错误处理中间件

app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    ctx.status = err.status || 500
    ctx.body = {
      error: {
        message: err.message,
        ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
      }
    }
    ctx.app.emit('error', err, ctx)
  }
})

// 全局错误事件
app.on('error', (err, ctx) => {
  console.error('全局错误:', err)
})

// 抛出错误
app.use(async (ctx) => {
  ctx.throw(400, '参数错误')
  // 或
  ctx.throw(401, '未授权', { code: 'AUTH_REQUIRED' })
})

路由

Koa 核心不包含路由功能,需要使用 @koa/router

const Koa = require('koa')
const Router = require('@koa/router')

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

// 基本路由
router.get('/', (ctx) => {
  ctx.body = '首页'
})

router.post('/users', (ctx) => {
  ctx.body = { message: '创建用户' }
})

router.put('/users/:id', (ctx) => {
  ctx.body = { userId: ctx.params.id }
})

router.delete('/users/:id', (ctx) => {
  ctx.status = 204
})

// 多个中间件
router.get('/protected',
  async (ctx, next) => {
    const token = ctx.get('Authorization')
    if (!token) ctx.throw(401, '未授权')
    await next()
  },
  (ctx) => {
    ctx.body = { message: '受保护的资源' }
  }
)

// 路由前缀
const apiRouter = new Router({ prefix: '/api' })

apiRouter.get('/users', (ctx) => {
  ctx.body = { users: [] }
})

// 嵌套路由
const usersRouter = new Router({ prefix: '/users' })

usersRouter.get('/', (ctx) => {
  ctx.body = { users: [] }
})

usersRouter.get('/:id', (ctx) => {
  ctx.body = { userId: ctx.params.id }
})

apiRouter.use(usersRouter.routes())

// 注册路由
app
  .use(router.routes())
  .use(router.allowedMethods())  // 自动处理 OPTIONS 请求
  .use(apiRouter.routes())
  .use(apiRouter.allowedMethods())

app.listen(3000)

路由参数

const router = new Router()

// 必需参数
router.get('/users/:id', (ctx) => {
  ctx.body = { id: ctx.params.id }
})

// 多个参数
router.get('/users/:userId/posts/:postId', (ctx) => {
  const { userId, postId } = ctx.params
  ctx.body = { userId, postId }
})

// 参数正则匹配
router.get('/files/:filename(\\w+\\.\\w+)', (ctx) => {
  ctx.body = { filename: ctx.params.filename }
})

// 查询参数
router.get('/search', (ctx) => {
  const { q, page = 1, limit = 10 } = ctx.query
  ctx.body = { query: q, page, limit }
})

请求体解析

const Koa = require('koa')
const { koaBody } = require('koa-body')

const app = new Koa()

app.use(koaBody({
  multipart: true,                    // 支持文件上传
  formidable: {
    maxFileSize: 10 * 1024 * 1024,    // 最大文件大小
    uploadDir: './uploads',           // 上传目录
    keepExtensions: true              // 保留扩展名
  },
  jsonLimit: '1mb',                   // JSON 大小限制
  formLimit: '1mb',                   // 表单大小限制
  textLimit: '1mb'                    // 文本大小限制
}))

// JSON 请求体
app.post('/api/json', (ctx) => {
  console.log(ctx.request.body)
  ctx.body = { received: ctx.request.body }
})

// 表单请求体
app.post('/api/form', (ctx) => {
  console.log(ctx.request.body)
  ctx.body = { received: ctx.request.body }
})

// 文件上传
app.post('/api/upload', (ctx) => {
  const file = ctx.request.files.file
  ctx.body = {
    filename: file.newFilename,
    originalFilename: file.originalFilename,
    size: file.size,
    mimetype: file.mimetype
  }
})

// 多文件上传
app.post('/api/uploads', (ctx) => {
  const files = ctx.request.files.files
  ctx.body = files.map(f => ({
    filename: f.newFilename,
    size: f.size
  }))
})

app.listen(3000)

静态文件服务

const Koa = require('koa')
const serve = require('koa-static')
const mount = require('koa-mount')

const app = new Koa()

// 静态文件服务
app.use(serve('./public'))

// 挂载到特定路径
app.use(mount('/static', serve('./static')))

// 配置选项
app.use(serve('./public', {
  maxage: 86400000,       // 缓存时间
  gzip: true,             // 启用 gzip
  index: 'index.html',    // 默认文件
  defer: true             // 延迟处理
}))

app.listen(3000)

Session 和 JWT 认证

Session 认证

const Koa = require('koa')
const session = require('koa-session')

const app = new Koa()

// Session 配置
app.keys = ['secret-key-1', 'secret-key-2']

app.use(session({
  key: 'koa:sess',           // Cookie 名称
  maxAge: 86400000,          // 过期时间
  overwrite: true,
  httpOnly: true,
  signed: true,
  rolling: false,
  renew: false
}, app))

// 登录
app.post('/login', (ctx) => {
  const { username, password } = ctx.request.body
  
  // 验证用户
  if (username === 'dongba' && password === 'password') {
    ctx.session.user = { username }
    ctx.body = { message: '登录成功' }
  } else {
    ctx.throw(401, '用户名或密码错误')
  }
})

// 获取用户信息
app.get('/profile', (ctx) => {
  if (!ctx.session.user) {
    ctx.throw(401, '未登录')
  }
  ctx.body = { user: ctx.session.user }
})

// 登出
app.post('/logout', (ctx) => {
  ctx.session = null
  ctx.body = { message: '已登出' }
})

app.listen(3000)

JWT 认证

const Koa = require('koa')
const jwt = require('koa-jwt')
const jsonwebtoken = require('jsonwebtoken')

const app = new Koa()
const SECRET = 'dongba-secret-key'

// 登录获取 token
app.post('/login', (ctx) => {
  const { username, password } = ctx.request.body
  
  if (username === 'dongba' && password === 'password') {
    const token = jsonwebtoken.sign(
      { username, id: 1 },
      SECRET,
      { expiresIn: '24h' }
    )
    ctx.body = { token }
  } else {
    ctx.throw(401, '认证失败')
  }
})

// JWT 中间件
app.use(jwt({ secret: SECRET }).unless({
  path: [/^\/public/, '/login']
}))

// 受保护的路由
app.get('/protected', (ctx) => {
  ctx.body = { 
    message: '受保护的资源',
    user: ctx.state.user 
  }
})

app.listen(3000)

数据库集成

MySQL

const Koa = require('koa')
const mysql = require('mysql2/promise')

const app = new Koa()

// 数据库连接池
const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'dongba_db',
  waitForConnections: true,
  connectionLimit: 10
})

// 数据库中间件
app.use(async (ctx, next) => {
  ctx.db = pool
  await next()
})

// 查询用户
app.get('/api/users', async (ctx) => {
  const [rows] = await ctx.db.query('SELECT * FROM users')
  ctx.body = { users: rows }
})

// 创建用户
app.post('/api/users', async (ctx) => {
  const { name, email } = ctx.request.body
  const [result] = await ctx.db.execute(
    'INSERT INTO users (name, email) VALUES (?, ?)',
    [name, email]
  )
  ctx.status = 201
  ctx.body = { id: result.insertId, name, email }
})

app.listen(3000)

MongoDB

const Koa = require('koa')
const { MongoClient } = require('mongodb')

const app = new Koa()
const MONGO_URL = 'mongodb://localhost:27017'
const DB_NAME = 'dongba_db'

let db

// 连接数据库
async function connectDB() {
  const client = new MongoClient(MONGO_URL)
  await client.connect()
  db = client.db(DB_NAME)
  console.log('数据库连接成功')
}

connectDB()

// 数据库中间件
app.use(async (ctx, next) => {
  ctx.db = db
  await next()
})

// 查询用户
app.get('/api/users', async (ctx) => {
  const users = await ctx.db.collection('users').find({}).toArray()
  ctx.body = { users }
})

// 创建用户
app.post('/api/users', async (ctx) => {
  const result = await ctx.db.collection('users').insertOne({
    ...ctx.request.body,
    createdAt: new Date()
  })
  ctx.status = 201
  ctx.body = { id: result.insertedId }
})

app.listen(3000)

完整项目示例

const Koa = require('koa')
const Router = require('@koa/router')
const { koaBody } = require('koa-body')
const serve = require('koa-static')
const logger = require('koa-logger')
const helmet = require('koa-helmet')
const cors = require('@koa/cors')

const app = new Koa()
const router = new Router({ prefix: '/api' })

// 安全中间件
app.use(helmet())

// CORS
app.use(cors({
  origin: 'https://db-w.cn',
  credentials: true
}))

// 日志
app.use(logger())

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

// 静态文件
app.use(serve('./public'))

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

// 模拟数据
let users = [
  { id: 1, name: '东巴文', email: 'dongba@example.com' }
]
let nextId = 2

// 路由
router
  .get('/users', (ctx) => {
    ctx.body = { users }
  })
  .get('/users/:id', (ctx) => {
    const user = users.find(u => u.id === parseInt(ctx.params.id))
    if (!user) ctx.throw(404, '用户不存在')
    ctx.body = user
  })
  .post('/users', (ctx) => {
    const { name, email } = ctx.request.body
    if (!name || !email) ctx.throw(400, '参数不完整')
    
    const user = { id: nextId++, name, email }
    users.push(user)
    
    ctx.status = 201
    ctx.body = user
  })
  .put('/users/:id', (ctx) => {
    const index = users.findIndex(u => u.id === parseInt(ctx.params.id))
    if (index === -1) ctx.throw(404, '用户不存在')
    
    users[index] = { ...users[index], ...ctx.request.body }
    ctx.body = users[index]
  })
  .delete('/users/:id', (ctx) => {
    const index = users.findIndex(u => u.id === parseInt(ctx.params.id))
    if (index === -1) ctx.throw(404, '用户不存在')
    
    users.splice(index, 1)
    ctx.status = 204
  })

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

// 404 处理
app.use((ctx) => {
  ctx.status = 404
  ctx.body = { error: '未找到资源' }
})

// 启动
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
  console.log(`服务器运行在 http://localhost:${PORT}`)
})

下一步

掌握 Koa 后,你可以继续学习:


东巴文(db-w.cn)—— 让 Koa 开发更优雅