资讯动态

Vue+Node.js+MongoDB课程站源码包:从环境配置到业务闭环

发布时间:2026/9/15 5:47:30 来源:尧图企业网站定制
简介这是一份基于Vue.js、Node.js与MongoDB构建的在线学习平台完整源码案例适合具备一定前端基础、希望进阶全栈开发的开发者学习参考。项目覆盖前端页面交互、后端API设计与数据库建模能帮助理解三者如何协同工作。资源包共529个文件约11.71MB核心包括285个JavaScript逻辑文件、22个Vue组件、19个CSS样式表以及大量png/gif图片素材和HTML/XML等页面辅助文件结构上清晰区分前后端与数据库模型。源码内含用户登录注册、课程列表与播放器、评论评分等典型业务模块并配备RESTful接口文档便于对照学习。已有134人学习下载对于想从零搭建完整Web应用、掌握全栈开发关键技能的开发者来说是一份值得深入研究的实战案例。1. 一套 VueNode.jsMongoDB 精品课程站的源码包到底该从哪下手标题里这种「设计源码案例.zip」第一反应别是双击 index.html里面大概率不是静态网页而是一套前后端分离的课设/毕设项目Vue 管页面Node.js 管接口MongoDB 存课程、用户和学习进度。这类项目最劝退的点恰恰不在代码而在环境链太长——缺 Node.js 前端构建先挂缺 MongoDB 后端秒报连接失败两个服务都装好了又不知道该先启动哪一个。下面按「为什么这么设计 → 怎么初始化 → 核心业务闭环 → 验收与打包」的顺序铺一条可复现的路径先装环境再定数据模型然后打通登录、课程列表、m3u8 播放和进度记录最后用数据导入与打包验证收尾。读者定位是有 JavaScript 基础、要领着这套三件套源码从安装一路走到可演示状态的人。2. 为什么选 VueNode.jsMongoDB课程站的模块拆解与数据模型设计2.1 前后端分离与文档型数据库的匹配点一个精品课程在线学习站业务面上至少有四块用户体系注册、登录、学习记录、课程目录分类、课程、课时、视频地址、学习行为播放到第几秒、是否看完、内容管理课程上下架、章节排序。用 Vue Node.js MongoDB 的组合做这套业务最大的收益是 JavaScript 贯穿三层浏览器里是 Vue 组件服务端是 Node.js 模块数据层是 BSON 文档。三个上下文之间传递的都是「对象」不需要在 JSON 和关系表结构之间来回翻译心智模型这正是课程设计类项目最看重的一个人能全流程写完。MongoDB 的文档模型对「课程 - 章节 - 视频」这类树状数据很友好。一门课包含若干课时每个课时有标题、视频地址、时长、排序号是一个天然嵌套结构。换成 MySQL 就得拆课程表、章节表、视频表三张表再加外键查询时要 join文档模型则可以把整个课程目录塞进一个文档打开课程详情页时读一次文档就拿全了目录。但这不代表嵌套越多越好设计原则是读多写少的聚合体适合内嵌高频且独立更新的数据要单独成集合否则每次更新都要重写整个大文档。2.2 用户、课程、章节、学习记录四类核心集合的文档结构集合名主要字段写入频率设计说明users_id, nickname, email, passwordHash, createdAt低passwordHash 只存哈希绝不要存明文密码courses_id, title, category, cover, sections[]低课时作为子文档内嵌在课程里sectionstitle, videoUrl, duration, sort低不单独建集合由 sections 数组承载progressuserId, courseId, sectionId, position, completed高学习进度独立成集合按用户 课时高频 upsert这里最容易走偏的是把「学习进度」也塞进 courses 文档比如在课时对象上加一个 watchedUsers 数组。这种设计在演示阶段看不出问题一旦多个用户同时看同一节课频繁更新同一个课程文档轻则写入互相覆盖重则触发文档锁竞争。正确做法是 progress 按 userId courseId sectionId 建唯一索引每个用户每个课时只保留一条记录position 记录播放到第几秒completed 标记是否看完updatedAt 用于「继续学习」列表排序。2.3 用 mongoose schema 把数据模型固定下来// server/src/models/schemas.js const mongoose require(mongoose); // 课时内嵌在课程文档中打开课程详情页一次查询即可拿全目录 const sectionSchema new mongoose.Schema({ title: { type: String, required: true }, videoUrl: { type: String, required: true }, // 存 m3u8 或 mp4 地址 duration: { type: Number, default: 0 }, // 单位秒 sort: { type: Number, default: 0 }, }, { _id: true }); const courseSchema new mongoose.Schema({ title: { type: String, required: true }, category: { type: String, index: true }, cover: String, sections: [sectionSchema], }, { timestamps: true }); // 学习进度独立成集合避免高频更新打爆课程文档 const progressSchema new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: User, required: true }, courseId: { type: mongoose.Schema.Types.ObjectId, ref: Course, required: true }, sectionId: { type: mongoose.Schema.Types.ObjectId, required: true }, position: { type: Number, default: 0 }, // 播放进度单位秒 completed: { type: Boolean, default: false }, }, { timestamps: true }); // 一个用户在一门课的一个课时下只能有一条进度 progressSchema.index({ userId: 1, courseId: 1, sectionId: 1 }, { unique: true }); module.exports { Course: mongoose.model(Course, courseSchema), Progress: mongoose.model(Progress, progressSchema), };代码里最值得留意的不是字段本身而是 sectionId 的类型。内嵌子文档的 _id 同样是 ObjectId在 progress 集合里引用它时必须显式用 mongoose.Schema.Types.ObjectId 声明。否则前端传过来的是字符串后端直接拿字符串去查 ObjectId 字段永远匹配不上接口返回空数组还不报错这是 Mongo 查询里非常隐蔽的「类型不一致」问题。另外 timestamps 选项会自动维护 createdAt 和 updatedAt做「最近学习课程」排序时可以直接靠 updatedAt 倒序不必手工写时间戳。3. 环境配置与项目初始化从 Node.js 安装到前后端同时跑起来3.1 Node.js 安装与环境配置以及 npm 执行策略报错起步先装 Node.js建议 18 及以上 LTS 版本。Windows 安装包默认会把 node.exe 写进系统 PATH装完新开一个终端执行 node -v 和 npm -v 能同时看到版本号才算成功。如果提示「node 不是内部或外部命令」去系统环境变量里检查 PATH 是否包含 Node.js 安装目录手动解压的绿色版还要自己新建 NODE_HOME 并绑定到 PATH这一步很多人会漏。装完 Node 后在 PowerShell 里执行 npm 命令很容易碰到这句npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。这不是 npm 坏了是 PowerShell 的脚本执行策略默认阻止 .ps1 脚本。处理方式是在 PowerShell 里执行Set-ExecutionPolicy -Scope CurrentUser RemoteSigned这个策略的含义是本机创建的脚本可以运行从外部下载的未签名脚本会被拦截。它不会降低系统整体安全性只是允许当前用户在本地开发时执行 npm 自带的 PowerShell 包装脚本。执行完重新打开终端npm -v 就能正常工作。如果公司电脑的管理策略锁死了 ExecutionPolicy换用 CMD 终端运行 npm 也可以绕开这个限制。3.2 前端 Vue 项目与后端 Node.js 服务的最小骨架源码包里即使已经带了代码也建议先搞清楚标准目录长什么样后面排查问题才知道去哪一层找东西。常见做法是前端和后端分两个目录目录/文件作用server/src/app.jsExpress 应用入口挂载路由和中间件server/src/config/db.jsmongoose 连接 MongoDBserver/src/models/数据模型定义server/src/routes/按业务拆分的路由模块web/src/views/Vue 页面组件web/src/router/前端路由配置web/src/api/封装 axios 请求后端初始化用 npm 一把梭在 server 目录下执行npm init -y然后安装npm i express mongoose cors jsonwebtoken bcryptjs dotenv。express 负责 HTTP 接口mongoose 负责连库和模型cors 解决前端开发服务器的跨域限制jsonwebtoken 生成登录令牌bcryptjs 处理密码哈希。连接数据库的代码放在 config/db.js 里// server/src/config/db.js const mongoose require(mongoose); require(dotenv).config(); async function connectDB() { const uri process.env.MONGODB_URI || mongodb://127.0.0.1:27017/course_platform; await mongoose.connect(uri); console.log(MongoDB connected:, uri); } module.exports connectDB;连接串里推荐写127.0.0.1而不是localhost。Node.js 18 以上版本在部分系统上会把 localhost 解析成 IPv6 地址::1而 MongoDB 默认只监听 IPv4结果就是连接被拒绝报 ECONNREFUSED。改用 127.0.0.1 能直接避开这个坑。数据库名 course_platform 不存在也没关系mongoose 在第一次写入文档时会自动创建。前端用 Vite 创建 Vue 项目按官方脚手架走npm create vuelatest web选择 Vue Router 和 Pinia其余按需勾选。创建完成后先跑一次npm install再用npm run dev确认 8080 或 5173 端口能打开默认页面。前后端暂时是两条平行线下一步把它们串成一条启动命令。3.3 用 nodemon concurrently 让前后端一条命令跑起来开发时最烦的是手动开两个终端一个盯前端 Vite一个盯后端 Node。可以用 nodemon 做后端热重启用 concurrently 把两个进程合并到一条命令。// server/package.json 的 scripts 部分 { scripts: { dev: nodemon src/app.js, start: node src/app.js } }// 项目根目录 package.json { scripts: { dev: concurrently \npm run dev --prefix server\ \npm run dev --prefix web\ } }根目录先安装npm i -D concurrently --prefix对应的写法实际更直接的做法是在根目录执行npm i -D concurrently然后 scripts 里并排启动两个子命令。nodemon 监听 src 下文件变化自动重启改一行后端代码不用手动重启进程concurrently 默认会把两个进程的输出带前缀打在同一终端方便对照前后端日志。加了--kill-others-on-fail参数后任一进程退出会连坐杀掉另一个避免后端崩了前端还挂着。3.4 MongoDB 安装失败排查与 Compass 连接验证MongoDB 7.0 在 Windows 上安装最常见的失败是安装到一半弹the installer has encountered an unexpected error。这个报错覆盖很多原因机器上残留了旧版 MongoDB 服务、安装目录没有写权限、安全软件拦截了服务注册。排查顺序建议是先用管理员运行安装包装到默认盘符失败后去临时目录找以 MB 开头或 setup 命名的安装日志搜索 error 关键字定位失败阶段再到服务管理器里看有没有残留的 MongoDB 服务有的话用管理员权限的 CMD 执行sc delete MongoDB清掉最后重装。装完验证服务状态命令行执行mongosh --version确认客户端可用再看 Windows 服务列表里 MongoDB 是否处于 Running。如果服务起不来多半是 dbpath 指向的目录不存在去安装目录下的 mongod.cfg 里检查 dbPath 配置手动创建对应目录再启动服务。MongoDB 跑起来之后推荐装一个 MongoDB Compass 图形客户端。连接串填mongodb://127.0.0.1:27017能看到 course_platform 数据库以及其中的集合。源码包里如果自带数据文件导入前先用 Compass 看清集合名和样例字段能少走很多弯路。4. 核心业务闭环登录鉴权、课程列表与 m3u8 视频播放4.1 接口设计先于编码课程站后端 API 一览写业务代码前先把接口清单列出来前端路由参数、后端路径参数、MongoDB 查询条件三处才能对齐。精品课程站核心接口如下方法路径说明是否需登录POST/api/auth/register注册用户返回用户信息否POST/api/auth/login登录返回 JWT否GET/api/courses课程列表支持 ?category 过滤否GET/api/courses/:id课程详情含 sections 章节数组否POST/api/progress写入或更新学习进度是GET/api/progress/:courseId获取当前用户在一门课上的全部进度是注意 order 的顺序/api/courses/:id里的参数名要和 Vue 路由里route.params.id保持一致前端跳转课程详情页时传的就是这个 id。如果前端写route.query.courseId而后端读req.params.id两边各说各话接口返回 404 还不容易察觉。4.2 JWT 登录与路由守卫Vue 路由拦截器的落地代码登录接口返回的是一串 JWT后续所有需要身份的接口都要在请求头里带上它。后端写一个中间件统一做校验// server/src/middleware/auth.js const jwt require(jsonwebtoken); module.exports function auth(req, res, next) { const header req.headers.authorization || ; const token header.startsWith(Bearer ) ? header.slice(7) : null; if (!token) return res.status(401).json({ message: 未登录 }); try { req.user jwt.verify(token, process.env.JWT_SECRET || dev-secret); next(); // 校验通过req.user 里含 userId后续路由直接使用 } catch (e) { res.status(401).json({ message: 登录已过期请重新登录 }); } };req.headers.authorization的惯用格式是Bearer token代码里先判断前缀再截取第七位之后的内容拿到纯 token。校验成功后把解析出的用户信息挂到 req.user 上后面的路由处理函数直接从 req.user.id 取用户身份而不是相信前端传来的 userId。这是安全边界如果写进度时用的是前端传的 userId别人改一个请求参数就能往任意用户头上写数据。前端在 vue-router 里加一个全局前置守卫保护需要登录的页面// web/src/router/index.js router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { // redirect 参数让登录成功后能跳回原目标页面 next({ name: login, query: { redirect: to.fullPath } }); } else { next(); } });配套在 axios 封装里加请求拦截器每次发请求自动携带 token// web/src/api/request.js import axios from axios; const request axios.create({ baseURL: http://localhost:3000/api, }); request.interceptors.request.use((config) { const token localStorage.getItem(token); if (token) config.headers.Authorization Bearer ${token}; return config; });要说明的是路由守卫只能控制前端页面跳转它挡不住别人直接 curl 后端接口真正的鉴权必须以后端 auth 中间件为准。前端守卫是体验层面的拦截后端中间件是数据层面的底线两层都要有。4.3 课程详情页与 vue 播放 m3u8 的两种方式精品课程的视频通常不是 MP4 直链而是 HLS 协议的 m3u8 地址里面是一串 ts 分片文件的索引。浏览器原生 video 标签支持 MP4却不支持 m3u8Safari 除外所以 Vue 里播放 m3u8 要引第三方库hls.js 是常见方案。!-- web/src/components/M3u8Player.vue -- template video refvideoRef controls classvideo-player/video /template script setup import { ref, onMounted, onBeforeUnmount } from vue; import Hls from hls.js; const props defineProps({ src: { type: String, required: true }, }); const videoRef ref(null); let hls null; onMounted(() { const video videoRef.value; if (Hls.isSupported()) { hls new Hls(); hls.loadSource(props.src); hls.attachMedia(video); } else if (video.canPlayType(application/vnd.apple.mpegurl)) { // Safari 走原生 HLS 能力直接把地址交给 video video.src props.src; } }); onBeforeUnmount(() { if (hls) hls.destroy(); // 释放 hls.js 内部创建的 blob URL 和网络请求 }); /scripthls.js 的原理是用 Media Source Extensions 把 ts 分片拼成视频流喂给 video 元素所以组件卸载时一定要调用 hls.destroy()否则页面切走之后浏览器还在继续拉分片造成网络请求泄漏。另一个可行的替代是 video.js它内置了对 HLS 的支持区别在于引入方式import video.js/dist/video-js.css之后调用player.src({ src: props.src, type: application/x-mpegURL })。播放地址的跨域问题不在这里展开只需要知道切片资源所在的服务器必须允许请求来源否则视频会卡在加载状态但控制台只有一条跨域报错。4.4 学习进度写入嵌套文档还是独立集合以及 upsert 的正确用法播放页通常要上报两个事件播放过程中周期性上报当前秒数播放结束上报 completed。后端对应一个写接口用 findOneAndUpdate 配合 upsert 做到「第一次插入之后更新」// server/src/routes/progress.js const express require(express); const router express.Router(); const { Progress } require(../models/schemas); const auth require(../middleware/auth); // 写入/更新当前用户在某课时的学习进度 router.post(/, auth, async (req, res, next) { try { const { courseId, sectionId, position, completed } req.body; const progress await Progress.findOneAndUpdate( // 查询条件三个字段定位唯一记录 { userId: req.user.id, courseId, sectionId }, // 更新部分position 和 completed 每次覆盖 { $set: { position, completed } }, // upsert: 不存在则插入new: 返回更新后文档 { upsert: true, new: true } ); res.json(progress); } catch (error) { next(error); } }); module.exports router;upsert 解决的是「先 find 再 save」两步操作的竞态问题。两个请求同时发现记录不存在先后插入两条唯一索引直接报错upsert 在数据库层面原子完成不会出重复数据。另外还要注意查询条件里只能用 req.user.id不能用 req.body.userId前面已经强调过。类似场景的查询习惯也值得对比Mongoose 的Model.find({})对应 Spring Data 的 findAll()findOne({ userId: xxx })对应按条件取单条都是传入普通对象当条件不需要链式 where。还有个影响性能的小细节只读接口不返回 mongoose 文档时加一个.lean()让查询直接返回普通 JSON 对象能省掉文档对象实例化的开销。比如课程列表接口可以写成Course.find({ category }).lean()前端拿到的是干净对象序列化也更省时。5. 拿到源码包后的验收清单数据导入、接口自测与打包路径修复5.1 用 mongoimport 和 Compass 把初始数据灌进 MongoDB源码包里通常带 data 目录里面是导出的 JSON 或 CSV 文件。导入命令要指定数据库名和集合名且集合名必须与代码里 mongoose.model 注册的名称一致否则接口查到的还是空集合。mongoimport --db course_platform --collection courses --file data/courses.json mongoimport --db course_platform --collection users --file data/users.json导入完成后打开 Compass点开 courses 集合确认文档里的字段名和 schema 对得上注意看 _id 是不是 ObjectId 类型。如果导入后发现嵌套的 sections 数组变成字符串通常是 JSON 里写了_id: {$oid: ...}这种格式导出工具版本不同会漏转换在 Compass 里手动改几条对比最直观。5.2 一条 curl 验证 JWT 全链路前后端都启动后先用 curl 打后端接口排除前端因素确认接口本身是通的。curl -X POST http://localhost:3000/api/auth/login \ -H Content-Type: application/json \ -d {email:demoexample.com,password:123456}拿到返回的 token 后再带 token 请求受保护的进度接口curl http://localhost:3000/api/progress/660000000000000000000001 \ -H Authorization: Bearer 粘贴token这一步能一次性暴露三类问题401 说明 token 没带对或已过期404 说明路径参数不符检查路由里冒号参数名是否和前端一致500 说明业务代码报错回到后端终端看堆栈。全部通过之后前端页面再报错问题就缩小到前端网络层或组件层。5.3 视频播放验证与 vue 打包后布局异常的处理本地npm run dev一切正常打成静态包部署到服务器后常常出现白屏或样式全丢这就是 vue 打包后布局异常的高发场景。最常见原因是 Vite 默认把静态资源路径写成绝对路径/assets/xxx.js部署到子目录后请求落在域名根目录自然 404。// vite.config.js import { defineConfig } from vite; import vue from vitejs/plugin-vue; export default defineConfig({ base: ./, // 改成相对路径dist 可以放到任意子目录 plugins: [vue()], });base: ./会把资源路径变成相对当前页面目录对直接把 dist 目录扔进 Nginx 子路径的部署方式最省心。同时还要检查 vue-router 是不是用了 history 模式history 模式刷新页面时服务器找不到对应的前端路由需要 Nginx 配置 fallback不想改服务器配置就退回 createWebHashHistory代价是 URL 里多个 # 号。验证时打开浏览器控制台的 Network 面板看 .js 和 .css 文件是否全部 200再用装好的 Vue DevTools 插件检查当前路由组件是否按预期渲染这一步比反复刷新页面盲猜更快。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价