基于Vue与Node.js的游戏化背单词网站开发实战

基于Vue与Node.js的游戏化背单词网站开发实战

大家好,我是专注于分享实用技术方案的博主。在辅导孩子学习英语的过程中,我发现传统的背单词方法枯燥乏味,孩子很难坚持。今天要分享的,是一个将“养成宠物”与“背单词”巧妙结合的趣味学习网站项目。它不仅是一个工具测评,更是一个完整的技术实现思路拆解,适合想了解如何将游戏化机制融入教育应用的开发者,也适合家长寻找新颖的学习方法。通过本文,你将掌握从项目构思、技术选型到核心功能实现的全过程,并能获得可运行的代码示例。

1. 项目背景与核心价值

1.1 传统背单词的痛点

对于小学生而言,记忆英语单词常常是一项被动且枯燥的任务。常见的痛点包括:

  • 缺乏内在动力:记忆过程与奖励脱钩,难以形成正反馈循环。
  • 形式单一:重复的抄写、默写容易引起厌倦。
  • 遗忘速度快:缺乏符合艾宾浩斯遗忘曲线的科学复习机制。
  • 脱离语境:孤立地记忆单词,难以理解其实际用法。

1.2 游戏化学习(Gamification)的解决方案

游戏化学习是将游戏设计元素应用于非游戏场景,以此提升用户参与度和动力的方法论。其核心机制包括:

  • 目标与规则:清晰的任务(如每日背诵10个单词)。
  • 反馈系统:即时奖励(如获得宠物食物、经验值)。
  • 自愿参与:吸引用户沉浸于设定好的互动中。

“养宠物背词”项目正是基于此理念:用户通过完成背单词任务来获取虚拟资源(如狗粮、金币),用以喂养和升级自己的电子宠物。宠物成长的状态,直观地反映了用户的学习投入,将抽象的学习成果转化为具象的、情感化的反馈。

1.3 项目技术定位

这是一个典型的前后端分离的Web应用,涉及前端交互、后端逻辑、数据存储和简单的游戏化算法。我们将使用主流且易上手的技术栈进行构建,确保项目结构清晰,便于理解和二次开发。

2. 技术栈与开发环境准备

2.1 技术选型说明

选择合适的技术栈是项目成功的基础,以下是我们的选择及理由:

模块技术选型说明
前端Vue 3 + Element PlusVue 3响应式开发体验好,Element Plus组件库丰富,能快速搭建美观界面。
后端Node.js + Koa2Node.js适合I/O密集型的Web应用,Koa2轻量、优雅,中间件机制灵活。
数据库MySQL 8.0关系型数据库,适合存储结构化的用户、单词、宠物数据。
ORM框架Sequelize优秀的Node.js ORM,支持Promise,简化数据库操作。
构建工具Vite前端构建工具,启动快,热更新迅速,提升开发体验。

2.2 本地开发环境搭建

请确保你的开发机已安装以下基础环境:

  1. Node.js: 版本建议16.x或18.x LTS。
    • 访问 Node.js官网 下载安装包。
    • 安装后,在终端运行node -vnpm -v检查版本。
  2. MySQL: 版本8.0或以上。
    • 访问 MySQL官网 下载社区版。
    • 安装并记住root密码,确保MySQL服务已启动。
  3. 代码编辑器: 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 nodemon

4.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_this

4.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为例)

  1. 构建与准备:确保后端代码已通过测试。
  2. 安装PM2npm install -g pm2
  3. 配置生产环境变量:创建.env.production文件,配置数据库连接、JWT密钥等。
  4. 使用PM2启动
    NODE_ENV=production pm2 start src/app.js --name pet-vocab-backend
  5. 设置开机自启pm2 startup然后pm2 save

6.2 前端部署

  1. 构建生产包:在frontend目录下运行npm run build,生成dist文件夹。
  2. 部署静态资源:可以将dist目录下的文件部署到Nginx、Apache或任何静态文件服务器,也可以上传到对象存储(如阿里云OSS)并配置CDN。
  3. 配置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. 常见问题与排查思路

在开发和部署过程中,你可能会遇到以下问题:

问题现象可能原因排查步骤与解决方案
前端页面空白,控制台报4041. 前端路由模式为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)。
  • 错误处理:后端使用全局错误处理中间件,捕获并分类处理异常,返回友好的错误信息。
  • 日志记录:使用winstonlog4js记录应用日志,便于问题追踪。
  • 数据验证:对前端传入的数据,在后端使用Joivalidator.js进行严格的校验。
  • 安全性
    • 用户密码必须加盐哈希存储(使用bcryptjs)。
    • API接口使用JWT认证,并对敏感操作(如消费金币)进行权限校验。
    • 防止SQL注入(Sequelize已提供防护)、XSS攻击(对用户输入进行转义)。

8.2 游戏化与学习算法优化

  • 更科学的复习算法:实现完整的艾宾浩斯遗忘曲线算法(如SM-2),替代简单的固定间隔数组。
  • 动态难度调整:根据用户的历史正确率,动态调整推送单词的难度。
  • 丰富的宠物系统:增加宠物装备、皮肤、技能系统,消耗金币解锁,提升长期动力。
  • 成就与排行榜:设立学习成就(如“连续学习7天”、“掌握500单词”),并提供好友排行榜,增加社交竞争元素。
  • 学习数据可视化:为用户提供学习报告图表,展示学习趋势、单词掌握度分布。

8.3 运营与内容扩展

  • 单词库管理后台:开发一个管理员后台,方便批量导入、分类、管理单词库。
  • 多端适配:使用响应式设计或开发微信小程序版本,方便学生在不同设备上使用。
  • 家长监控功能:为家长提供独立入口,查看孩子的学习进度和报告。
  • 社交分享:允许孩子将宠物成长状态分享到社交媒体,获得额外奖励。

这个项目将趣味性与学习性结合,技术栈覆盖了现代Web开发的主要环节。你可以根据这个基础框架进行深化,例如引入更复杂的游戏逻辑、增加AI单词推荐、或者优化移动端体验。核心在于理解“激励-反馈”循环的设计,以及如何用可靠的技术将其实现。