Koa上下文库详解
1. Koa上下文概述
在Koa框架中,上下文(Context)是请求处理过程中的核心对象,它封装了Node.js原生的request和response对象,并提供了一系列便捷的方法和属性,使开发者能够更优雅地处理HTTP请求和响应。本章将深入探讨Koa上下文的设计理念、核心功能和高级用法。
1.1 上下文的设计理念
Koa的上下文设计遵循了"洋葱模型"和"组合优于继承"的原则,具有以下特点:
- 简洁性:上下文对象提供了简洁明了的API,隐藏了Node.js原生API的复杂性
- 组合性:上下文对象通过委托而非继承的方式组合了request和response对象
- 扩展性:允许开发者通过中间件扩展上下文对象的功能
- 上下文隔离:每个请求都有独立的上下文对象,确保请求之间互不干扰
1.2 上下文对象的创建过程
在Koa应用中,每当收到一个HTTP请求时,Koa会创建一个新的上下文对象。这个过程主要包括以下步骤:
- 创建context、request和response对象
- 将request和response对象挂载到context上
- 将应用实例app挂载到context上
- 通过中间件链传递上下文对象
- 请求处理完成后,上下文对象被垃圾回收
下面是Koa源码中创建上下文的核心代码片段:
// Koa源码简化版
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
}
2. 上下文核心对象
2.1 Context对象
Context对象(ctx)是Koa中最常用的对象,它作为中间件函数的第一个参数传递,封装了请求和响应的所有信息。
2.1.1 常用属性
ctx.app:应用实例的引用ctx.req:Node.js原生的request对象ctx.res:Node.js原生的response对象ctx.request:Koa封装的request对象ctx.response:Koa封装的response对象ctx.state:推荐的命名空间,用于在中间件间传递数据ctx.cookies:Cookie操作对象ctx.throw():抛出错误的方法ctx.assert():断言测试方法
2.1.2 常用方法
// 抛出错误
ctx.throw(400, 'Bad Request');
// 断言测试
ctx.assert(ctx.request.accepts('json'), 406, 'json only');
// 重定向
ctx.redirect('/login');
ctx.redirect('https://example.com');
// 检测是否为XMLHttpRequest请求
const isAjax = ctx.accepts('json') && ctx.request.get('X-Requested-With') === 'XMLHttpRequest';
2.2 Request对象
Request对象(ctx.request)是Koa对Node.js原生request对象的封装,提供了更便捷的请求信息访问方法。
2.2.1 常用属性
request.header/request.headers:请求头对象request.method:HTTP方法(GET, POST等)request.url:请求URLrequest.originalUrl:原始请求URLrequest.href:完整的请求URL(包含协议、主机和路径)request.path:请求路径request.query:查询字符串对象request.querystring:原始查询字符串request.search:带?的查询字符串request.host:主机名(包含端口)request.hostname:主机名(不包含端口)request.origin:请求源(协议+主机)request.protocol:请求协议(http或https)request.ip:客户端IP地址request.ips:客户端IP地址列表(如果经过代理)request.subdomains:子域名数组request.isSecure:是否为HTTPS请求request.body:请求体(需要使用koa-bodyparser中间件)
2.2.2 常用方法
// 获取指定请求头
const contentType = ctx.request.get('Content-Type');
const userAgent = ctx.request.get('User-Agent');
// 检查是否接受指定的内容类型
const acceptsJson = ctx.request.accepts('json');
const acceptsHTMLorText = ctx.request.accepts(['html', 'text']);
// 检查请求类型
const isJson = ctx.request.is('json');
const isText = ctx.request.is('text/plain');
// 获取路径参数(需要使用路由中间件如koa-router)
const userId = ctx.params.id;
// 获取Cookie(也可以通过ctx.cookies.get获取)
const sessionId = ctx.cookies.get('sessionId');
2.3 Response对象
Response对象(ctx.response)是Koa对Node.js原生response对象的封装,提供了更便捷的响应操作方法。
2.3.1 常用属性
response.header/response.headers:响应头对象response.status:HTTP状态码response.message:HTTP状态消息response.body:响应体response.length:响应体长度response.type:响应内容类型response.isSent:响应是否已发送response.redirect:重定向URL
2.3.2 常用方法
// 设置响应头
ctx.response.set('Content-Type', 'application/json');
ctx.response.set({
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
});
// 删除响应头
ctx.response.remove('X-Powered-By');
// 设置状态码
ctx.response.status = 201;
// 设置响应体
ctx.response.body = { success: true, data: user };
ctx.response.body = 'Hello World';
ctx.response.body = fs.createReadStream('index.html');
// 设置内容类型
ctx.response.type = 'json';
ctx.response.type = 'text/html';
ctx.response.type = '.png'; // Koa会自动映射为image/png
// 重定向
ctx.response.redirect('/login');
ctx.response.redirect(301, 'https://example.com');
// 设置Cookie
ctx.cookies.set('token', 'abc123', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 86400000 // 24小时
});
3. Context对象的委托机制
Koa的上下文对象使用了委托(delegation)模式,将一些常用的属性和方法从request和response对象委托到context对象上,使API更加简洁和易用。
3.1 委托原理
Koa内部使用了delegates库来实现委托机制。通过委托,我们可以直接在ctx上访问ctx.request和ctx.response的属性和方法。
以下是一些常用的委托属性和方法:
// 从request委托的属性
ctx.header = ctx.request.header
ctx.method = ctx.request.method
ctx.url = ctx.request.url
ctx.path = ctx.request.path
ctx.query = ctx.request.query
ctx.querystring = ctx.request.querystring
ctx.host = ctx.request.host
ctx.hostname = ctx.request.hostname
ctx.protocol = ctx.request.protocol
ctx.ip = ctx.request.ip
// 从request委托的方法
ctx.get = ctx.request.get
ctx.accepts = ctx.request.accepts
ctx.acceptsEncodings = ctx.request.acceptsEncodings
ctx.acceptsCharsets = ctx.request.acceptsCharsets
ctx.acceptsLanguages = ctx.request.acceptsLanguages
ctx.is = ctx.request.is
// 从response委托的属性
ctx.body = ctx.response.body
ctx.status = ctx.response.status
ctx.message = ctx.response.message
ctx.length = ctx.response.length
ctx.type = ctx.response.type
// 从response委托的方法
ctx.set = ctx.response.set
ctx.append = ctx.response.append
ctx.remove = ctx.response.remove
ctx.redirect = ctx.response.redirect
ctx.attachment = ctx.response.attachment
ctx.vary = ctx.response.vary
3.2 实际应用示例
委托机制使我们可以编写更加简洁的代码:
// 不使用委托
app.use(async (ctx, next) => {
ctx.response.status = 200;
ctx.response.body = {
method: ctx.request.method,
path: ctx.request.path,
query: ctx.request.query
};
});
// 使用委托(更简洁)
app.use(async (ctx, next) => {
ctx.status = 200;
ctx.body = {
method: ctx.method,
path: ctx.path,
query: ctx.query
};
});
4. 上下文扩展
Koa允许开发者扩展上下文对象,添加自定义的属性和方法,以满足特定的业务需求。
4.1 通过app.context扩展
最常用的扩展方式是通过app.context对象添加属性和方法,这些扩展会应用到每个请求的上下文对象上。
const Koa = require('koa');
const app = new Koa();
// 添加自定义属性
app.context.db = () => {
// 假设这是数据库连接
return dbConnection;
};
// 添加自定义方法
app.context.render = async function(template, data) {
// 简单的模板渲染实现
const html = await templateEngine.render(template, data);
this.type = 'text/html';
this.body = html;
};
// 添加格式化响应的方法
app.context.success = function(data, message = 'Success') {
this.status = 200;
this.type = 'application/json';
this.body = {
code: 0,
message,
data
};
};
app.context.fail = function(code = 1, message = 'Failed', status = 400) {
this.status = status;
this.type = 'application/json';
this.body = {
code,
message,
data: null
};
};
// 在中间件中使用扩展的方法
app.use(async (ctx) => {
try {
const db = ctx.db();
const user = await db.collection('users').findOne({ id: ctx.params.id });
if (!user) {
ctx.fail(404, 'User not found', 404);
return;
}
ctx.success(user);
} catch (error) {
ctx.fail(500, error.message, 500);
}
});
// 使用render方法
app.use(async (ctx) => {
await ctx.render('index', { title: 'Koa App', user: ctx.state.user });
});
4.2 通过中间件扩展
除了通过app.context扩展,我们还可以通过中间件动态地为每个请求的上下文对象添加属性和方法。
// 用户认证中间件
const authenticate = async (ctx, next) => {
const token = ctx.cookies.get('token') || ctx.get('Authorization');
if (token) {
try {
// 验证token并获取用户信息
const user = await verifyToken(token);
// 将用户信息添加到上下文
ctx.user = user;
ctx.isAuthenticated = true;
} catch (error) {
ctx.isAuthenticated = false;
}
} else {
ctx.isAuthenticated = false;
}
await next();
};
// 请求日志中间件
const requestLogger = async (ctx, next) => {
const start = Date.now();
// 添加日志方法
ctx.log = (level, message, meta = {}) => {
console.log(`[${level.toUpperCase()}] ${message}`, {
requestId: ctx.state.requestId,
...meta
});
};
await next();
const ms = Date.now() - start;
ctx.log('info', `${ctx.method} ${ctx.url} - ${ms}ms`);
};
// 应用中间件
app.use(authenticate);
app.use(requestLogger);
// 在路由中使用扩展的属性和方法
app.use(async (ctx) => {
if (ctx.isAuthenticated) {
ctx.log('info', `Authenticated user: ${ctx.user.username}`);
ctx.body = `Welcome, ${ctx.user.username}!`;
} else {
ctx.log('warn', 'Unauthenticated access attempt');
ctx.body = 'Please login first';
}
});
4.3 模块化扩展
对于大型应用,我们可以将上下文扩展封装成模块,以提高代码的可维护性。
// extensions/response.js
module.exports = {
success(data, message = 'Success') {
this.status = 200;
this.type = 'application/json';
this.body = {
code: 0,
message,
data
};
},
fail(code = 1, message = 'Failed', status = 400) {
this.status = status;
this.type = 'application/json';
this.body = {
code,
message,
data: null
};
},
created(data, message = 'Created') {
this.status = 201;
this.type = 'application/json';
this.body = {
code: 0,
message,
data
};
},
noContent(message = 'No Content') {
this.status = 204;
this.body = null;
}
};
// extensions/context.js
module.exports = (app) => {
// 加载响应扩展
const responseExtensions = require('./response');
// 添加响应辅助方法
Object.keys(responseExtensions).forEach(method => {
app.context[method] = responseExtensions[method];
});
// 添加数据库访问方法
app.context.db = function() {
return app.database; // 假设app.database是数据库连接
};
// 添加缓存方法
app.context.cache = function(key, value, ttl) {
if (value === undefined) {
// 获取缓存
return app.redis.get(key);
} else {
// 设置缓存
return app.redis.set(key, JSON.stringify(value), 'EX', ttl || 3600);
}
};
};
// app.js
const Koa = require('koa');
const app = new Koa();
// 初始化数据库和Redis连接
app.database = require('./config/database');
app.redis = require('./config/redis');
// 应用上下文扩展
require('./extensions/context')(app);
// 在中间件中使用扩展
app.use(async (ctx) => {
try {
// 尝试从缓存获取数据
let users = await ctx.cache('users');
if (!users) {
// 缓存未命中,从数据库获取
const db = ctx.db();
users = await db.collection('users').find().toArray();
// 缓存数据
await ctx.cache('users', users, 3600);
} else {
// 解析缓存数据
users = JSON.parse(users);
}
ctx.success(users);
} catch (error) {
ctx.fail(500, error.message, 500);
}
});
5. ctx.state的使用
ctx.state是Koa推荐的命名空间,用于在中间件之间传递数据。它是一个普通的JavaScript对象,可以安全地添加任何属性。
5.1 基本用法
// 请求ID中间件
app.use(async (ctx, next) => {
ctx.state.requestId = uuidv4(); // 生成唯一请求ID
await next();
});
// 用户认证中间件
app.use(async (ctx, next) => {
// 验证用户身份
const user = await authenticateUser(ctx);
if (user) {
ctx.state.user = user; // 存储用户信息
ctx.state.isAuthenticated = true;
}
await next();
});
// 权限检查中间件
app.use(async (ctx, next) => {
if (ctx.path.startsWith('/admin') && (!ctx.state.user || !ctx.state.user.isAdmin)) {
ctx.status = 403;
ctx.body = 'Forbidden';
return;
}
await next();
});
// 日志中间件
app.use(async (ctx, next) => {
await next();
// 使用之前中间件存储在state中的数据
logger.info({
requestId: ctx.state.requestId,
userId: ctx.state.user ? ctx.state.user.id : null,
path: ctx.path,
method: ctx.method,
status: ctx.status,
responseTime: ctx.responseTime
});
});
5.2 高级用法:状态管理中间件
对于复杂应用,我们可以创建专门的状态管理中间件来管理ctx.state:
// 状态管理中间件
const createStateManager = () => {
const managers = {};
// 注册状态管理器
const register = (name, manager) => {
managers[name] = manager;
};
// 获取状态管理器
const get = (name) => {
return managers[name];
};
// 中间件函数
const middleware = async (ctx, next) => {
// 为每个请求创建新的state对象
ctx.state = {};
// 初始化所有状态管理器
Object.entries(managers).forEach(([name, manager]) => {
if (typeof manager.init === 'function') {
ctx.state[name] = manager.init(ctx);
}
});
try {
await next();
} finally {
// 请求处理完成后的清理工作
Object.entries(managers).forEach(([name, manager]) => {
if (typeof manager.cleanup === 'function') {
manager.cleanup(ctx.state[name], ctx);
}
});
}
};
return { register, get, middleware };
};
// 创建状态管理器
const stateManager = createStateManager();
// 注册用户状态管理器
stateManager.register('user', {
init: (ctx) => {
return {
id: null,
name: null,
role: null,
isAuthenticated: false
};
},
cleanup: (state, ctx) => {
// 清理用户状态,例如删除敏感信息
if (state && state.token) {
delete state.token;
}
}
});
// 注册请求状态管理器
stateManager.register('request', {
init: (ctx) => {
return {
id: uuidv4(),
startTime: Date.now(),
startTimeString: new Date().toISOString()
};
},
cleanup: (state, ctx) => {
// 计算响应时间
state.responseTime = Date.now() - state.startTime;
// 记录请求日志
logger.info({
requestId: state.id,
startTime: state.startTimeString,
responseTime: state.responseTime,
path: ctx.path,
method: ctx.method,
status: ctx.status
});
}
});
// 应用状态管理中间件
app.use(stateManager.middleware);
// 用户认证中间件
app.use(async (ctx, next) => {
const token = ctx.cookies.get('token');
if (token) {
try {
const userInfo = verifyToken(token);
ctx.state.user = {
id: userInfo.id,
name: userInfo.name,
role: userInfo.role,
isAuthenticated: true
};
} catch (error) {
// token验证失败,保持默认用户状态
}
}
await next();
});
// 路由处理函数
app.use(async (ctx) => {
ctx.body = {
requestId: ctx.state.request.requestId,
user: ctx.state.user,
message: 'Hello World'
};
});
6. Cookie和Session管理
6.1 Cookie操作
Koa提供了便捷的Cookie操作API,通过ctx.cookies对象进行操作:
// 设置Cookie
ctx.cookies.set('name', 'value', {
domain: 'example.com', // 域名
path: '/', // 路径
maxAge: 86400000, // 最大过期时间(毫秒)
expires: new Date('2023-12-31'), // 过期日期
httpOnly: true, // 仅HTTP,防止XSS攻击
secure: false, // 仅HTTPS
sameSite: 'lax', // 防止CSRF攻击
signed: true, // 签名Cookie
overwrite: false // 是否覆盖同名Cookie
});
// 获取Cookie
const value = ctx.cookies.get('name', { signed: true });
// 删除Cookie
ctx.cookies.set('name', '', { maxAge: 0 });
6.2 Session管理
Koa本身不提供Session功能,但可以通过第三方中间件如koa-session来实现:
const Koa = require('koa');
const session = require('koa-session');
const app = new Koa();
// 配置Session
app.keys = ['your-session-secret-key'];
const sessionConfig = {
key: 'koa.sess', // Cookie键名
maxAge: 86400000, // 会话过期时间(毫秒)
autoCommit: true, // 自动提交到响应头
overwrite: true, // 是否覆盖
httpOnly: true, // 仅HTTP
signed: true, // 签名
rolling: false, // 每次请求都重置过期时间
renew: false, // 快过期时自动续订
secure: false, // 仅HTTPS
sameSite: null, // 跨站策略
};
// 应用Session中间件
app.use(session(sessionConfig, app));
// 使用Session
app.use(async (ctx, next) => {
// 访问Session
if (ctx.session.views) {
ctx.session.views++;
} else {
ctx.session.views = 1;
}
// 存储用户信息到Session
if (ctx.state.user) {
ctx.session.user = ctx.state.user;
}
ctx.body = `Views: ${ctx.session.views}`;
await next();
});
// 销毁Session(登出)
app.use(async (ctx) => {
if (ctx.path === '/logout') {
ctx.session = null; // 销毁Session
ctx.redirect('/login');
}
});
6.3 使用Redis存储Session
对于生产环境,通常会使用Redis等外部存储来保存Session,以提高性能和可靠性:
const Koa = require('koa');
const session = require('koa-session');
const Redis = require('ioredis');
const RedisStore = require('koa-session-ioredis');
const app = new Koa();
app.keys = ['your-session-secret-key'];
// 创建Redis连接
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379
});
// 配置Redis存储
const redisStore = new RedisStore({
client: redis,
keyPrefix: 'koa:sess:', // Redis键前缀
ttl: 86400, // 过期时间(秒)
});
// 应用Session中间件
app.use(session({
key: 'koa.sess',
store: redisStore,
maxAge: 86400000,
httpOnly: true,
signed: true
}, app));
7. 请求和响应的完整生命周期
了解Koa上下文在请求和响应生命周期中的变化,有助于更好地理解和使用Koa框架。
7.1 请求处理流程
- 请求接收:Koa应用接收到HTTP请求
- 上下文创建:Koa创建新的context、request和response对象
- 中间件执行:按照洋葱模型依次执行中间件
- 进入中间件,执行
await next()之前的代码 - 递归执行下一个中间件
- 所有中间件执行完成后,返回当前中间件,执行
await next()之后的代码
- 进入中间件,执行
- 响应发送:将
ctx.body作为响应体发送给客户端 - 上下文销毁:请求处理完成后,上下文对象被垃圾回收
7.2 实际案例:请求日志中间件
下面是一个记录请求完整生命周期的中间件示例:
// 完整的请求日志中间件
const requestLogger = async (ctx, next) => {
// 请求开始时间
const startTime = Date.now();
const startHrTime = process.hrtime();
// 记录请求信息
console.log('----------------------------------------');
console.log(`[${new Date().toISOString()}] ${ctx.method} ${ctx.url}`);
console.log('Headers:', ctx.headers);
console.log('Query:', ctx.query);
console.log('Body:', ctx.request.body);
try {
// 执行后续中间件
await next();
// 计算响应时间
const ms = Date.now() - startTime;
const hrTime = process.hrtime(startHrTime);
const preciseMs = hrTime[0] * 1000 + hrTime[1] / 1000000;
// 记录响应信息
console.log(`Response Status: ${ctx.status}`);
console.log('Response Headers:', ctx.response.headers);
console.log(`Response Time: ${ms}ms (${preciseMs.toFixed(3)}ms precise)`);
} catch (error) {
// 记录错误信息
const ms = Date.now() - startTime;
console.error(`Request failed after ${ms}ms`);
console.error('Error:', error.message);
console.error('Stack:', error.stack);
// 重新抛出错误,让错误处理中间件处理
throw error;
} finally {
console.log('----------------------------------------');
}
};
app.use(requestLogger);
8. 上下文最佳实践
8.1 命名约定
- 使用
ctx作为上下文对象的变量名(Koa社区惯例) - 使用
ctx.state存储中间件间共享的数据 - 为扩展的上下文方法使用清晰、描述性的名称
8.2 性能优化
- 避免在中间件中频繁访问
ctx的深层属性,可以将常用属性缓存到局部变量中 - 不要在
ctx上存储大量数据,这可能导致内存泄漏 - 对于需要在多个中间件间共享的复杂对象,考虑使用引用而非复制
// 优化前:多次访问ctx.state.user
app.use(async (ctx, next) => {
if (ctx.state.user && ctx.state.user.isAdmin && ctx.state.user.active) {
// 执行管理员操作
console.log(`Admin ${ctx.state.user.name} is performing action`);
}
await next();
});
// 优化后:缓存到局部变量
app.use(async (ctx, next) => {
const user = ctx.state.user;
if (user && user.isAdmin && user.active) {
// 执行管理员操作
console.log(`Admin ${user.name} is performing action`);
}
await next();
});
8.3 错误处理
- 使用
ctx.throw()抛出HTTP错误 - 使用
ctx.assert()进行断言测试 - 为不同类型的错误设置合适的HTTP状态码
// 抛出不同类型的错误
app.use(async (ctx) => {
// 客户端错误
if (!ctx.query.id) {
ctx.throw(400, 'Missing required parameter: id');
}
// 未授权错误
if (!ctx.state.user) {
ctx.throw(401, 'Authentication required');
}
// 禁止访问错误
if (!ctx.state.user.isAdmin) {
ctx.throw(403, 'Insufficient permissions');
}
// 资源不存在错误
const resource = await getResource(ctx.query.id);
if (!resource) {
ctx.throw(404, `Resource with id ${ctx.query.id} not found`);
}
// 使用断言
ctx.assert(ctx.query.type, 400, 'Type parameter is required');
ctx.assert(['image', 'video', 'audio'].includes(ctx.query.type), 400, 'Invalid type parameter');
ctx.body = resource;
});
8.4 安全性考虑
- 不要在
ctx.body中泄露敏感信息 - 使用
httpOnly和secure选项保护Cookie - 对用户输入进行验证和清理,防止XSS和CSRF攻击
- 实现适当的访问控制,基于
ctx.state.user信息
// 安全的响应处理
app.use(async (ctx) => {
const user = await getUser(ctx.params.id);
if (user) {
// 返回安全的用户信息,不包含敏感数据
ctx.body = {
id: user.id,
name: user.name,
email: user.email,
// 不返回密码、token等敏感信息
// 只返回与当前请求相关的字段
};
} else {
ctx.status = 404;
ctx.body = { error: 'User not found' };
}
});
// 设置安全的Cookie
app.use(async (ctx) => {
// 生产环境下使用secure=true
const isSecure = process.env.NODE_ENV === 'production';
ctx.cookies.set('sessionId', generateSessionId(), {
httpOnly: true, // 防止JavaScript访问
secure: isSecure, // 仅HTTPS
sameSite: 'lax', // 防止CSRF攻击
maxAge: 86400000
});
});
9. 实战案例:构建RESTful API
9.1 项目概述
在这个实战案例中,我们将使用Koa上下文的各种功能构建一个完整的RESTful API,包括用户认证、资源管理、错误处理等功能。
9.2 技术栈
- Koa 2.x
- koa-router
- koa-bodyparser
- koa-jwt
- bcrypt
- Joi
- MongoDB + Mongoose
9.3 项目结构
koa-rest-api/
├── app.js
├── package.json
├── config/
│ └── db.js
├── middlewares/
│ ├── auth.js
│ ├── errorHandler.js
│ └── validator.js
├── models/
│ └── User.js
├── routes/
│ ├── auth.js
│ └── users.js
└── utils/
└── response.js
9.4 核心代码实现
1. 应用入口
// app.js
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const dotenv = require('dotenv');
const connectDB = require('./config/db');
// 加载环境变量
dotenv.config();
// 连接数据库
connectDB();
// 创建应用
const app = new Koa();
// 扩展上下文
app.context.success = function(data, message = 'Success') {
this.status = 200;
this.body = {
code: 0,
message,
data
};
};
app.context.fail = function(code = 1, message = 'Failed', status = 400) {
this.status = status;
this.body = {
code,
message,
data: null
};
};
// 应用中间件
app.use(bodyParser());
app.use(require('./middlewares/errorHandler'));
// 应用路由
app.use(require('./routes/auth').routes());
app.use(require('./routes/users').routes());
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
2. 错误处理中间件
// middlewares/errorHandler.js
module.exports = async (ctx, next) => {
try {
await next();
// 处理404错误
if (ctx.status === 404) {
ctx.fail(404, 'Resource not found', 404);
}
} catch (error) {
// 记录错误日志
console.error('Error:', error);
// 处理已知错误类型
if (error.name === 'ValidationError') {
ctx.fail(400, error.message, 400);
} else if (error.name === 'UnauthorizedError') {
ctx.fail(401, 'Authentication failed', 401);
} else if (error.status) {
ctx.fail(error.status, error.message, error.status);
} else {
// 处理未知错误
ctx.fail(500, 'Internal server error', 500);
}
}
};
3. 认证中间件
// middlewares/auth.js
const jwt = require('koa-jwt');
// JWT认证中间件
const authenticate = jwt({
secret: process.env.JWT_SECRET,
algorithms: ['HS256'],
getToken: (ctx) => {
if (ctx.headers.authorization && ctx.headers.authorization.split(' ')[0] === 'Bearer') {
return ctx.headers.authorization.split(' ')[1];
}
return null;
}
});
// 错误处理包装器
const handleAuthError = (ctx, next) => {
return next().catch((err) => {
if (err.name === 'UnauthorizedError') {
ctx.fail(401, 'Invalid or expired token', 401);
} else {
throw err;
}
});
};
module.exports = { authenticate, handleAuthError };
4. 数据验证中间件
// middlewares/validator.js
const Joi = require('joi');
const validator = (schema) => {
return async (ctx, next) => {
try {
await schema.validateAsync(ctx.request.body);
await next();
} catch (error) {
ctx.fail(400, error.details[0].message, 400);
}
};
};
module.exports = validator;
5. 用户模型
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true
},
password: {
type: String,
required: true,
minlength: 6,
select: false
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
createdAt: {
type: Date,
default: Date.now
}
});
// 密码加密中间件
userSchema.pre('save', async function(next) {
if (this.isModified('password')) {
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
}
next();
});
// 验证密码方法
userSchema.methods.matchPassword = async function(enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
// 隐藏密码方法
userSchema.methods.toJSON = function() {
const user = this.toObject();
delete user.password;
return user;
};
module.exports = mongoose.model('User', userSchema);
6. 认证路由
// routes/auth.js
const Router = require('koa-router');
const Joi = require('joi');
const User = require('../models/User');
const jwt = require('jsonwebtoken');
const validator = require('../middlewares/validator');
const router = new Router({ prefix: '/api/auth' });
// 验证模式
const registerSchema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required()
});
const loginSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required()
});
// 用户注册
router.post('/register', validator(registerSchema), async (ctx) => {
const { name, email, password } = ctx.request.body;
// 检查用户是否已存在
const userExists = await User.findOne({ email });
if (userExists) {
ctx.fail(400, 'User with this email already exists', 400);
return;
}
// 创建新用户
const user = await User.create({ name, email, password });
// 生成JWT token
const token = jwt.sign({
id: user._id,
role: user.role
}, process.env.JWT_SECRET, {
expiresIn: '30d'
});
ctx.success({ user, token }, 'User registered successfully');
});
// 用户登录
router.post('/login', validator(loginSchema), async (ctx) => {
const { email, password } = ctx.request.body;
// 查找用户
const user = await User.findOne({ email }).select('+password');
if (!user || !(await user.matchPassword(password))) {
ctx.fail(401, 'Invalid email or password', 401);
return;
}
// 生成JWT token
const token = jwt.sign({
id: user._id,
role: user.role
}, process.env.JWT_SECRET, {
expiresIn: '30d'
});
ctx.success({ user, token }, 'Login successful');
});
module.exports = router;
7. 用户路由
// routes/users.js
const Router = require('koa-router');
const User = require('../models/User');
const { authenticate, handleAuthError } = require('../middlewares/auth');
const router = new Router({ prefix: '/api/users' });
// 获取当前用户信息
router.get('/me', handleAuthError, authenticate, async (ctx) => {
const user = await User.findById(ctx.state.user.id);
ctx.success(user);
});
// 更新当前用户信息
router.put('/me', handleAuthError, authenticate, async (ctx) => {
const updates = ctx.request.body;
// 不允许更新的字段
delete updates.role;
delete updates.password;
const user = await User.findByIdAndUpdate(ctx.state.user.id, updates, {
new: true,
runValidators: true
});
ctx.success(user, 'User updated successfully');
});
// 获取用户列表(管理员)
router.get('/', handleAuthError, authenticate, async (ctx) => {
// 检查是否为管理员
if (ctx.state.user.role !== 'admin') {
ctx.fail(403, 'Access denied', 403);
return;
}
const users = await User.find();
ctx.success(users);
});
module.exports = router;
9.5 测试API
使用curl或Postman测试API的各个端点:
- 用户注册:POST /api/auth/register
- 用户登录:POST /api/auth/login
- 获取当前用户信息:GET /api/users/me (需要认证)
- 更新当前用户信息:PUT /api/users/me (需要认证)
- 获取用户列表:GET /api/users (需要管理员权限)
10. 总结与进阶建议
Koa的上下文系统是框架的核心部分,它通过简洁而强大的API使HTTP请求和响应处理变得更加优雅和高效。本章深入探讨了Koa上下文的设计理念、核心对象、委托机制、扩展方法以及最佳实践。
最佳实践回顾
- 利用上下文委托机制编写简洁的代码
- 使用
ctx.state在中间件间安全地共享数据 - 合理扩展上下文对象,添加自定义方法
- 遵循命名约定和性能优化原则
- 重视安全性,保护敏感数据
- 实现统一的错误处理机制
进阶学习建议
- 深入研究Koa源码,了解上下文的实现细节
- 学习如何编写高性能的Koa中间件
- 探索Koa生态系统中的其他上下文相关库
- 研究大型Koa应用中的上下文管理模式
- 比较Koa与其他Node.js框架的上下文设计
- 学习在微服务架构中使用Koa上下文
通过掌握Koa上下文的高级用法,你将能够构建更加灵活、高效和可维护的Node.js Web应用,为用户提供更好的服务体验。