简介:这是一套面向Web全栈开发学习者与毕业设计参考者的MOBA游戏攻略分享平台完整项目源码,聚焦于游戏社区类应用的前后端协同实现。项目采用Spring Boot构建后端服务,集成MySQL持久化存储,并支持JSP与Vue双前端方案,兼顾传统服务端渲染与现代单页应用交互体验,适合Java Web与前端进阶实践。压缩包共814个文件,含115个Java业务逻辑与控制器代码、45个Vue组件(涵盖导航、面包屑、用户中心等核心模块)、164个JS交互脚本、79个GIF动效资源及53个CSS样式文件,整体大小26.78MB,结构清晰、模块划分明确。已有29373人学习下载,资源中保留了.bak备份文件与多套批处理脚本(如install.bat、run.bat),便于环境快速搭建与版本回溯;配套SQL建表语句、YML配置及完整目录层级,可直接部署运行并深入理解MOBA类社区系统的权限管理、攻略发布与用户互动等典型业务流程。 我从一个实际做完并打包成 lw.zip 的前后端分离项目说起——基于 Vue 的 MOBA 类游戏攻略分享平台。这个项目本身不算“大”,但涉及的链条很长:Vue 组件化开发、路由设计、状态管理、后端接口对接、跨域处理、打包部署,最后还要把整个工程压缩成 zip 交付或分发。任何一个环节没处理好,后面都会连环踩坑。这篇文章就是把我从零到一实现这个平台的过程,以及过程中踩过的坑、验证过的方案,全部沉淀下来,希望能给正在做 Vue 项目实战的同学一些能直接抄作业的参考。
这项目适合谁看?如果你正在学 Vue 但只停留在“照着视频敲代码”,没真正从头规划过一个完整项目;或者你刚接触前后端分离,想搞明白 Vue 项目从开发到打包再到 zip 发布的全流程;又或者你准备做毕设、作品集、面试项目——这篇内容都可以给你一个相对完整的参照系。
1. 项目整体设计与技术选型思路
1.1 需求定位:MOBA 攻略平台到底需要什么
先说项目背景。MOBA 类游戏(比如英雄联盟、DOTA2、王者荣耀这类)有一个很核心的玩家需求:查攻略。这个需求细拆下来其实有三层:一是英雄资料,包括技能介绍、出装推荐、符文/天赋搭配;二是玩法攻略,包括对线技巧、团战思路、版本强势英雄排行;三是个人数据,比如自己常用英雄的胜率、场次记录。
所以这个平台的前端核心模块可以归纳成四个:英雄库、攻略列表、用户系统、个人数据中心。
英雄库要解决的是“查得快”,所以列表页要有搜索、筛选、排序;攻略列表要解决“看得爽”,所以要有分页、分类、富文本渲染;用户系统要解决“进来能玩”,所以要有注册登录、Token 鉴权;个人数据中心要解决“数据看得懂”,所以要用图表展示胜率趋势、英雄使用频率。
这几个需求组合在一起,用 Vue 来落地是非常合适的。组件化开发天然适合这种“页面多但结构相似”的场景——英雄卡片、攻略卡片、评论列表、筛选栏,这些都可以抽象成独立组件复用,避免每个页面都复制粘贴一大段模板代码。
1.2 为什么选 Vue 而不是 React 或者原生 JS
这可能是很多人在项目启动前纠结的问题。我当时的考虑很简单直接:Vue 的上手曲线比 React 平缓,对新手友好;模板语法贴近传统 HTML,团队里如果有人之前写 jQuery 或原生 JS,迁移成本很低;而且 Vue 的响应式系统是“自动追踪依赖”的思路,写业务代码时不需要手动 memo,心智负担小很多。
如果拿原生 JS 实现同样的功能,光是英雄列表的重新渲染——每次搜索关键词变化都要手动操作 DOM、拼接字符串、绑定事件——代码量至少翻一倍,而且维护起来非常痛苦。Vue 的响应式机制把“数据变化 -> 视图更新”这条链路封装掉了,开发效率高得多。
这里不是捧一踩一,React 有 React 的优势,但在这个以内容展示和表单交互为主的攻略平台上,Vue 的模板语法和计算属性能让业务代码更直接。尤其是后面做胜率趋势图表时,computed 属性对数据的派生计算非常顺手——我只要维护原始数据源,所有展示层的计算都交给 computed 去自动推导,不用手动同步。
1.3 前后端分离架构与项目目录规划
项目最终采用的是前后端分离架构:前端 Vue 3 + Vite,后端 Spring Boot(如果你不想上 Java,用 Node.js + Express 或者 Python FastAPI 也完全可以,核心逻辑是一样的),数据库用 MySQL。前后端通过 RESTful API 通信。这种架构的好处是前端开发和后端开发可以并行推进,而且部署时可以分开——前端打包成静态文件丢给 Nginx,后端按服务方式跑。
前端目录结构我按“功能模块 + 资源类型”的方式组织,这样一个人维护也不会乱:
src/ api/ # 接口请求封装 hero.js article.js user.js assets/ # 静态资源 components/ # 公共组件 HeroCard.vue ArticleCard.vue Pagination.vue SearchBar.vue router/ # 路由配置 index.js store/ # 状态管理(我用的 Pinia) user.js hero.js views/ # 页面级组件 Home.vue HeroList.vue HeroDetail.vue ArticleList.vue ArticleDetail.vue Login.vue Register.vue Profile.vue utils/ # 工具函数 request.js auth.js App.vue main.js这个结构看起来简单,但实践下来有几个好处:api 目录单独抽象,页面里不直接写 axios 请求;components 目录放复用组件,views 目录只做“页面组装”,业务逻辑尽量下沉到 composables 或者 store 里。
2. 核心业务模块实现与关键技术点
2.1 英雄列表页:组件拆分与搜索筛选
英雄列表页是这个平台的门面。需求就一句话:把游戏里所有英雄展示出来,支持按名字搜索、按定位筛选(战士、法师、辅助等)、按难度排序。但实现时牵扯到一个核心问题——怎么让搜索和筛选的效率最高。
我在这里用的是computed+ref的组合。搜索关键词、筛选条件都定义成ref,展示数据则是一个computed,它把原始英雄列表(从接口拿到的全量数据)和当前搜索条件做匹配,返回过滤后的结果。
const heroes = ref([]) const keyword = ref('') const category = ref('全部') const filteredHeroes = computed(() => { let list = heroes.value if (category.value !== '全部') { list = list.filter(h => h.category === category.value) } if (keyword.value.trim()) { const kw = keyword.value.trim().toLowerCase() list = list.filter(h => h.name.toLowerCase().includes(kw)) } return list })这里有个很关键的思考:为什么要用 computed 而不是在搜索的时候手动执行一个 filter 方法并赋值给另一个 ref?因为 computed 有缓存,只有依赖项(keyword、category、heroes)变化时才会重新计算。如果手动方法执行,性能上差距不大,但代码里多了一个“需要手动同步”的状态,一旦哪天某个触点忘了更新视图,就会出现“数据改了页面没变”的诡异 bug。computed 把这种问题从根上杜绝了。
组件拆分上,我抽了SearchBar.vue(输入框 + 下拉框)和HeroCard.vue(英雄卡片)。父组件负责数据管理和状态协调,子组件只管展示和抛事件。子组件通过defineProps接收数据、通过defineEmits向外通知,这个模式一目了然:
<!-- SearchBar.vue --> <template> <div class="search-bar"> <input v-model="keywordLocal" placeholder="搜索英雄名称" /> <select v-model="categoryLocal"> <option>全部</option> <option>战士</option> <option>法师</option> <option>辅助</option> </select> </div> </template> <script setup> defineProps({ keyword: String, category: String }) const emit = defineEmits(['update:keyword', 'update:category']) </script>这种拆分的好处从第二次写页面时就开始显现了。后面做攻略列表页,搜索栏和筛选栏几乎可以复用SearchBar的套路,只是字段不同。如果不做组件化,那就是两份几乎一样的代码互相复制——改一个 bug 要改两遍。
2.2 英雄详情页:动态路由与参数处理
英雄详情页的 URL 设计为/hero/:id,这样每个英雄都有独立地址,方便分享。Vue Router 里配置动态路由:
{ path: '/hero/:id', name: 'HeroDetail', component: () => import('../views/HeroDetail.vue') }在详情页里获取路由参数,最直接的方式是route.params.id。但这里有一个坑:同一个组件被复用时——比如从“法师英雄列表”点进某个英雄,再返回列表点另一个英雄——路由参数变了,但组件实例不会重新创建,onMounted里的初始化请求不会重新执行。这是新手最容易踩的坑。
解决办法是在HeroDetail.vue里监听路由参数变化:
import { watch } from 'vue' import { useRoute } from 'vue-router' const route = useRoute() const heroDetail = ref(null) const fetchHeroDetail = async (id) => { const res = await heroApi.getDetail(id) heroDetail.value = res.data } watch( () => route.params.id, (newId) => { fetchHeroDetail(newId) }, { immediate: true } )这里immediate: true让组件首次挂载就会请求一次,之后参数变化也会重新请求,覆盖了所有场景。这也是我在实际开发中比较喜欢用的模式——别在onMounted里做数据初始化,用watch + immediate更稳妥。
顺带提一下路由传参的另一个容易搞混的点:query和params的区别。query 是写在 URL 问号后面的,比如/search?keyword=ez,刷新页面后依然存在;params 是路径参数的一部分,比如/hero/1,刷新后也在。但如果用router.push({ name: 'HeroDetail', params: { id: '1' } })这种方式且配置了 props 接收,刷新页面时参数仍然在 URL 上,所以不会丢。真正会丢参数的是那些“靠组件内部状态记录的筛选条件”,后面问题排查部分我会着重讲。
2.3 攻略文章模块:富文本渲染与分页
攻略模块的核心是“文章列表 + 文章详情”。列表页数据是后端接口返回的,带分页信息。这里要注意分页参数的设计:我向后端传pageNum(页码)和pageSize(每页大小),后端返回total总数和list数据数组。
前端封装了Pagination.vue组件,通过v-model:page和v-model:size的方式绑定额外参数:
<Pagination v-model:page="pageNum" v-model:size="pageSize" :total="total" @change="loadArticles" />v-model的用法可能对刚接触 Vue 的人有点神秘,说白了就是语法糖:v-model:page等价于:page="pageNum" @update:page="pageNum = $event"。子组件里通过defineProps接收page,通过defineEmits抛update:page和update:size,当用户点击页码时抛事件,父组件收到后更新页码并重新请求数据。
文章详情页用的是富文本编辑器(我推荐 wangEditor 或者 Quill,看团队熟悉度)保存的 HTML 内容,前端直接用v-html渲染。这里有两个注意事项:一是要把<style>的作用域限制好,防止文章内容里的样式污染全局样式;二是v-html渲染的内容里如果有图片,要确认图片 URL 是否能正常访问——后端返回的富文本里存的可能是相对路径,需要前端拼接出完整地址。我在这个项目里写了一个过滤器,把文章内容里的src="/upload/xxx.jpg"替换成https://api.lw.com/upload/xxx.jpg这种完整路径。
2.4 登录注册与权限控制:Token 方案落地
用户系统是很多功能的基础。注册、登录、个人中心,这套逻辑很通用。我采用的方案是:用户提交用户名密码,后端校验通过后签发一个 Token(我用的 JWT),前端把 Token 存在localStorage里,后续每个请求头都带上Authorization: Bearer <token>。后端根据 Token 判断当前用户身份。
请求的封装是整个 Token 方案落地的关键。我用 axios 封装了一个request.js,核心逻辑是请求拦截器里加 Token,响应拦截器里统一处理 401 状态码——Token 过期或者非法时自动跳转登录页:
import axios from 'axios' import router from '../router' import { useUserStore } from '../store/user' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { const userStore = useUserStore() userStore.clearUserInfo() router.push('/login') } return Promise.reject(error) } ) export default service到这里,前端路由守卫按“是否需要登录”来划分页面。比如个人中心必须登录才能访问,我配置了meta字段:
{ path: '/profile', name: 'Profile', component: () => import('../views/Profile.vue'), meta: { requiresAuth: true } }路由前置守卫里判断:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next({ name: 'Login', query: { redirect: to.fullPath } }) } else { next() } })这里有个细节:我把redirect参数带上了,这样用户登录成功后可以自动跳回到原来想访问的页面,而不是固定在首页。这个小体验在真实项目里很重要,但很多教学示例都不会讲。
2.5 数据可视化:胜率趋势用 ECharts 展示
个人中心里我加了两个图表:英雄胜率趋势折线图、英雄使用频率柱状图。技术选型用的是 ECharts。
在 Vue 3 里使用 ECharts 典型的做法是:创建一个 div 容器,在组件挂载后初始化实例,然后监听数据变化重新 setOption。但要注意组件卸载时一定要调用dispose销毁实例,否则会内存泄漏。
import * as echarts from 'echarts' import { onMounted, onUnmounted, ref, watch } from 'vue' const chartRef = ref(null) let chartInstance = null const renderChart = () => { chartInstance.setOption({ xAxis: { type: 'category', data: props.dates }, yAxis: { type: 'value' }, series: [{ type: 'line', data: props.rates, smooth: true }] }) } onMounted(() => { chartInstance = echarts.init(chartRef.value) renderChart() }) watch(() => props.rates, renderChart) onUnmounted(() => { chartInstance && chartInstance.dispose() })实际开发中还有个兼容性问题——如果容器是隐藏状态(比如 Tab 切换),ECharts 初始化后宽度可能是 0,图表显示不出来。解决办法是在容器可见后再调用resize()。这里我用的是一个简单方案:切换到 Tab 时setTimeout(() => chartInstance && chartInstance.resize(), 0)。如果你用 el-tabs 这种组件,监听 Tab 切换事件就可以了。
3. 前后端联调、打包部署与 zip 发布
3.1 开发环境下 Vite 代理配置与跨域搞定
前后端分离开发时最烦的就是跨域。前端跑在http://localhost:5173,后端跑在http://localhost:8080,两个端口不一致,浏览器会拦截跨域请求。
解决跨域有两种常规思路:后端开启 CORS,或者前端通过代理转发。生产环境更推荐后端开 CORS,但开发环境我更喜欢用前端代理,因为它不需要后端改代码,改完立刻生效。
Vite 的代理配置在vite.config.js里,很简单:
export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } } })这样前端代码里请求/api/hero/list,Vite 开发服务器会自动转发到http://localhost:8080/hero/list。changeOrigin: true的意思是修改请求头里的 Host 字段为目标地址,避免后端在反代场景下判断来源出错。
3.2 生产构建:Vite 打包与 Nginx 部署
开发完成后,执行npm run build,Vite 会把项目打包到dist/目录。但生产部署没那么简单,有几个配置必须提前想好。
一是静态资源的 base 路径。如果你把 dist 部署在域名根路径下,base: '/'默认值没问题;但如果你要部署在子路径下,比如https://example.com/lw/,那就必须设置base: '/lw/'。这个配置直接影响 src 和 link 标签里静态资源的引用路径,配错了页面会白屏,控制台全是 404。
二是历史路由模式下的 Nginx 配置。Vue Router 默认用 HTML5 History 模式,URL 里没有#,看起来更干净。但这种模式要求服务器把所有请求都重定向到index.html,让前端路由自己决定展示哪个页面。Nginx 配置如下:
server { listen 80; server_name your-domain.com; root /var/www/lw/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这段配置里两个 location 块:第一个把前端路由丢给 index.html,第二个把/api/开头的请求反向代理到后端服务。这样前端和后端就包在同一个域名下了,生产环境也不存在跨域问题。
3.3 在 Linux 服务器上压缩与解压 zip:打包发布的常见操作
项目做完后,交付形式是lw.zip。这就要说 Linux 上 zip/unzip 的日常操作。把整个前端项目(包含源码和构建产物)压缩成 zip,最常用的命令是:
zip -r lw.zip ./-r表示递归压缩子目录。但如果只是想压缩构建产物和部署配置,不包含 node_modules(那个东西七八千个文件,压缩了也没意义),可以这样:
zip -r lw-deploy.zip dist/ public/ package.json到了服务器上,解压的命令是:
unzip lw.zip -d /var/www/lw/-d指定解压目标目录。如果没有 unzip 命令,先装一下:
sudo apt install unzip # Ubuntu / Debian sudo yum install unzip # CentOS / RHEL还有一个很实用的操作:如果 zip 包损坏了,可以尝试用zip -FF修复。比如下载的 zip 包在传输过程中有字节损坏,执行:
zip -FF damaged.zip --out fixed.zip这个命令会尝试扫描 zip 包的 central directory 并修复索引,能救回不少数据。但注意修复后要检查一下内部文件的完整性,不能保证 100% 恢复。
3.4 压缩包在开发协作中的坑:node_modules 和操作系统差异
这个项目打包成 zip 分发给别人的时候,最容易踩的坑就是:把 node_modules 一起打进去了。node_modules 体积动辄几百 MB,里面可能有上千个文件,压缩和传输都极慢,而且换一台电脑、换一个 Node.js 版本,里面的原生模块很可能不兼容。
正确做法是压缩包只包含源码、配置文件和 package-lock.json,拿到的人自己执行npm install安装依赖。有锁文件的情况下,npm 会安装到完全一致的依赖版本,避免“我这跑得好好的,你那报错”的扯皮现象。
另一个针对 zip 的细节是操作系统的换行符和文件权限差异。Windows 下压缩的 zip 解压到 Linux 服务器后,shell 脚本可能因为\r\n换行报错,可执行文件可能丢失执行权限。如果项目里有 .sh 脚本,解压后记得执行:
chmod +x deploy.sh sed -i 's/\r$//' deploy.sh先把回车符清掉,再加执行权限,再运行,避免被坑。
4. 开发过程中遇到的典型问题与排查实录
4.1 “file is not a zip file”到底怎么解决
这是个非常经典的 zip 异常,我身边不少人都遇到过。
先说现象:执行unzip xxx.zip时,终端报错file is not a zip file或者invalid zip archive: could not find eocd。
这两个报错的原因基本一样——文件其实根本不是标准的 zip 格式,或者文件下载不完整。could not find eocd里的 eocd 是 End Of Central Directory 的缩写,是 zip 文件末尾的目录索引块。如果文件下载到一半断了,或者文件根本不是 zip,只是改了扩展名,就会出现这个错误。
排查步骤我总结成三步:
第一步,用file命令看真实文件类型:
file lw.zip如果输出显示HTML document或者ASCII text,那说明下载落地的是网页内容(可能是 404 页面或者拦截页),根本不是 zip。
第二步,看文件大小是否合理:
ls -lh lw.zip前一个下载进度 90% 断掉后,文件大小明显小于预期,重新下载即可。
第三步,如果确认是 zip 但内容有损坏,试一下zip -FF修复。另外,如果你用的是 Windows 自带的“发送到压缩文件夹”生成的 zip,偶尔解压器不兼容,可以换 7-Zip 重新压缩一次,很多时候就好了。
4.2 项目解压后跑不起来的 node_modules 问题
团队分发lw.zip后,最常听到的一句话是:“怎么我 npm run dev 报错了?”点开报错,通常写着The project can not found node_modules或者You can: 1. use npm install -g @vue/cli。
核心原因就一句话:压缩包里没有 node_modules,收包方没有执行依赖安装,或者安装了但版本不匹配。
做法很简单:解压后先执行npm install,如果网络环境差,可以配置镜像源。个人建议先把 npm 镜像切到国内源再装:
npm config set registry https://registry.npmmirror.com npm install注意package-lock.json要和源码一起分发。锁文件记录了每个依赖包的精确版本,只有带上它,才能保证团队所有成员安装的依赖完全一致。如果只带 package.json,可能你用的是 Vue 3.4,队友装成了 3.5,某些 API 用法对不上,就会出现“我这边好好的你那边报错”的灵异事件。
4.3 路由参数刷新丢失:筛选条件不翼而飞
功能开发完,测试提了一个 bug:在英雄列表页选了“法师”分类,然后刷新浏览器,分类又变成“全部”了。
这个 bug 的原因非常典型——筛选条件存在组件内部的内存变量里,刷新页面后 Vue 实例重新创建,组件状态全部重置,所有筛选条件自然清空。想让刷新后保留条件,必须把筛选条件同步到 URL query 中。
我当时的修复方案是:把 category 和 keyword 同步到 URL 的 query 参数。组件挂载时从route.query读取初始值,筛选变化时通过router.replace更新 URL:
import { useRoute, useRouter } from 'vue-router' const route = useRoute() const router = useRouter() const keyword = ref(route.query.keyword || '') const category = ref(route.query.category || '全部') watch([keyword, category], () => { router.replace({ name: 'HeroList', query: { keyword: keyword.value, category: category.value } }) })这样筛选条件就在 URL 里“落地”了。刷新页面后,Vue 从 URL 恢复条件,用户甚至可以把筛选好的 URL 发给别人直接看到同样的列表。这个方案同时解决了“刷新丢失”和“分享快照”两个问题。
4.4 keep-alive 切换路由后 el-table 滚回头部
这是一个 Vue 细节优化,但实际体验影响很大。项目里个人中心和攻略列表之间切换时,我用keep-alive缓存了列表页,防止每次切换都重新请求接口。但问题来了——当用户把 el-table 滚到很下面,切到别的页面再回来时,表格的滚动条位置变了?不是变了,是滚回了顶部。
原因是 keep-alive 缓存的是组件实例,包括了数据和 DOM 状态,但 el-table 的滚动位置不是持久化到组件 data 里的,而是在 DOM 节点的 scrollTop 属性上。keep-alive 缓存的是 VNode 和实例,DOM 被移除了,重新插入时表格的滚动容器是新的 DOM 节点,scrollTop 自然归零。
解决办法是在组件停用前记录滚动位置,重新激活后恢复:
<script setup> import { onActivated, onDeactivated } from 'vue' let scrollTop = 0 const handleTableScroll = (e) => { scrollTop = e.target.scrollTop } onActivated(() => { // 恢复滚动位置 const wrap = document.querySelector('.el-scrollbar__wrap') wrap && (wrap.scrollTop = scrollTop) }) </script>关键点是onDeactivated时记录位置,onActivated时恢复。这种问题纯看文档很难发现,只有实际用过 el-table 配合 keep-alive 才会碰到。
4.5 Vue DevTools 调试经验
开发 Vue 项目,强烈建议装 Vue DevTools 浏览器插件。这个工具能帮你实时查看组件树、Props、data、computed 状态,排查“数据为什么没更新”“computed 为什么没重算”这类问题,效率成倍提升。
实际调试中我常用的三个功能:
一是组件面板,点击页面上某个元素,可以在组件树里定位到对应组件,直接看到它的 props 和内部状态。
二是 Pinia/Vuex 面板,查看 store 里所有状态和 action 调用记录,非常适合排查“store 数据被谁改了”这种问题。
三是时间旅行调试,在 DevTools 里可以回放状态变化记录,定位是哪个操作导致状态突变。
如果你装了插件但没生效,先在浏览器地址栏确认插件已启用、Vue 项目运行在开发模式(未压缩的 vue.js),生产构建下不会显示 DevTools 面板。
4.6 前后端联调和 Node.js 版本冲突的坑
最后分享一个环境上的问题。有段时间项目跑着跑着 Vite 打包就报错,查了半天发现是 Node.js 版本太老导致的。Vite 3 以上要求 Node.js 14.18+ / 16+,Vite 5 要 Node 18+ 或 20+。我本地原来是 Node 14,后来一查 Vite 版本要求,直接切到 Node 18 LTS 才顺畅。
用nvm(Node Version Manager)管理多个 Node 版本非常方便:
nvm install 18.20.2 nvm use 18.20.2如果你拿到项目的压缩包后npm install各种警告或者编译原生模块失败,第一反应先确认 Node 版本是否满足要求,别先怀疑源码。因为很多原生依赖包在编译时会调用 node-gyp,Node 版本和 Python 环境都会影响结果。
5. 项目实战收官:从 lw.zip 到可持续迭代
项目写到这一步,功能已经跑通,交付包也打好了。但作为一篇实战分享,我还想聊几个项目后续会自然遇到的问题和我的应对思路,这些决定了你做完的 demo 能不能真正升级成一个可以被别人用的平台。
第一个是内容运营的问题。攻略平台的生死线在内容。技术上是增删改查,但内容从哪来?我的规划是给管理员做一个简单的后台发布页面,富文本编辑器 + 图片上传,同时允许注册用户投稿,由管理员审核后发布。这样内容生产从“你一个人写”变成“社区用户在写”,平台才有持续更新的动力。
第二个是性能优化的问题。当英雄数量、攻略文章数量涨到几千甚至几万条时,首页一次性拉全量列表就不现实了。我的做法是给列表接口强制分页,前端加“加载更多”按钮而不是一页一页翻;对英雄图谱这类数据变更频率不高的接口,在 Nginx 层开启 gzip 压缩,几个大 JSON 接口的传输体积直接能缩小 70% 以上。再加上后端给英雄详情接口做 Redis 缓存,大多数情况下数据库都不会有压力。
第三个是安全加固的问题。我实际操作中遇到的第一版安全隐患是对上传图片没做任何格式校验。实测直接传一个带脚本的 html 文件也能成功,这个漏洞一旦被利用就是存储型 XSS。后来我改成了三件事:限制上传扩展名(白名单 .jpg/.png/.gif/.webp)、校验文件头(Magic Number)而不是只看扩展名、上传目录禁止执行脚本。这三点做完,风险面基本可控。
还有很实际的一点:打包发布前,花十分钟把node_modules从 zip 包里排除掉,同时把.env文件里的数据库密码、API 密钥等敏感信息全部移除。开发的时候图省事把密钥写在注入 env 文件里是常态,但交付出去就是安全事故。如果你需要给别人演示,用.env.example提供一份脱敏模板,这才是正经做法。
个人感觉,做完这个 MOBA 攻略平台项目后学到最多的不是某个 API 怎么调,而是“一个真实项目需要经历的全套流程”——需求拆分、组件抽象、路由设计、状态管理、跨域处理、打包部署、问题排查。这些东西单拎出来看知识点都不难,但串起来却很考验对整体链路的理解。我把我的踩坑记录写出来,就是希望你在做同类项目时,能在这些地方少花点排查时间,把精力放在更有技术含量的模块上。
本文还有配套的精品资源,点击获取