基于Vue与Node.js的游戏化背单词网站开发实战
大家好,我是专注于分享实用技术方案的博主。在辅导孩子学习英语的过程中,我发现传统的背单词方法枯燥乏味,孩子很难坚持。今天要分享的,是一个将“养成宠物”与“背单词”巧妙结合的趣味学习网站项目。它不仅是一个工具测评,更是一个完整的技术实现思路拆解,适合想了解如何将游戏化机制融入教育应用的开发者,也适合家长寻找新颖的学习方法。通过本文,你将掌握从项目构思、技术选型到核心功能实现的全过程,并能获得可运行的代码示例。
1. 项目背景与核心价值
1.1 传统背单词的痛点
对于小学生而言,记忆英语单词常常是一项被动且枯燥的任务。常见的痛点包括:
- 缺乏内在动力:记忆过程与奖励脱钩,难以形成正反馈循环。
- 形式单一:重复的抄写、默写容易引起厌倦。
- 遗忘速度快:缺乏符合艾宾浩斯遗忘曲线的科学复习机制。
- 脱离语境:孤立地记忆单词,难以理解其实际用法。
1.2 游戏化学习(Gamification)的解决方案
游戏化学习是将游戏设计元素应用于非游戏场景,以此提升用户参与度和动力的方法论。其核心机制包括:
- 目标与规则:清晰的任务(如每日背诵10个单词)。
- 反馈系统:即时奖励(如获得宠物食物、经验值)。
- 自愿参与:吸引用户沉浸于设定好的互动中。
“养宠物背词”项目正是基于此理念:用户通过完成背单词任务来获取虚拟资源(如狗粮、金币),用以喂养和升级自己的电子宠物。宠物成长的状态,直观地反映了用户的学习投入,将抽象的学习成果转化为具象的、情感化的反馈。
1.3 项目技术定位
这是一个典型的前后端分离的Web应用,涉及前端交互、后端逻辑、数据存储和简单的游戏化算法。我们将使用主流且易上手的技术栈进行构建,确保项目结构清晰,便于理解和二次开发。
2. 技术栈与开发环境准备
2.1 技术选型说明
选择合适的技术栈是项目成功的基础,以下是我们的选择及理由:
| 模块 | 技术选型 | 说明 |
|---|---|---|
| 前端 | Vue 3 + Element Plus | Vue 3响应式开发体验好,Element Plus组件库丰富,能快速搭建美观界面。 |
| 后端 | Node.js + Koa2 | Node.js适合I/O密集型的Web应用,Koa2轻量、优雅,中间件机制灵活。 |
| 数据库 | MySQL 8.0 | 关系型数据库,适合存储结构化的用户、单词、宠物数据。 |
| ORM框架 | Sequelize | 优秀的Node.js ORM,支持Promise,简化数据库操作。 |
| 构建工具 | Vite | 前端构建工具,启动快,热更新迅速,提升开发体验。 |
2.2 本地开发环境搭建
请确保你的开发机已安装以下基础环境:
- Node.js: 版本建议16.x或18.x LTS。
- 访问 Node.js官网 下载安装包。
- 安装后,在终端运行
node -v和npm -v检查版本。
- MySQL: 版本8.0或以上。
- 访问 MySQL官网 下载社区版。
- 安装并记住root密码,确保MySQL服务已启动。
- 代码编辑器: VS Code,并安装Volar (Vue 3支持)、ESLint等插件。
2.3 初始化项目结构
我们将创建两个独立的项目:前端 (pet-vocab-frontend) 和后端 (pet-vocab-backend)。
# 1. 创建项目根目录 mkdir pet-vocab-project cd pet-vocab-project # 2. 创建后端项目(使用Koa生成器或手动初始化) mkdir backend && cd backend npm init -y # 3. 创建前端项目(使用Vite官方模板) cd .. npm create vue@latest frontend # 根据提示选择:Vue, TypeScript, Router, Pinia, ESLint初始化后的目录结构应大致如下:
pet-vocab-project/ ├── backend/ # 后端服务 │ ├── src/ │ ├── package.json │ └── ... └── frontend/ # 前端应用 ├── src/ ├── package.json └── ...3. 数据库设计与核心模型
在编写代码前,设计合理的数据模型至关重要。我们将创建四张核心表。
3.1 数据表ER关系(概念模型)
- 用户(User)拥有一个宠物(Pet)。
- 用户(User)学习多个单词(Word),产生多条学习记录(StudyRecord)。
- 学习记录(StudyRecord)关联某个单词(Word)。
3.2 SQL建表语句
在MySQL中执行以下SQL语句创建数据库和表。
-- 创建数据库 CREATE DATABASE IF NOT EXISTS `pet_vocab_db` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE `pet_vocab_db`; -- 用户表 CREATE TABLE `user` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', `password_hash` VARCHAR(255) NOT NULL COMMENT '加密后的密码', `email` VARCHAR(100) UNIQUE COMMENT '邮箱', `coin` INT NOT NULL DEFAULT 0 COMMENT '金币数', `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='用户表'; -- 宠物表 CREATE TABLE `pet` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, `user_id` INT UNSIGNED NOT NULL UNIQUE COMMENT '关联用户ID', `name` VARCHAR(50) NOT NULL DEFAULT '我的宠物' COMMENT '宠物名称', `type` TINYINT NOT NULL DEFAULT 1 COMMENT '宠物类型:1-狗,2-猫,3-熊猫等', `level` INT NOT NULL DEFAULT 1 COMMENT '宠物等级', `exp` INT NOT NULL DEFAULT 0 COMMENT '当前经验值', `hunger` INT NOT NULL DEFAULT 80 COMMENT '饱食度 (0-100)', `mood` INT NOT NULL DEFAULT 80 COMMENT '心情值 (0-100)', `last_feed_time` TIMESTAMP NULL COMMENT '上次喂食时间', `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB COMMENT='宠物表'; -- 单词表(预置数据) CREATE TABLE `word` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, `english` VARCHAR(100) NOT NULL COMMENT '英文单词', `phonetic` VARCHAR(100) COMMENT '音标', `chinese` VARCHAR(255) NOT NULL COMMENT '中文释义', `example_sentence` TEXT COMMENT '例句', `difficulty` TINYINT DEFAULT 1 COMMENT '难度等级 1-5', `category` VARCHAR(50) COMMENT '分类,如:动物、水果', `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='单词库表'; -- 学习记录表 CREATE TABLE `study_record` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, `user_id` INT UNSIGNED NOT NULL COMMENT '用户ID', `word_id` INT UNSIGNED NOT NULL COMMENT '单词ID', `familiarity` TINYINT NOT NULL DEFAULT 0 COMMENT '熟悉度 (0-陌生,1-认识,2-熟悉,3-掌握)', `next_review_date` DATE NOT NULL COMMENT '下次复习日期(基于艾宾浩斯曲线)', `review_count` INT NOT NULL DEFAULT 0 COMMENT '已复习次数', `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_user_next_review (`user_id`, `next_review_date`), FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, FOREIGN KEY (`word_id`) REFERENCES `word`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB COMMENT='单词学习记录表';4. 后端API服务实现
后端负责提供数据接口和核心业务逻辑。我们使用Koa2框架。
4.1 初始化后端项目并安装依赖
进入backend目录,安装必要依赖。
cd backend npm install koa koa-router koa-bodyparser @koa/cors koa-helmet npm install sequelize mysql2 npm install bcryptjs jsonwebtoken npm install -D nodemon4.2 核心目录结构与配置
创建以下目录和文件:
backend/ ├── src/ │ ├── config/ │ │ └── database.js # 数据库配置 │ ├── models/ │ │ ├── index.js # 模型初始化 │ │ ├── User.js │ │ ├── Pet.js │ │ ├── Word.js │ │ └── StudyRecord.js │ ├── controllers/ # 控制器 │ ├── routes/ # 路由 │ ├── middleware/ # 中间件 │ └── app.js # 应用入口 ├── .env # 环境变量 └── package.json配置文件src/config/database.js:
// 数据库连接配置 module.exports = { development: { username: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD || 'your_password', database: process.env.DB_NAME || 'pet_vocab_db', host: process.env.DB_HOST || '127.0.0.1', port: process.env.DB_PORT || 3306, dialect: 'mysql', logging: console.log, // 开发时显示SQL日志 }, test: { ... }, production: { ... } };环境变量文件.env:
DB_USER=root DB_PASSWORD=your_mysql_password DB_NAME=pet_vocab_db DB_HOST=localhost JWT_SECRET=your_super_secret_jwt_key_change_this4.3 定义数据模型(以User和Pet为例)
使用Sequelize定义模型,建立关联关系。
文件src/models/User.js:
const { DataTypes } = require('sequelize'); module.exports = (sequelize) => { const User = sequelize.define('User', { username: { type: DataTypes.STRING(50), allowNull: false, unique: true, }, passwordHash: { type: DataTypes.STRING(255), allowNull: false, field: 'password_hash', // 映射到数据库字段 }, email: { type: DataTypes.STRING(100), unique: true, }, coin: { type: DataTypes.INTEGER, defaultValue: 0, }, }, { tableName: 'user', timestamps: true, // 自动管理createdAt和updatedAt underscored: true, // 使用下划线命名风格 }); User.associate = (models) => { // 一个用户拥有一只宠物 User.hasOne(models.Pet, { foreignKey: 'userId', onDelete: 'CASCADE' }); // 一个用户有多条学习记录 User.hasMany(models.StudyRecord, { foreignKey: 'userId' }); }; return User; };文件src/models/Pet.js:
const { DataTypes } = require('sequelize'); module.exports = (sequelize) => { const Pet = sequelize.define('Pet', { name: { type: DataTypes.STRING(50), defaultValue: '我的宠物', }, type: { type: DataTypes.TINYINT, defaultValue: 1, comment: '1-狗,2-猫,3-熊猫', }, level: { type: DataTypes.INTEGER, defaultValue: 1, }, exp: { type: DataTypes.INTEGER, defaultValue: 0, }, hunger: { type: DataTypes.INTEGER, defaultValue: 80, validate: { min: 0, max: 100 }, }, mood: { type: DataTypes.INTEGER, defaultValue: 80, validate: { min: 0, max: 100 }, }, lastFeedTime: { type: DataTypes.DATE, field: 'last_feed_time', }, }, { tableName: 'pet', timestamps: true, underscored: true, }); Pet.associate = (models) => { // 一只宠物属于一个用户 Pet.belongsTo(models.User, { foreignKey: 'userId' }); }; return Pet; };4.4 实现核心业务逻辑与API
我们以实现“完成单词学习”这个核心接口为例,展示前后端交互和游戏化逻辑。
路由文件src/routes/study.js:
const Router = require('koa-router'); const router = new Router({ prefix: '/api/study' }); const StudyController = require('../controllers/studyController'); const authMiddleware = require('../middleware/auth'); // 需要用户登录认证 router.use(authMiddleware); // 获取今日需学习的单词列表 router.get('/today-words', StudyController.getTodayWords); // 提交单词学习结果 router.post('/complete', StudyController.completeStudy); module.exports = router;控制器文件src/controllers/studyController.js:
const { User, Pet, Word, StudyRecord } = require('../models'); class StudyController { // 获取今日需要学习的单词(新学+复习) static async getTodayWords(ctx) { const userId = ctx.state.user.id; const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD try { // 1. 查找需要复习的单词(next_review_date <= today) const reviewRecords = await StudyRecord.findAll({ where: { userId, nextReviewDate: { [Op.lte]: today } }, include: [{ model: Word }], limit: 20, // 每日复习上限 }); // 2. 查找今日可能的新单词(例如,从未学过的) const learnedWordIds = (await StudyRecord.findAll({ where: { userId }, attributes: ['wordId'], })).map(r => r.wordId); const newWords = await Word.findAll({ where: { id: { [Op.notIn]: learnedWordIds }, difficulty: { [Op.lte]: 2 }, // 先学简单单词 }, limit: 10, // 每日新学上限 order: sequelize.random(), // 随机抽取 }); // 3. 合并列表 const todayWords = [ ...reviewRecords.map(r => ({ ...r.Word.toJSON(), recordId: r.id, type: 'review' })), ...newWords.map(w => ({ ...w.toJSON(), recordId: null, type: 'new' })) ]; ctx.body = { code: 200, data: todayWords }; } catch (error) { ctx.status = 500; ctx.body = { code: 500, message: '获取单词列表失败', error: error.message }; } } // 完成一个单词的学习 static async completeStudy(ctx) { const userId = ctx.state.user.id; const { wordId, isCorrect, recordId } = ctx.request.body; // recordId不为空则为复习 const t = await sequelize.transaction(); // 开启事务,确保数据一致性 try { const user = await User.findByPk(userId, { transaction: t }); const pet = await Pet.findOne({ where: { userId }, transaction: t }); const word = await Word.findByPk(wordId, { transaction: t }); if (!user || !pet || !word) { throw new Error('用户、宠物或单词不存在'); } let studyRecord; let expGained = 0; let coinGained = 0; // --- 游戏化奖励计算逻辑 --- if (isCorrect) { expGained = 10; // 答对获得10经验 coinGained = 5; // 答对获得5金币 // 根据单词难度调整奖励? expGained *= word.difficulty; } else { expGained = 2; // 答错获得少量经验,鼓励继续 coinGained = 1; } // --- 更新学习记录(基于艾宾浩斯遗忘曲线简化版)--- if (recordId) { // 复习:更新已有记录 studyRecord = await StudyRecord.findByPk(recordId, { transaction: t }); studyRecord.reviewCount += 1; // 根据复习次数和正确与否,计算下次复习间隔(天) const intervals = [1, 3, 7, 14, 30]; // 简化间隔 const nextIntervalIdx = Math.min(studyRecord.reviewCount, intervals.length - 1); const nextInterval = intervals[nextIntervalIdx]; const nextDate = new Date(); nextDate.setDate(nextDate.getDate() + nextInterval); studyRecord.nextReviewDate = nextDate; studyRecord.familiarity = isCorrect ? Math.min(3, studyRecord.familiarity + 1) : Math.max(0, studyRecord.familiarity - 1); } else { // 新学:创建记录 const nextDate = new Date(); nextDate.setDate(nextDate.getDate() + 1); // 新单词第二天复习 studyRecord = await StudyRecord.create({ userId, wordId, familiarity: isCorrect ? 1 : 0, nextReviewDate: nextDate, reviewCount: 0, }, { transaction: t }); } await studyRecord.save({ transaction: t }); // --- 更新用户和宠物数据 --- user.coin += coinGained; await user.save({ transaction: t }); pet.exp += expGained; // 经验升级逻辑:每100经验升1级 const levelUpExp = pet.level * 100; if (pet.exp >= levelUpExp) { pet.level += 1; pet.exp = pet.exp - levelUpExp; // 可以在这里触发升级特效或通知 } // 学习行为略微降低饱食度和心情,促进喂食互动 pet.hunger = Math.max(0, pet.hunger - 2); pet.mood = Math.max(0, pet.mood - 1); await pet.save({ transaction: t }); await t.commit(); // 提交事务 ctx.body = { code: 200, message: '学习完成!', data: { expGained, coinGained, userCoin: user.coin, petLevel: pet.level, petExp: pet.exp, petHunger: pet.hunger, petMood: pet.mood, } }; } catch (error) { await t.rollback(); // 回滚事务 ctx.status = 500; ctx.body = { code: 500, message: '学习过程出错', error: error.message }; } } } module.exports = StudyController;5. 前端界面与交互实现
前端使用Vue 3和Element Plus,通过Pinia进行状态管理。
5.1 核心页面:学习与宠物主页
我们创建一个主要的学习页面,同时展示宠物状态。
组件src/views/StudyView.vue:
<template> <div class="study-container"> <!-- 左侧:宠物状态面板 --> <div class="pet-panel"> <el-card> <template #header> <div class="pet-header"> <span>{{ petStore.pet?.name || '加载中...' }}</span> <el-tag type="success">Lv.{{ petStore.pet?.level }}</el-tag> </div> </template> <!-- 宠物形象(可根据type显示不同图片) --> <div class="pet-avatar"> <el-image :src="getPetImage(petStore.pet?.type)" fit="cover" /> </div> <!-- 状态条 --> <div class="status-bars"> <div class="status-item"> <span>经验:</span> <el-progress :percentage="(petStore.pet?.exp / (petStore.pet?.level * 100)) * 100" :show-text="false" /> <span>{{ petStore.pet?.exp }}/{{ petStore.pet?.level * 100 }}</span> </div> <div class="status-item"> <span>饱食度:</span> <el-progress :percentage="petStore.pet?.hunger" status="warning" :show-text="false" /> <span>{{ petStore.pet?.hunger }}/100</span> </div> <div class="status-item"> <span>心情值:</span> <el-progress :percentage="petStore.pet?.mood" status="success" :show-text="false" /> <span>{{ petStore.pet?.mood }}/100</span> </div> </div> <div class="user-info"> <span>金币: {{ userStore.user?.coin }}</span> <el-button type="primary" size="small" @click="handleFeed">喂食</el-button> </div> </el-card> </div> <!-- 右侧:单词学习区 --> <div class="study-panel"> <el-card> <template #header> <div class="study-header"> <span>今日任务 ({{ todayWords.length }})</span> <el-button type="primary" @click="fetchTodayWords" :loading="loading">刷新列表</el-button> </div> </template> <div v-if="todayWords.length === 0 && !loading" class="empty-tip"> <el-empty description="恭喜!今日任务已完成,记得明天再来哦~" /> </div> <div v-else> <!-- 单词卡片 --> <div v-for="(word, index) in todayWords" :key="word.id" class="word-card"> <el-card shadow="hover"> <div class="word-content"> <div class="word-main"> <h3>{{ word.english }}</h3> <p class="phonetic">{{ word.phonetic }}</p> <p class="chinese">{{ word.chinese }}</p> <p v-if="word.example_sentence" class="example"> <i>例句: {{ word.example_sentence }}</i> </p> </div> <div class="word-actions"> <el-button type="success" @click="handleAnswer(word, true, word.recordId)" :disabled="answered.has(word.id)">认识</el-button> <el-button type="danger" @click="handleAnswer(word, false, word.recordId)" :disabled="answered.has(word.id)">不认识</el-button> <el-tag v-if="word.type === 'review'" type="info">复习</el-tag> <el-tag v-else type="success">新学</el-tag> </div> </div> </el-card> </div> </div> </el-card> </div> </div> <!-- 学习结果弹窗 --> <el-dialog v-model="resultDialogVisible" title="学习结果" width="30%"> <div v-if="lastResult"> <p v-if="lastResult.isCorrect" style="color: #67C23A;">太棒了!答对了!</p> <p v-else style="color: #F56C6C;">没关系,下次一定记得!</p> <p>获得经验: <strong>{{ lastResult.expGained }}</strong></p> <p>获得金币: <strong>{{ lastResult.coinGained }}</strong></p> <p>宠物饱食度: {{ lastResult.petHunger }}</p> <p>宠物心情值: {{ lastResult.petMood }}</p> </div> <template #footer> <el-button @click="resultDialogVisible = false">继续学习</el-button> </template> </el-dialog> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue'; import { ElMessage } from 'element-plus'; import { useUserStore } from '@/stores/user'; import { usePetStore } from '@/stores/pet'; import { getTodayWords, completeStudy } from '@/api/study'; const userStore = useUserStore(); const petStore = usePetStore(); const loading = ref(false); const todayWords = ref([]); const answered = ref(new Set()); // 记录已作答的单词ID const resultDialogVisible = ref(false); const lastResult = ref(null); // 获取宠物图片 const getPetImage = (type) => { const images = { 1: '/images/pet_dog.png', 2: '/images/pet_cat.png', 3: '/images/pet_panda.png', }; return images[type] || images[1]; }; // 获取今日单词 const fetchTodayWords = async () => { loading.value = true; try { const res = await getTodayWords(); if (res.code === 200) { todayWords.value = res.data; answered.value.clear(); // 重置作答状态 } else { ElMessage.error(res.message || '获取单词失败'); } } catch (error) { ElMessage.error('网络请求失败'); } finally { loading.value = false; } }; // 处理用户答案 const handleAnswer = async (word, isCorrect, recordId) => { try { const res = await completeStudy({ wordId: word.id, isCorrect, recordId, }); if (res.code === 200) { // 记录已作答 answered.value.add(word.id); // 显示结果 lastResult.value = { isCorrect, ...res.data, }; resultDialogVisible.value = true; // 更新本地状态 userStore.updateCoin(res.data.userCoin); petStore.updatePet(res.data); // 从列表中移除已学习的单词(可选) todayWords.value = todayWords.value.filter(w => w.id !== word.id); ElMessage.success(res.message); } else { ElMessage.error(res.message || '提交失败'); } } catch (error) { ElMessage.error('提交答案失败'); } }; // 喂食操作 const handleFeed = async () => { // 调用喂食API,消耗金币,增加宠物饱食度和心情 ElMessage.info('喂食功能需调用后端API实现'); }; onMounted(() => { fetchTodayWords(); // 初始化时也获取一次宠物信息 petStore.fetchPet(); }); </script> <style scoped> .study-container { display: flex; gap: 20px; padding: 20px; max-width: 1200px; margin: 0 auto; } .pet-panel { flex: 0 0 300px; } .study-panel { flex: 1; } .pet-header { display: flex; justify-content: space-between; align-items: center; } .pet-avatar { text-align: center; margin: 15px 0; } .pet-avatar .el-image { width: 150px; height: 150px; border-radius: 50%; } .status-item { margin-bottom: 10px; display: flex; align-items: center; gap: 10px; } .status-item span:first-child { width: 60px; } .user-info { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; } .study-header { display: flex; justify-content: space-between; align-items: center; } .word-card { margin-bottom: 15px; } .word-content { display: flex; justify-content: space-between; align-items: center; } .word-main h3 { margin: 0 0 5px 0; font-size: 1.5em; } .phonetic { color: #909399; font-style: italic; margin: 5px 0; } .chinese { color: #303133; font-weight: bold; margin: 5px 0; } .example { color: #606266; margin-top: 8px; } .word-actions { display: flex; align-items: center; gap: 10px; } .empty-tip { padding: 40px 0; } </style>5.2 状态管理(Pinia Store)
使用Pinia管理用户和宠物的全局状态。
文件src/stores/user.ts:
import { defineStore } from 'pinia'; import { ref } from 'vue'; import { getUserInfo } from '@/api/user'; export const useUserStore = defineStore('user', () => { const user = ref(null); const token = ref(localStorage.getItem('token') || ''); const setUser = (userData) => { user.value = userData; }; const setToken = (newToken) => { token.value = newToken; localStorage.setItem('token', newToken); }; const clearUser = () => { user.value = null; token.value = ''; localStorage.removeItem('token'); }; const updateCoin = (newCoin) => { if (user.value) { user.value.coin = newCoin; } }; // 获取用户信息 const fetchUserInfo = async () => { if (!token.value) return; try { const res = await getUserInfo(); if (res.code === 200) { setUser(res.data); } } catch (error) { console.error('获取用户信息失败', error); } }; return { user, token, setUser, setToken, clearUser, updateCoin, fetchUserInfo, }; });6. 项目部署与上线要点
6.1 后端部署(以PM2为例)
- 构建与准备:确保后端代码已通过测试。
- 安装PM2:
npm install -g pm2 - 配置生产环境变量:创建
.env.production文件,配置数据库连接、JWT密钥等。 - 使用PM2启动:
NODE_ENV=production pm2 start src/app.js --name pet-vocab-backend - 设置开机自启:
pm2 startup然后pm2 save。
6.2 前端部署
- 构建生产包:在
frontend目录下运行npm run build,生成dist文件夹。 - 部署静态资源:可以将
dist目录下的文件部署到Nginx、Apache或任何静态文件服务器,也可以上传到对象存储(如阿里云OSS)并配置CDN。 - 配置Nginx反向代理(关键):让Nginx同时服务前端静态文件,并将API请求代理到后端Node.js服务。
server { listen 80; server_name your-domain.com; # 你的域名 # 前端静态文件 location / { root /path/to/your/frontend/dist; try_files $uri $uri/ /index.html; } # 后端API代理 location /api/ { proxy_pass http://localhost: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; } }
7. 常见问题与排查思路
在开发和部署过程中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 前端页面空白,控制台报404 | 1. 前端路由模式为history,未配置Nginxtry_files。2. 静态资源路径错误。 | 1. 检查Nginx配置,确保所有非API请求都返回index.html。2. 检查前端 vite.config.ts中的base配置。 |
| 后端连接数据库失败 | 1. 数据库服务未启动。 2. 连接配置(用户名、密码、主机、端口)错误。 3. 数据库用户权限不足。 | 1. 运行systemctl status mysql检查服务状态。2. 逐一核对 .env文件中的数据库配置。3. 使用命令行工具(如 mysql -u root -p)测试连接。 |
| 完成学习后,宠物状态未更新 | 1. 前端未正确调用更新状态的Store方法。 2. 后端事务提交失败,数据未持久化。 3. 前端接收响应后,未重新获取宠物数据。 | 1. 检查浏览器开发者工具Network面板,确认API调用是否成功。 2. 查看后端日志,检查事务中是否有错误导致回滚。 3. 在前端 handleAnswer方法中,确认已调用petStore.updatePet(res.data)。 |
| 单词列表不更新,总是重复 | 1. 学习记录表next_review_date逻辑计算错误。2. 前端缓存了旧的API响应。 3. 查询条件 [Op.lte]: today的日期格式不匹配。 | 1. 在后端控制器中打印生成的SQL语句,检查查询条件。 2. 在前端API调用中添加时间戳参数防止缓存。 3. 确保数据库和服务器时区一致, today变量格式为YYYY-MM-DD。 |
| 页面加载缓慢 | 1. 前端资源文件过大。 2. 数据库查询未优化(如缺少索引)。 3. 服务器带宽或配置过低。 | 1. 使用Vite构建分析工具,优化包体积。启用Gzip压缩。 2. 为 study_record表的(user_id, next_review_date)添加复合索引。3. 考虑对静态资源使用CDN加速。 |
8. 最佳实践与扩展建议
8.1 工程化建议
- API接口规范化:统一响应格式
{ code, message, data },并编写详细的接口文档(可使用Swagger)。 - 错误处理:后端使用全局错误处理中间件,捕获并分类处理异常,返回友好的错误信息。
- 日志记录:使用
winston或log4js记录应用日志,便于问题追踪。 - 数据验证:对前端传入的数据,在后端使用
Joi或validator.js进行严格的校验。 - 安全性:
- 用户密码必须加盐哈希存储(使用
bcryptjs)。 - API接口使用JWT认证,并对敏感操作(如消费金币)进行权限校验。
- 防止SQL注入(Sequelize已提供防护)、XSS攻击(对用户输入进行转义)。
- 用户密码必须加盐哈希存储(使用
8.2 游戏化与学习算法优化
- 更科学的复习算法:实现完整的艾宾浩斯遗忘曲线算法(如SM-2),替代简单的固定间隔数组。
- 动态难度调整:根据用户的历史正确率,动态调整推送单词的难度。
- 丰富的宠物系统:增加宠物装备、皮肤、技能系统,消耗金币解锁,提升长期动力。
- 成就与排行榜:设立学习成就(如“连续学习7天”、“掌握500单词”),并提供好友排行榜,增加社交竞争元素。
- 学习数据可视化:为用户提供学习报告图表,展示学习趋势、单词掌握度分布。
8.3 运营与内容扩展
- 单词库管理后台:开发一个管理员后台,方便批量导入、分类、管理单词库。
- 多端适配:使用响应式设计或开发微信小程序版本,方便学生在不同设备上使用。
- 家长监控功能:为家长提供独立入口,查看孩子的学习进度和报告。
- 社交分享:允许孩子将宠物成长状态分享到社交媒体,获得额外奖励。
这个项目将趣味性与学习性结合,技术栈覆盖了现代Web开发的主要环节。你可以根据这个基础框架进行深化,例如引入更复杂的游戏逻辑、增加AI单词推荐、或者优化移动端体验。核心在于理解“激励-反馈”循环的设计,以及如何用可靠的技术将其实现。
