← 回到列表
Koa2 + MySQL + Redis + UniApp 项目开发指南
这一页
Koa2 + MySQL + Redis + UniApp 项目开发指南
技术栈:Koa2 (Node.js) + MySQL + Redis + UniApp,适合中小型全栈项目开发。本文档覆盖从环境搭建、项目初始化、数据库设计、前后端联调到部署上线的全流程。
一、技术栈概述
1.1 技术选型
| 层级 | 技术 | 版本建议 | 说明 |
|---|---|---|---|
| 后端框架 | Koa2 | ^2.15.x | 轻量级 Node.js 框架,洋葱圈中间件模型 |
| 运行环境 | Node.js | >= 18 LTS | 推荐使用 LTS 版本 |
| 数据库 | MySQL | 8.0+ | 关系型数据库,存储业务数据 |
| 缓存 | Redis | 6.x / 7.x | 缓存、Session、限流、队列 |
| ORM | Sequelize | ^6.x | 支持 MySQL 的 Node.js ORM |
| 前端 | UniApp | 最新版 | 一套代码多端运行(小程序/H5/App) |
| 包管理 | pnpm / npm | - | 推荐 pnpm |
1.2 Koa2 核心特性
请求进入 │ ▼ ┌─── Middleware 1 (外层) ───┐ │ ┌── Middleware 2 ──┐ │ │ │ ┌─ Middleware 3 ─┐ │ │ │ │ Controller │ │ │ │ └───────────────┘ │ │ └─────────────────────┘ │ └────────────────────────────┘ │ ▼ 响应返回(洋葱模型)- 中间件机制:Koa 的核心是中间件,采用洋葱圈模型(
await next()) - 轻量简洁:核心代码不到 2000 行,不捆绑任何中间件
- Async/Await:原生支持异步,彻底解决回调地狱
- Context 上下文:将
request和response封装到ctx中
1.3 Koa vs Express 对比
| 对比项 | Koa2 | Express |
|---|---|---|
| 中间件模型 | 洋葱圈(支持 await next()) |
线性链式 |
| 异步处理 | 原生 async/await | 回调 / Promise |
| 核心体积 | 极小(不含路由、静态文件等) | 稍大 |
| 错误处理 | 可统一 try/catch 捕获 |
需专门的 error middleware |
| 社区生态 | 偏小,需自行组合中间件 | 极其丰富 |
| 自由度 | 高,灵活组合 | 约定稍多 |
| 适用场景 | API 服务、微服务 | 全栈应用 |
二、环境搭建与项目初始化
2.1 环境准备
# 1. 安装 Node.js (推荐使用 nvm 管理版本)# 下载地址:https://nodejs.org/# 2. 验证安装node -v # >= 18.xnpm -v # >= 9.x# 3. 安装 pnpm(可选但推荐)npm install -g pnpm# 4. 安装 MySQL 8.0+# 下载地址:https://dev.mysql.com/downloads/mysql/# 5. 安装 Redis# Windows: 使用 WSL 或 https://github.com/microsoftarchive/redis/releases# Mac: brew install redis# Linux: apt install redis-server / yum install redis2.2 初始化 Koa2 项目
# 创建项目目录mkdir koa2-uniapp-democd koa2-uniapp-demo# 初始化 package.jsonpnpm init# 安装核心依赖pnpm add koa koa-router koa-body koa-cors jsonwebtoken bcryptjspnpm add sequelize mysql2 redis ioredis dotenvpnpm add joi # 参数校验# 安装开发依赖pnpm add -D nodemon2.3 项目目录结构
koa2-uniapp-demo/├── src/│ ├── app.js # 应用入口│ ├── config/│ │ └── index.js # 配置文件│ ├── middleware/│ │ ├── auth.js # JWT 鉴权中间件│ │ ├── errorHandler.js # 全局错误处理│ │ └── validator.js # 参数校验中间件│ ├── routes/│ │ ├── index.js # 路由汇总│ │ ├── user.js # 用户模块路由│ │ └── article.js # 文章模块路由│ ├── models/│ │ ├── index.js # 数据库连接初始化│ │ ├── User.js # 用户模型│ │ └── Article.js # 文章模型│ ├── services/│ │ ├── userService.js # 用户业务逻辑│ │ └── articleService.js # 文章业务逻辑│ ├── controllers/│ │ ├── userController.js # 用户控制器│ │ └── articleController.js│ ├── utils/│ │ ├── jwt.js # JWT 工具函数│ │ ├── redis.js # Redis 连接与工具│ │ └── response.js # 统一响应格式│ └── validators/│ ├── user.js # 用户参数校验规则│ └── article.js├── .env # 环境变量├── .env.example # 环境变量模板└── package.json三、后端核心配置
3.1 环境变量(.env)
# 应用配置PORT=3000NODE_ENV=development# MySQL 配置DB_HOST=localhostDB_PORT=3306DB_USER=rootDB_PASSWORD=your_passwordDB_NAME=koa2_demo# Redis 配置REDIS_HOST=127.0.0.1REDIS_PORT=6379REDIS_PASSWORD=REDIS_DB=0# JWT 配置JWT_SECRET=your_jwt_secret_keyJWT_EXPIRES_IN=7d3.2 配置文件(src/config/index.js)
require('dotenv').config();module.exports = { port: process.env.PORT || 3000, env: process.env.NODE_ENV || 'development', database: { host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT) || 3306, username: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD || '', database: process.env.DB_NAME || 'koa2_demo', dialect: 'mysql', timezone: '+08:00', pool: { max: 10, min: 0, acquire: 30000, idle: 10000, }, logging: process.env.NODE_ENV === 'development' ? console.log : false, }, redis: { host: process.env.REDIS_HOST || '127.0.0.1', port: parseInt(process.env.REDIS_PORT) || 6379, password: process.env.REDIS_PASSWORD || undefined, db: parseInt(process.env.REDIS_DB) || 0, }, jwt: { secret: process.env.JWT_SECRET || 'default_secret', expiresIn: process.env.JWT_EXPIRES_IN || '7d', },};3.3 应用入口(src/app.js)
const Koa = require('koa');const Router = require('koa-router');const bodyParser = require('koa-body');const cors = require('koa-cors');const config = require('./config');const { initDatabase } = require('./models');const errorHandler = require('./middleware/errorHandler');const routes = require('./routes');const app = new Koa();// 中间件注册顺序(洋葱模型外到内)app.use(cors());app.use(errorHandler);app.use(bodyParser({ multipart: true }));// 注册路由app.use(routes.routes());app.use(routes.allowedMethods());// 启动服务async function bootstrap() { try { // 初始化数据库连接 await initDatabase(); console.log('Database connected successfully'); app.listen(config.port, () => { console.log(`Server running on http://localhost:${config.port}`); }); } catch (error) { console.error('Failed to start server:', error); process.exit(1); }}bootstrap();module.exports = app;四、数据库与模型设计
4.1 数据库初始化(src/models/index.js)
const { Sequelize } = require('sequelize');const config = require('../config');const sequelize = new Sequelize( config.database.database, config.database.username, config.database.password, { host: config.database.host, port: config.database.port, dialect: config.database.dialect, timezone: config.database.timezone, pool: config.database.pool, logging: config.database.logging, });const User = require('./User')(sequelize);const Article = require('./Article')(sequelize);// 模型关联User.hasMany(Article, { foreignKey: 'userId', as: 'articles' });Article.belongsTo(User, { foreignKey: 'userId', as: 'author' });async function initDatabase() { // 开发环境自动同步表结构(生产环境请用 migrations) if (config.env === 'development') { await sequelize.sync({ alter: true }); }}module.exports = { sequelize, initDatabase, User, Article,};4.2 用户模型(src/models/User.js)
const { DataTypes } = require('sequelize');module.exports = (sequelize) => { const User = sequelize.define('User', { id: { type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true, }, username: { type: DataTypes.STRING(50), allowNull: false, unique: true, comment: '用户名', }, nickname: { type: DataTypes.STRING(50), allowNull: true, comment: '昵称', }, email: { type: DataTypes.STRING(100), allowNull: false, unique: true, comment: '邮箱', }, password: { type: DataTypes.STRING(255), allowNull: false, comment: '密码(bcrypt 加密)', }, avatar: { type: DataTypes.STRING(255), allowNull: true, comment: '头像 URL', }, status: { type: DataTypes.TINYINT, defaultValue: 1, comment: '状态:0-禁用 1-正常', }, lastLoginAt: { type: DataTypes.DATE, allowNull: true, comment: '最后登录时间', }, }, { tableName: 'users', timestamps: true, // 自动添加 createdAt、updatedAt underscored: true, // 使用下划线命名 }); return User;};4.3 文章模型(src/models/Article.js)
const { DataTypes } = require('sequelize');module.exports = (sequelize) => { const Article = sequelize.define('Article', { id: { type: DataTypes.INTEGER.UNSIGNED, primaryKey: true, autoIncrement: true, }, title: { type: DataTypes.STRING(200), allowNull: false, comment: '文章标题', }, content: { type: DataTypes.TEXT('long'), allowNull: false, comment: '文章内容', }, summary: { type: DataTypes.STRING(500), allowNull: true, comment: '文章摘要', }, cover: { type: DataTypes.STRING(255), allowNull: true, comment: '封面图', }, status: { type: DataTypes.TINYINT, defaultValue: 0, comment: '状态:0-草稿 1-已发布', }, viewCount: { type: DataTypes.INTEGER.UNSIGNED, defaultValue: 0, comment: '浏览量', }, userId: { type: DataTypes.INTEGER.UNSIGNED, allowNull: false, comment: '作者 ID', }, }, { tableName: 'articles', timestamps: true, underscored: true, }); return Article;};五、Redis 集成
5.1 Redis 连接工具(src/utils/redis.js)
const Redis = require('ioredis');const config = require('../config');const redis = new Redis({ host: config.redis.host, port: config.redis.port, password: config.redis.password, db: config.redis.db, retryStrategy(times) { const delay = Math.min(times * 50, 2000); return delay; }, maxRetriesPerRequest: 3,});redis.on('connect', () => { console.log('Redis connected successfully');});redis.on('error', (err) => { console.error('Redis error:', err);});module.exports = redis;5.2 Redis 常用场景使用示例
const redis = require('./redis');// ========== 1. 缓存热点数据 ==========// 设置缓存(过期时间 3600 秒)async function setCache(key, data, ttl = 3600) { await redis.setex(key, ttl, JSON.stringify(data));}// 获取缓存async function getCache(key) { const data = await redis.get(key); return data ? JSON.parse(data) : null;}// 删除缓存async function delCache(key) { await redis.del(key);}// ========== 2. 接口限流 ==========// 简单滑动窗口限流async function rateLimit(key, maxRequests = 60, windowMs = 60) { const current = await redis.incr(key); if (current === 1) { await redis.expire(key, windowMs); } return current <= maxRequests;}// ========== 3. 分布式锁 ==========async function acquireLock(lockKey, ttl = 10) { const result = await redis.set(lockKey, '1', 'NX', 'EX', ttl); return result === 'OK';}async function releaseLock(lockKey) { await redis.del(lockKey);}// ========== 4. 消息队列(List)==========// 生产者async function pushToQueue(queueName, data) { await redis.rpush(queueName, JSON.stringify(data));}// 消费者(阻塞读取)async function popFromQueue(queueName, timeout = 0) { const result = await redis.blpop(queueName, timeout); return result ? JSON.parse(result[1]) : null;}六、统一响应与工具函数
6.1 统一响应格式(src/utils/response.js)
class Response { // 成功响应 static success(data = null, message = 'success') { return { code: 200, message, data, }; } // 分页成功响应 static paginate(list, pagination) { return { code: 200, message: 'success', data: { list, pagination: { page: pagination.page, pageSize: pagination.pageSize, total: pagination.total, totalPages: Math.ceil(pagination.total / pagination.pageSize), }, }, }; } // 创建成功 static created(data = null, message = 'created') { return { code: 201, message, data, }; } // 错误响应 static error(code = 400, message = 'bad request') { return { code, message, }; }}module.exports = Response;6.2 JWT 工具(src/utils/jwt.js)
const jwt = require('jsonwebtoken');const config = require('../config');function sign(payload) { return jwt.sign(payload, config.jwt.secret, { expiresIn: config.jwt.expiresIn, });}function verify(token) { return jwt.verify(token, config.jwt.secret);}module.exports = { sign, verify };七、中间件
7.1 全局错误处理(src/middleware/errorHandler.js)
const Response = require('../utils/response');module.exports = async (ctx, next) => { try { await next(); // 404 处理 if (ctx.status === 404) { ctx.body = Response.error(404, '接口不存在'); } } catch (err) { console.error('Server error:', err); // 参数校验错误 if (err.name === 'ValidationError') { ctx.status = 400; ctx.body = Response.error(400, err.message); return; } // Sequelize 校验错误 if (err.name === 'SequelizeValidationError' || err.name === 'SequelizeUniqueConstraintError') { ctx.status = 400; ctx.body = Response.error(400, err.errors?.[0]?.message || '数据校验失败'); return; } // JWT 错误 if (err.name === 'UnauthorizedError' || err.name === 'JsonWebTokenError') { ctx.status = 401; ctx.body = Response.error(401, '登录已过期,请重新登录'); return; } // 默认服务器错误 ctx.status = err.status || 500; ctx.body = Response.error(ctx.status, err.message || '服务器内部错误'); }};7.2 JWT 鉴权中间件(src/middleware/auth.js)
const { verify } = require('../utils/jwt');const Response = require('../utils/response');module.exports = (options = {}) => { return async (ctx, next) => { const authHeader = ctx.headers.authorization; if (!authHeader) { ctx.throw(401, '请先登录'); } const token = authHeader.replace('Bearer ', ''); try { const decoded = verify(token); ctx.state.user = decoded; // 将用户信息挂载到 ctx.state await next(); } catch (err) { ctx.body = Response.error(401, '登录已过期,请重新登录'); } };};7.3 参数校验中间件(src/middleware/validator.js)
module.exports = (schema) => { return async (ctx, next) => { const data = { ...ctx.request.body, ...ctx.query, ...ctx.params, }; const { error, value } = schema.validate(data, { abortEarly: false, stripUnknown: true, }); if (error) { const messages = error.details.map((d) => d.message).join('; '); ctx.throw(400, messages); } ctx.validatedData = value; await next(); };};八、控制器与路由示例
8.1 用户控制器(src/controllers/userController.js)
const bcrypt = require('bcryptjs');const { User } = require('../models');const { sign } = require('../utils/jwt');const redis = require('../utils/redis');const Response = require('../utils/response');// 注册async function register(ctx) { const { username, email, password } = ctx.validatedData; // 检查用户是否已存在 const existingUser = await User.findOne({ where: { [require('sequelize').Op.or]: [{ username }, { email }] }, }); if (existingUser) { ctx.body = Response.error(400, '用户名或邮箱已被注册'); return; } // 加密密码 const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(password, salt); // 创建用户 const user = await User.create({ username, email, password: hashedPassword, nickname: username, }); ctx.body = Response.created( { id: user.id, username: user.username, email: user.email, }, '注册成功' );}// 登录async function login(ctx) { const { username, password } = ctx.validatedData; const user = await User.findOne({ where: { username } }); if (!user) { ctx.body = Response.error(400, '用户名或密码错误'); return; } if (user.status === 0) { ctx.body = Response.error(403, '账号已被禁用'); return; } const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { ctx.body = Response.error(400, '用户名或密码错误'); return; } // 生成 Token const token = sign({ id: user.id, username: user.username }); // 更新最后登录时间 await user.update({ lastLoginAt: new Date() }); ctx.body = Response.success({ token, user: { id: user.id, username: user.username, nickname: user.nickname, avatar: user.avatar, }, }, '登录成功');}// 获取当前用户信息async function getProfile(ctx) { const user = await User.findByPk(ctx.state.user.id, { attributes: { exclude: ['password'] }, }); ctx.body = Response.success(user);}// 退出登录(加入黑名单)async function logout(ctx) { const token = ctx.headers.authorization.replace('Bearer ', ''); // 将 token 加入黑名单,过期时间与 token 一致 await redis.setex(`blacklist:${token}`, 7 * 24 * 3600, '1'); ctx.body = Response.success(null, '退出成功');}module.exports = { register, login, getProfile, logout };8.2 用户路由(src/routes/user.js)
const Router = require('koa-router');const userController = require('../controllers/userController');const auth = require('../middleware/auth');const validator = require('../middleware/validator');const { registerSchema, loginSchema } = require('../validators/user');const router = new Router({ prefix: '/api/users' });// 注册router.post('/register', validator(registerSchema), userController.register);// 登录router.post('/login', validator(loginSchema), userController.login);// 获取个人信息(需登录)router.get('/profile', auth(), userController.getProfile);// 退出登录router.post('/logout', auth(), userController.logout);module.exports = router;8.3 路由汇总(src/routes/index.js)
const Router = require('koa-router');const userRoutes = require('./user');const articleRoutes = require('./article');const router = new Router();router.use(userRoutes.routes());router.use(articleRoutes.routes());// 健康检查router.get('/api/health', (ctx) => { ctx.body = { code: 200, message: 'ok', data: { uptime: process.uptime() } };});module.exports = router;九、参数校验(Joi 示例)
9.1 用户校验规则(src/validators/user.js)
const Joi = require('joi');const registerSchema = Joi.object({ username: Joi.string() .min(3) .max(20) .pattern(/^[a-zA-Z0-9_]+$/) .required() .messages({ 'string.min': '用户名至少3个字符', 'string.max': '用户名最多20个字符', 'string.pattern.base': '用户名只能包含字母、数字和下划线', 'any.required': '用户名不能为空', }), email: Joi.string() .email() .required() .messages({ 'string.email': '邮箱格式不正确', 'any.required': '邮箱不能为空', }), password: Joi.string() .min(6) .max(30) .required() .messages({ 'string.min': '密码至少6个字符', 'string.max': '密码最多30个字符', 'any.required': '密码不能为空', }),});const loginSchema = Joi.object({ username: Joi.string().required().messages({ 'any.required': '用户名不能为空', }), password: Joi.string().required().messages({ 'any.required': '密码不能为空', }),});module.exports = { registerSchema, loginSchema };十、UniApp 前端对接
10.1 API 请求封装
// utils/request.js - UniApp 请求封装const BASE_URL = 'http://localhost:3000/api';let token = '';export function setToken(t) { token = t; uni.setStorageSync('token', t);}export function getToken() { if (!token) { token = uni.getStorageSync('token') || ''; } return token;}export function clearToken() { token = ''; uni.removeStorageSync('token');}// 统一请求封装function request(method, url, data = {}, options = {}) { return new Promise((resolve, reject) => { const header = { 'Content-Type': 'application/json', }; const t = getToken(); if (t) { header['Authorization'] = `Bearer ${t}`; } uni.request({ url: `${BASE_URL}${url}`, method, data, header, timeout: 10000, ...options, success: (res) => { const { code, message, data: responseData } = res.data; if (code === 200 || code === 201) { resolve(res.data); } else if (code === 401) { // Token 过期,跳转登录 clearToken(); uni.reLaunch({ url: '/pages/login/login' }); reject(new Error(message)); } else { reject(new Error(message)); } }, fail: (err) => { reject(new Error('网络请求失败')); }, }); });}// 封装 GET / POST / PUT / DELETEexport const get = (url, params) => request('GET', url, params);export const post = (url, data) => request('POST', url, data);export const put = (url, data) => request('PUT', url, data);export const del = (url, params) => request('DELETE', url, params);10.2 登录页面示例
<template> <view class="login-container"> <view class="form"> <input v-model="username" placeholder="请输入用户名" class="input" /> <input v-model="password" type="password" placeholder="请输入密码" class="input" /> <button @click="handleLogin" class="btn">登录</button> <button @click="goRegister" class="btn btn-link">注册账号</button> </view> </view></template><script>import { post, setToken } from '@/utils/request';export default { data() { return { username: '', password: '', }; }, methods: { async handleLogin() { if (!this.username || !this.password) { uni.showToast({ title: '请填写完整信息', icon: 'none' }); return; } uni.showLoading({ title: '登录中...' }); try { const res = await post('/users/login', { username: this.username, password: this.password, }); setToken(res.data.token); uni.hideLoading(); uni.showToast({ title: '登录成功', icon: 'success' }); // 跳转到首页 uni.switchTab({ url: '/pages/index/index' }); } catch (err) { uni.hideLoading(); uni.showToast({ title: err.message, icon: 'none' }); } }, goRegister() { uni.navigateTo({ url: '/pages/register/register' }); }, },};</script>十一、部署指南
11.1 生产环境准备
# 1. 安装 PM2 进程管理npm install -g pm2# 2. 配置环境变量(生产环境)# 复制 .env.example 并修改为生产配置cp .env.example .env# 3. 构建 / 启动NODE_ENV=production pm2 start src/app.js --name koa2-app -i max# 4. 查看状态pm2 statuspm2 logs koa2-app# 5. 设置开机自启pm2 startuppm2 save11.2 Nginx 反向代理配置
server { listen 80; server_name your-domain.com; # 前端静态文件(UniApp H5 构建产物) root /path/to/uniapp-dist; index index.html; # API 反向代理 location /api/ { 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; proxy_set_header X-Forwarded-Proto $scheme; # WebSocket 支持(如有需要) proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } # 静态资源缓存 location /static/ { expires 30d; add_header Cache-Control "public, no-transform"; } # SPA 路由处理 location / { try_files $uri $uri/ /index.html; }}11.3 Docker 部署(可选)
# DockerfileFROM node:18-alpineWORKDIR /appCOPY package*.json ./RUN npm ci --only=productionCOPY . .EXPOSE 3000CMD ["node", "src/app.js"]# docker-compose.ymlversion: '3.8'services: app: build: . ports: - "3000:3000" environment: - NODE_ENV=production - DB_HOST=mysql - REDIS_HOST=redis depends_on: - mysql - redis restart: always mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: koa2_demo volumes: - mysql_data:/var/lib/mysql ports: - "3306:3306" restart: always redis: image: redis:7-alpine ports: - "6379:6379" volumes: - redis_data:/data restart: alwaysvolumes: mysql_data: redis_data:十二、常见问题与最佳实践
12.1 性能优化建议
| 场景 | 方案 |
|---|---|
| 数据库查询频繁 | 加 Redis 缓存,缓存热点数据 |
| 列表查询慢 | 加索引、分页、只查必要字段 |
| 接口响应慢 | 使用 koa-compress 压缩响应体 |
| 大文件上传 | 使用 koa-body 的 multipart 配置 + 分片上传 |
| 日志管理 | 使用 log4js 或 winston 记录日志 |
| 跨域问题 | 生产环境用 Nginx 反向代理,开发用 @koa/cors |
12.2 安全注意事项
- 密码存储:始终使用
bcryptjs哈希,不要明文存储 - SQL 注入:使用 Sequelize ORM 替代原生 SQL 拼接
- XSS 防护:前端输出时对用户输入做转义
- 接口限流:使用 Redis 实现接口限流,防止恶意请求
- HTTPS:生产环境必须启用 HTTPS
- JWT 安全:密钥足够复杂,设置合理的过期时间
- 参数校验:所有接口参数必须做校验(Joi / 自定义校验)
12.3 项目规范
- 命名规范:文件名小写 + 中划线(
user-controller.js),变量名 camelCase - 错误处理:统一使用
errorHandler中间件兜底 - 响应格式:统一使用
Response工具类 - 代码组织:Controller 不直接操作数据库,调 Service 层
- Git 规范:遵循 Conventional Commits(
feat:/fix:/chore:) - 环境变量:敏感信息放在
.env,不提交到 Git
12.4 常用命令速查
# 开发模式启动(nodemon 热重载)npx nodemon src/app.js# 生产模式启动(PM2)pm2 start src/app.js --name koa2-apppm2 restart koa2-apppm2 stop koa2-apppm2 logs koa2-app# 数据库迁移(手动建表)npx sequelize-cli db:migrate# 生成迁移文件npx sequelize-cli migration:generate --name create-users-table# 测试接口curl http://localhost:3000/api/health