ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Node.js + Vue 构建前后端分离校园足球赛事平台

Node.js + Vue 构建前后端分离校园足球赛事平台 前段时间帮人搭了一套校园足球比赛网站后端用的node.js前端用的vue。整体折腾下来差不多两个星期不算复杂但这个项目非常典型前后端分离、用户权限、赛程状态流转、报名数据处理几乎把Web开发日常要碰到的知识点都串了一遍。如果你正准备做类似的系统或者想找一个难度适中但五脏俱全的前后端分离项目练手这篇东西应该能给你省下不少弯路。先说清楚这个网站到底解决什么问题。校园足球赛的痛点其实很具体比赛信息靠群公告口口相传、报名靠人工登记、赛果统计散落在各个Excel里。做一个统一平台把赛事发布、报名、赛程查询、球队球员管理、结果归档全串起来这就是系统的核心价值。适合的人群也很明确——课程设计/毕业设计的学生、想入门前后端分离开发的初学者、还有校园里真正想把这套东西用起来的体育部门或学生社团。1. 校园足球比赛网站到底要做什么1.1 从使用场景倒推核心功能做这类系统最大的忌讳是一上来就写代码。我习惯先列用户再把每个用户的使用场景走一遍然后倒推功能。这套系统里用户大致分三类普通学生、球队相关人员、后台管理员。学生要什么看比赛列表、看某场比赛详情、自己报名参赛、查看报名的状态。球队相关人员要什么维护球队信息、维护球员名单、查看自己球队的赛程。管理员要什么创建比赛、设置报名时间段、录入比分和赛果、审核报名、管理用户。基于这些场景功能模块就很清晰了。我习惯用一张表把模块和页面入口对应起来方便理清前端路由和后端接口的边界。模块核心功能主要页面对应赛事信息发布比赛、列表展示、详情页首页 / 赛事列表 / 赛事详情赛程管理按日期安排赛程、状态流转赛程列表按轮次/日期分组报名系统个人/团队报名、重复报名校验赛事详情页内嵌报名入口球队球员球队信息维护、球员归属球队列表 / 球队详情用户权限注册登录、管理员身份识别登录页、个人中心后台管理赛事审核、比分录入、用户管理后台管理布局页这里有一个容易被忽略的点比赛是有状态机的。一场比赛从创建到结束至少要经历“待开始 / 报名中 / 进行中 / 已结束”几个阶段。比如管理员创建比赛后一开始是“待开始”到了报名时间自动或手动切到“报名中”比赛日当天切到“进行中”录入比分后进入“已结束”。这个状态字段不做好后面赛程展示、报名开关都会写成一团浆糊。1.2 报名逻辑的细节设计报名是整个系统里最容易翻车的部分。个人赛好办用户点一下报名就完事。团队赛就麻烦了一支队伍由谁报名、队员名单谁维护、能不能中途换人这都需要提前定好规则。我的建议是先定义一个简单的规则再考虑扩展。比如第一版就做“个人报名”每名用户对同一场比赛只能报名一次后端做唯一索引或查询校验。团队类比赛单独建一个“球队申请”流程由队长提交队员ID列表管理员在后台审核。千万不要在一开始就把两种报名流搅在一起后面排查问题会非常痛苦。另外报名时间窗口也要想清楚比赛结束前能不能取消报名报名截止后管理员手动关闭报名入口这些建议加一个简单的状态判断而不是只靠前端隐藏按钮。前端隐藏只是体验问题后端必须能强制校验否则就等着被练手的人造数据搞崩。2. 为什么是Node.js Vue这套组合2.1 后端Node.js到底适合干什么选技术栈时很多人会犹豫要不要上Spring Boot。我只能说看场景。校园足球比赛网站这种业务数据量不大、并发不高、业务逻辑不深但迭代速度要求高、要快速上线看效果这就是Node.js的主场。它有几个实打实的优势第一是异步非阻塞I/O。虽然在这种项目里并发量低到根本用不上但Node.js的模型决定了它处理频繁的数据库读写、文件操作时很舒服配合Express写RESTful接口非常顺。第二是npm生态几乎每个功能都能找到现成库mongoose操作MongoDB、jsonwebtoken做登录鉴权、cors处理跨域装完就能用不用自己造轮子。第三是部署轻一个小服务器、一个pm2进程就能跑起来不像Java那套要配Tomcat或者Spring Boot内嵌容器还要考虑内存占用。当然Spring Boot在复杂业务、强类型约束、企业级规范上有不可替代的优势。但你要是拿它来做这个校园项目光实体类和配置就要写一大堆性价比不高。技术选型的关键是匹配业务复杂度而不是越重越好。2.2 前端Vue为什么这么搭前端选Vue理由更直接组件化的开发方式特别适合这种信息展示加表单操作的网站。比赛列表、赛事卡片、报名按钮、倒计时组件定义一次到处复用。Vue Router管页面跳转Pinia管全局状态登录状态、用户信息、报名状态Element Plus提供现成的表格、表单、弹窗组件后台管理页面半小时就能搭出个能看的骨架。这套组合最舒服的地方是渐进式。你不需要一开始就把Vue全家桶全部铺上去。核心页面用单文件组件就能跑。等做到后台管理发现需要更统一的状态管理、更复杂的路由控制时再引入Pinia、动态路由这一层完全来得及。2.3 一门语言贯穿前后端省下的不只是写代码时间很多人忽略了一点Node.js和Vue都用JavaScriptVue底层是JavaScript/TypeScript但上手时还是JavaScript为主这意味着你可以用一套语言同时写后端接口和前端页面。调试成本直接降一半后端返回的数据结构你一眼就能看懂不用担心“这个字段Java那边叫createTime前端这边要写成created_at”这种割裂问题。对一个人全栈开发这种校园项目来说这个优势尤其明显。你不用在两种语言之间反复切换上下文写完后端接口直接写前端调用对数据结构的理解是连贯的。这一条我觉得就足够说服很多单兵作战的人选这套组合了。3. 数据模型设计与前端路由规划3.1 用MongoDB来描述这个业务为什么舒服项目里我选了MongoDB做存储配合mongoose操作。有人会问为什么不用MySQL我的回答很简单这个业务的实体关系没那么硬用文档模型反而更自然。比赛信息、球队信息、用户信息每一块的字段都可能随需求微调。MongoDB不要求建表时把字段固定死改起来成本低JSON结构直接对应JS对象存取都不用做太多转换。用户表users是最基础的用户名、密码加密后的hash、角色字段student/admin、所属球队ID、手机号、学号等。比赛表matches是核心标题、比赛类型、状态、开始时间、地点、报名截止时间、参赛球队或选手、创建者。报名表signups维护用户与比赛的关联用户ID、比赛ID、报名时间、状态。这种_id引用连表的方式读起来直观写起来也快。const mongoose require(mongoose) const MatchSchema new mongoose.Schema({ title: { type: String, required: true }, type: { type: String, enum: [联赛, 友谊赛, 淘汰赛], default: 友谊赛 }, status: { type: String, enum: [pending, signup, ongoing, finished], default: pending }, startTime: { type: Date, required: true }, place: String, teams: [{ type: mongoose.Schema.Types.ObjectId, ref: Team }], creator: { type: mongoose.Schema.Types.ObjectId, ref: User } }, { timestamps: true }) module.exports mongoose.model(Match, MatchSchema)这里特别要注意两个字段的设计。status字段是整个业务流转的中枢前端展示、报名按钮开关、赛程列表分组全都依赖它。teams字段用的是ObjectId数组引用如果有球队参与就关联球队如果是个人赛就留空。这种灵活度在MySQL里你得额外建一张中间表在MongoDB里一个数组字段就解决了省下的工作量是肉眼可见的。3.2 前端路由规划先想清楚用户怎么走前端路由不光是定义几个URL它决定了用户怎么在这个网站里完成整个旅程。我的做法是把用户路径画出来未登录用户能看什么、登录后能做什么、管理员从哪里进后台。按这个思路路由表大概是这样的import { createRouter, createWebHistory } from vue-router import Home from ../views/Home.vue const router createRouter({ history: createWebHistory(), routes: [ { path: /, name: home, component: Home }, { path: /matches, name: matches, component: () import(../views/MatchList.vue) }, { path: /matches/:id, name: match-detail, component: () import(../views/MatchDetail.vue), props: true }, { path: /teams, name: teams, component: () import(../views/TeamList.vue) }, { path: /my, name: my, component: () import(../views/MySignups.vue), meta: { requiresAuth: true } }, { path: /login, name: login, component: () import(../views/Login.vue) }, { path: /admin, name: admin, component: () import(../views/admin/AdminLayout.vue), meta: { requiresAuth: true, role: admin } } ] })路由参数在这里体现得很明显。赛事详情页用/matches/:id点击列表里的某场比赛通过router.push({ name: match-detail, params: { id: match._id } })跳过去在详情页里用route.params.id拿到比赛ID再调后端接口拿详情。这种写法比在跳转时用query传一大堆字段干净得多。权限控制我建议用路由meta加导航守卫而不是去做复杂的动态路由注册。校园项目后台就几个页面没必要在用户登录后动态塞路由那样反而增加排查难度。在全局router.beforeEach里判断requiresAuth和role不满足就跳登录页简单直接又够用。后台管理页面建议直接放在一个AdminLayout.vue布局组件下里面用嵌套路由/admin/matches、/admin/users、/admin/teams侧边栏菜单统一管理新增功能时只要加一个子路由改动成本很小。4. 从零搭建Node.js与Vue环境配置4.1 装对Node.js别一上来就撞版本坑环境配置是整个项目里最劝退新手的一步。安装Node.js不是下载最新版就行而是要选对版本。Node.js的版本节奏非常快新版本对旧模块的兼容性不一定好有些老项目的原生依赖在高版本上直接编译失败。我的建议是装Node.js 18.20.4这个LTS版本稳定、兼容性好绝大多数开源库都能跑。比直接装固定版本更推荐的方式是用nvm管理多个node版本。开发中你可能同时接触老项目和Vue3新项目不同项目对node版本要求不一样nvm可以随时切换省得反复系统级卸载安装。装好之后先跑一下node -v确认安装成功这也是排查环境问题的最基本操作。# 安装nvmWindows直接下载nvm-setup.exe curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # 安装并使用指定版本 nvm install 18.20.4 nvm use 18.20.4 # 验证 node -v npm -vnpm默认源在国内拉取依赖比较慢建议先配置成npmmirror镜像源。这一步不是必须的但会明显提升安装体验尤其是后面执行npm install时差别是几十分钟和几分钟的对比。npm config set registry https://registry.npmmirror.com4.2 用脚手架初始化Vue项目前端项目我推荐直接用Vite的官方脚手架创建也就是create-vue。Vite最大的优势是开发服务器启动快热更新也快写代码时体验比旧一代的webpack方案强太多。有些老教程还会让你用vue-cli那个在新的技术栈下没必要再坚持除非你要维护老项目。# 创建项目项目名避开中文和特殊符号 npm create vuelatest # 按提示选择要集成的功能 # 建议勾选Vue Router、Pinia、ESLint # 其余的生产环境特性按需选择 cd vue-campus-football npm install这个脚手架会帮你把Vue Router、Pinia这些核心依赖全部配好目录结构也清晰省去了手动配置一大半的麻烦。装完后再补几个常用依赖axios用于请求后端接口element-plus用于后台管理界面。npm install axios element-plus4.3 开发环境里必备的几件套一个是Vue Devtools插件。没有它你调试Vue组件状态的时候会非常痛苦。它可以在浏览器里直接看组件树、props、data、路由状态、Pinia store的数据定位“这个字段为什么没显示”之类的问题异常高效。Chrome扩展商店直接装就行装完记得在Vue应用启动时确保处于开发模式生产环境下插件默认不启用。另一个是Postman或Apifox这类接口调试工具。写后端接口时先不碰前端页面直接把接口调通能确认大部分问题出在后端还是前端。还有一个容易忽略的点统一用ESModule或CommonJS规范不要混着来。Vite的Vue项目默认用ESModule后端Node.js项目很多教程还在用CommonJS两者的导入导出语法不一样。初期学习阶段允许各用各的但心里要有这个意识后面想写同一个项目共享代码时就是隐形的坑。5. 核心接口开发与联调实操5.1 先把接口定好赛事模块接口设计写后端之前我强烈建议先花半小时把接口文档列出来。不需要很正式一张表格就够了。想清楚请求方法、路径、参数、返回结构后面写代码就是在填空。赛事模块最核心的接口大概是下面这些接口路径方法说明权限/api/matchesGET获取赛事列表支持状态筛选公开/api/matches/:idGET获取赛事详情公开/api/matchesPOST创建比赛管理员/api/matches/:idPUT更新比赛信息/录入比分管理员/api/matches/:id/registerPOST报名比赛登录用户/api/matches/:id/registerDELETE取消报名登录用户返回结构我习惯统一用{ code, message, data }。code为0表示成功非0是业务错误比如重复报名、比赛已截止。这样前端axios拦截器里统一判断code不需要每个接口单独处理错误分支。Express接口写起来非常直接。创建比赛的接口核心就是取出请求体数据、生成一个文档、校验一下管理员身份。const express require(express) const router express.Router() const Match require(../models/match) // 创建比赛 router.post(/, async (req, res) { try { const match await Match.create(req.body) res.status(201).json({ code: 0, message: created, data: match }) } catch (e) { res.status(400).json({ code: 1, message: e.message }) } }) // 比赛列表支持按状态筛选 router.get(/, async (req, res) { const { status } req.query const filter status ? { status } : {} const matches await Match.find(filter).limit(50).sort({ startTime: -1 }) res.json({ code: 0, message: ok, data: matches }) })注意列表接口要加.limit()不然随着比赛越来越多一次性返回全量数据会让前端越来越卡。条件筛选用query参数而不是body是为了保证GET请求的语义正确也好做缓存。5.2 从Express到axios一次完整联调后端接口写好后前端通过axios发请求。先把axios实例封装一下统一baseURL、超时时间、token注入和错误处理这是每个Vue项目都应该做的事。import axios from axios import { useUserStore } from /stores/user const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器带上登录token request.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }) // 响应拦截器统一处理业务错误 request.interceptors.response.use( response { if (response.data.code ! 0) { // 业务级错误提示 return Promise.reject(new Error(response.data.message)) } return response.data }, error { // 网络错误或后端500 return Promise.reject(error) } ) export default request这里要特别说明一个开发环境的关键问题baseURL写成/api那么前后端联调时必须处理代理否则浏览器会因为跨域把请求拦截掉。两种主流方案开发环境在Vite配置代理生产环境用Nginx反向代理。Vite的代理配置在vite.config.js里export default defineConfig({ server: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } })生产环境Nginx配置同样简单location /api/ { proxy_pass http://127.0.0.1:3000/; }这样做的好处很直观浏览器里请求的是同源地址/api/matches开发时由Vite转发到后端部署时由Nginx转发到后端不用在后端代码里硬编码可跨域的域名环境迁移非常丝滑。5.3 登录权限这样接JWT一步到位登录模块我用JWT方案流程是用户提交用户名密码后端用bcrypt对密码做哈希校验校验通过后签发一个token返回给前端。前端把token存到Pinia和localStorage之后的请求都带上这个token。后端写一个中间件统一解析token并识别当前用户ID。const jwt require(jsonwebtoken) // 登录成功生成token function signToken(user) { return jwt.sign( { uid: user._id, role: user.role }, process.env.JWT_SECRET, { expiresIn: 7d } ) } // 鉴权中间件 function auth(req, res, next) { const token req.headers.authorization?.replace(Bearer , ) if (!token) return res.status(401).json({ code: 401, message: 未登录 }) try { const payload jwt.verify(token, process.env.JWT_SECRET) req.userId payload.uid req.userRole payload.role next() } catch (e) { res.status(401).json({ code: 401, message: 登录已过期 }) } }设置了JWT_SECRET这个环境变量千万别把它硬编码到代码里。写代码时用环境变量管理密钥配置一个.env文件并且确保.env被加进.gitignore。一个小细节JWT的过期时间我定的是7天校园网站的用户使用频率没商业应用那么高隔三差五过期重登太影响体验7天是个人试下来比较折中的选择。报名接口要带上auth中间件服务端拿到req.userId后再去查比赛状态和重复报名情况。前后端都要校验前端控制按钮显隐后端做最终兜底。这个习惯看起来老生常谈但在实际项目中忘了后端校验导致的数据脏了比单纯的按钮没隐藏要麻烦得多。6. 实战排坑环境配置与运行时的常见问题6.1 环境配置阶段的坑先说说我踩过的几个环境问题。npm install报错或者卡住。大概率是网络问题或者依赖源配置的问题。先确认registry是否配置成国内镜像其次把node_modules目录完整删掉重新装不要只删一半。遇到node-sass或者node-gyp这类原生模块编译失败先看Node版本是不是太高很多原生模块在Node 20以上的版本跑不动切回18.20.4基本能解决。打开项目提示Digital Envelope Routines::unsupported。这个坑很经典通常是Node高版本对旧的加密算法不再支持导致一些旧依赖启动时报错。解决办法就是降级到Node 18这种LTS版本或者升级项目依赖。别去改加密库的底层那是白费功夫。端口被占用报EADDRINUSE。项目跑起来后提示这个说明3000或5173端口被别的进程占了。找到占用进程关掉或者直接换个端口启动。Vite和Express都可以通过环境变量指定端口临时排查非常方便。6.2 联调运行阶段的坑跨域请求报错No Access-Control-Allow-Origin header is present。这类问题排查思路是先确认请求有没有到后端如果后端有日志输出说明已到达那就是响应头或者CORS中间件没配对如果请求根本没到后端那就是前端代理配置的路劲有问题。校园项目联调时这个错最多但排查链条很短基本就是代理和cors二选一。Vue Router用了history模式部署到服务器后刷新页面404。这个坑出现的原因是前端路由是浏览器端的刷新时浏览器会向服务器请求真实的URL路径而服务器上并不存在这个路径的文件于是返回404。解决办法是Nginx配一条try_files把所有请求回退到index.html由前端路由接管。上面那个Nginx配置片段已经写到了抄上就能用。Vue Devtools在页面里不显示。先确认是不是开发模式然后确认插件版本和Vue版本是否兼容。Vue 3项目需要新版DevtoolsVue 2项目要用另一个版本装错版本是常见原因。常见报错可能原因排查思路npm install失败网络源太慢、依赖冲突切换镜像源、删除node_modules重装启动报OpenSSL错误Node版本过高用nvm切到18.x LTS版本跨域请求被拦截代理配置错误/CORS缺失看后端日志确认请求是否到达history路由刷新404Nginx未做回退配置try_files指向index.html端口占用已有进程占用端口查进程并kill或换端口这些问题的共性是错误信息看着吓人但实际上都是环境或配置层面的东西。排错的时候不要慌按“请求有没有到达后端 → 后端有没有正确响应 → 前端有没有正确处理响应”这个顺序走大多数问题都能在三分钟之内定位。写在最后我个人在实际操作中的体会是这类前后端分离项目最值得花时间的不是写代码而是把数据模型和接口定义先想明白。我见过太多人一上来就写页面写到一半发现报名逻辑和比赛状态对不上只能推倒重来。我自己的习惯是先把数据库集合建好、字段定下来再写接口文档最后才动手写前端页面。前端先用假数据把页面调通等后端接口真正可用再去替换两边并行又互不耽误。这个项目后续能扩展的方向其实不少生成比赛Excel报表、赛程冲突检测、移动端适配、甚至用electron打包成桌面版给体育组用。但第一版不必贪多把赛事、报名、权限这三条主线理顺系统就已经有了骨架后面加功能都是往上面挂肉的事。真要做毕设或者参加比赛多琢磨琢磨“赛程冲突检测”这种有业务深度的点比堆功能更出效果。
返回列表