Koa 是由 Express 原班人马打造的下一代 Web 框架,更轻量、更现代,充分利用了 async/await 语法。
| 特性 | 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}`)
})
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)
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)
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)
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)
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)
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 开发更优雅