ARTICLE DETAIL

资讯详情

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

Vue+Spring Boot实战:MOBA游戏攻略分享平台设计与部署

Vue+Spring Boot实战:MOBA游戏攻略分享平台设计与部署 简介这是一套面向前端与全栈开发学习者的MOBA类游戏攻略分享平台完整项目源码适用于掌握Vue、Spring Boot及MySQL技术栈的中级开发者进行实战复现与二次开发。平台采用前后端分离架构后端基于Spring Boot快速构建RESTful接口前端提供JSP与Vue双模板方案其中Vue组件45个.vue文件覆盖首页导航、面包屑、侧边栏等核心模块配合164个JS逻辑脚本与53个CSS样式文件实现高交互性攻略浏览体验数据层依托MySQL保障用户、攻略、评论等结构化信息的一致存储。资源包共814个文件含115个Java后端类、79个GIF动效资源、32个PNG图标及1个SQL建表脚本整体体积26.78MB目录中可见bat部署脚本与.bak备份文件体现完整开发-构建-运行流程。已有29373人学习下载可直接导入IDE运行获取从环境搭建、接口联调到页面渲染的全流程实践参考。 前段时间整理硬盘里的项目归档翻到一个名为lw.zip的压缩包解压出来的就是一个基于 Vue 的 MOBA 类游戏攻略分享平台。这个项目是我给一个游戏社区社团做的采用了前后端分离结构前端是 Vue 全家桶后端用的 Spring Boot数据库选的 MySQL。整个平台覆盖了用户注册登录、攻略文章的发布与管理、按英雄/版本/位置等维度分类检索以及评论、点赞、收藏这套互动体系。如果你正在准备类似的 Vue 实战项目、毕业设计或者想了解一个内容社区类系统从零到上线要经历哪些环节这篇文章应该能帮你省不少事。我会把项目设计的来龙去脉、核心模块的实现方式、部署到服务器时跟 zip 包打交道的实际坑都一次性讲清楚。这个项目在功能上不算特别花哨但胜在覆盖了内容平台最常见的完整闭环。而且越是这种看起来很常规的项目越能暴露问题——比如路由权限控制、富文本内容回显、前后端接口统一封装、打包之后往服务器上部署时的各种File is not a zip file类报错全都是在真实场景里会反复踩的坑。下文我会尽量按一个真实项目的推进节奏来拆解从选型到实现再到部署每一步都会说明当时的想法和取舍希望对你有参考价值。1. 项目定位与整体技术选型1.1 这个平台到底解决什么问题MOBA 类游戏的玩家群体相当庞大但攻略内容长期散落在各个渠道贴吧、B站评论区、公众号文章、个人博客内容没法聚合搜索效率也低。做这个平台的核心诉求很简单给攻略作者一个集中的发布入口给普通玩家一个按英雄、按版本、按位置筛选内容的阅读入口。作者能管理自己的文章和粉丝互动玩家能收藏、点赞、评论平台侧能对内容做基础的分类和审核。我当时给这个平台定义了三类核心用户普通玩家需要快速找到当前版本某个英雄的出装、符文、连招思路不想在视频网站上翻半天评论区。攻略作者希望有一个比论坛帖子更干净、更结构化的写作和发布环境同时能看到自己的内容被多少人收藏、点赞。平台运营需要能管理用户、管理文章分类对内容做审核和下架处理掌握内容的整体数据。围绕这三类用户项目的功能边界就很清晰了用户体系注册、登录、个人信息、内容体系攻略的发布、编辑、删除、详情展示、检索体系分类、标签、关键字搜索、互动体系评论、点赞、收藏再加一个简单的后台管理页面。做项目最忌讳的是功能越加越多我当时给自己定的原则是先把闭环跑通再谈扩展。所以 MVP 版本里没有做关注关系、私信、积分系统这些相对边缘的功能而是把所有精力集中到内容流转这条主线上。1.2 为什么用 Vue 而不是其他框架选择 Vue 而非 React 或者 Angular主要有几个实际原因。第一团队当时就是我加一个实习生对 Vue 的上手成本更低中文文档和社区资料都非常全遇到问题能很快搜到解决方案。第二Vue 的单文件组件开发方式很适合这种内容展示型的项目模板、脚本、样式放在一个.vue文件里组件之间通过 props 和事件通信代码组织起来非常直观。第三Vue 生态里的 Element Plus 组件库本身就很适合做中后台和管理类界面表单、表格、分页这些高频组件不用自己从零造轮子。版本上我直接用了 Vue 3 Vite没有选 Vue 2。虽然网上很多旧教程还在用 Vue 2 Webpack但作为新项目没有理由去沿用一套已经进入维护期的技术栈。Vite 开发服务器的冷启动速度比 Webpack 快很多日常开发体验完全不一样。构建用 Vite 也很省心纯静态资源打包后面部署到 Nginx 只需要把dist目录丢上去就行。配套选型如下构建工具Vite 5路由Vue Router 4状态管理Pinia比 Vuex 4 更清爽TypeScript 友好度也更高UI 组件库Element PlusHTTP 库Axios后端主框架选了 Spring Boot数据库用 MySQL这样和前端一样都走当前社区最主流、资料最全的路线遇到问题的时候可参考的案例最多。1.3 项目目录结构与工程化规范一个项目的结构合理度会直接影响后期维护和接手人的上手速度。我给前端项目设计的目录结构大致如下src/ ├── api/ # 接口请求模块按业务拆分 │ ├── user.js │ ├── article.js │ └── comment.js ├── assets/ # 静态资源 ├── components/ # 通用组件 │ ├── PaginationWrap.vue │ ├── UploadImage.vue │ └── ArticleCard.vue ├── layout/ # 整体布局 │ └── DefaultLayout.vue ├── router/ # 路由配置 │ └── index.js ├── stores/ # Pinia 状态 │ ├── user.js │ └── app.js ├── views/ # 页面组件 │ ├── Home.vue │ ├── ArticleDetail.vue │ ├── ArticleEdit.vue │ ├── Login.vue │ ├── Register.vue │ ├── UserCenter.vue │ └── admin/ └── utils/ # 工具函数 ├── request.js # axios 封装 └── auth.js # token 存取这个结构的好处是每个文件放哪里、干什么看一眼目录就清楚。api层统一管理所有后端接口地址页面组件里不直接写请求路径一是路径改了只动一个地方二是接口和页面解耦页面里只需要关心调用api/article.js暴露出来的方法。stores里只放全局共享的状态比如用户信息和登录状态、应用配置页面内部的数据有条件地放组件里不什么数据都塞进 Pinia。前后端分离的项目接口文档是强制要求不然前端等后端联调的时候寸步难行。我当时用 YApi 搭建了内部接口文档平台后面你可以换成 Apifox 或者直接写在 README 里重点是保证每个接口的请求方式、参数、返回结构都有明确约定不然联调阶段一定鸡飞狗跳。2. 核心功能模块的设计与实现2.1 用户注册登录与 token 鉴权用户体系是所有社区类平台的地基。这个项目没有做短信验证码也没接第三方授权登录采用最简单的用户名 密码 邮箱绑定方案完全够用。后端密码存储用的 BCrypt 哈希绝不允许明文入库这是最基本的底线不然数据库泄露就是灾难。前端这边的登录流程设计成三步登录表单校验成功后拿到 token存入 localStorage 和 Pinia。Axios 请求拦截器里统一给 header 加上Authorization: Bearer token。路由守卫检查当前页面是否需要登录未登录就跳转到/login?redirect原路径登录成功后再跳回来。Axios 封装那部分核心代码我简化在下面实际项目里也是这个骨架// utils/request.js import axios from axios import { ElMessage } from element-plus import router from /router import { getToken, clearToken } from /utils/auth const service axios.create({ baseURL: import.meta.env.VITE_API_BASE, timeout: 10000 }) service.interceptors.request.use(config { const token getToken() if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response { const res response.data // 这里和后端约定code 为 0 表示业务成功 if (res.code 0) { return res.data } ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) }, error { if (error.response error.response.status 401) { clearToken() router.push({ path: /login, query: { redirect: router.currentRoute.value.fullPath } }) } ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default service路由守卫的部分Vue Router 4 的写法是// router/index.js router.beforeEach((to, from, next) { const token getToken() if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else if (to.meta.guestOnly token) { next(/) } else { next() } })这里有两个值得注意的细节。第一个guestOnly是给登录页和注册页用的已经登录的用户不应该看到登录页否则就会出现我明明登录了点头像看个人中心却被一脚踢回登录页的诡异体验。第二个redirect参数一定要带上否则用户辛辛苦苦登录完发现自己被丢回首页得重新找刚才想看的文章体验非常差。在我实际测试中比较容易出问题的地方是 token 过期。很多人只做前端判断有没有 token但 token 其实可能已经失效了。所以响应拦截器里对 401 的统一处理是必须的遇到 401 就清理本地凭证、跳登录页而不是让页面陷入一个登录状态已经没了但用户还以为自己登录着的中间态。2.2 攻略文章发布与富文本编辑攻略文章是这个平台的核心内容载体。发布攻略时用的是富文本编辑器整体流程为用户进入编辑页 - 填写标题、选择分类和标签、编写正文 - 上传封面图 - 提交到后端保存。富文本编辑器我采用了 wangeditor 5。实际调研的时候对比过 Quill 和 TinyMCEQuill 生态不错但默认样式比较朴素TinyMCE 功能全但商用授权限制让人不太放心。wangeditor 5 在国内社区活跃中文文档清晰上传图片的接口接入方便对内容展示项目来说足够用了。编辑器接入时最需要注意的问题是图片处理。默认情况下粘贴图片或从本地上传图片会以 base64 编码直接嵌在正文里这会导致文章体积非常大数据库压力陡增。我把编辑器的图片上传行为改成了先走后端上传接口拿到图片 URL 后插入内容实现方案是自定义编辑器菜单的 upload 行为import { Boot } from wangeditor/editor const uploadImage { key: group-image, title: 上传图片, iconSvg: svg.../svg, menu: { async exec(editor, file) { const formData new FormData() formData.append(file, file) const url await uploadFile(formData) // 调用后端上传接口 editor.insertNode({ type: image, src: url, alt: , href: }) } } }编辑器内容是 HTML 字符串这带来了一个必须正视的安全问题XSS 注入。用户在编辑器里写入一段script或带onerror属性的img如果后端不处理直接存库前端再直接v-html渲染那打开文章详情页的用户就会被执行恶意脚本。当时做的防护措施是后端入库前用 Jsoup 做白名单过滤只保留 p、h1-h4、img、ul、ol、li、a、strong、em 等安全标签img 只允许 http/https 开头的src。前端层面不依赖过滤但渲染也尽量不做多余处理。这个坑建议所有做内容平台的人都重视起来等真的被攻击就晚了。文章发布后的详情页回显很多人会卡在样式上。编辑器生成的 HTML 用了很多类名比如标题层级、列表样式等。如果全站 CSS 里没有对应的样式重置文章内容会呈现得非常难看。我在全局样式表里专门给文章正文容器加了作用域样式比如.article-content img { max-width: 100% }、.article-content pre { background: #f6f8fa; padding: 12px; overflow-x: auto }保证内容在详情页展示时能自动适配手机和桌面端。2.3 分类、标签与全文搜索内容平台没有检索体系就只能靠人工翻页体验很差。我这里的攻略分类主要按英雄、版本、位置三大维度英雄维度是 MOBA 玩家的核心入口比如后羿出装攻略李白打野思路版本维度方便玩家只看当前版本的内容位置维度则对应上单、中单、打野、射手、辅助。分类表设计成一个树形结构支持无限层级比如英雄 - 射手 - 后羿这样以后如果想加二级分类不用改表结构。标签则单独用一张表存储文章和标签之间用中间表关联。前端页面里筛选区通过异步请求获取分类树和标签列表每次筛选都把当前条件拼进 query 参数并且和 Vue Router 的query保持同步好处是用户可以把某个筛选条件的 URL 复制发给朋友对方打开后看到的还是同一个筛选结果。搜索功能我一开始想上 Elasticsearch后来冷静一想这个项目的内容量级和团队规模根本没必要。直接用 MySQL 的LIKE %关键字%就能覆盖需求。虽然全文索引会更专业但引入搜索引擎带来的部署复杂度、数据同步等成本对个人项目来说是过度设计。不过这里有一点要注意LIKE 查询用的是%keyword%这会让索引失效在数据量达到几十万条的时候会明显变慢。现阶段可以接受但如果真的要做大得提前考虑 MySQL 全文索引或者引入 ES。我在文章表里建了一个冗余字段search_content把标题、摘要、分类名、标签名拼接在一起搜索时只对这个字段做 LIKE效果会好很多。筛选和排序组合起来的时候后端接口要处理的不只是简单的 SQL还要考虑排序的稳定性。我这里支持按最新、最热、最多收藏三种排序方式分别对应updated_at DESC、view_count DESC、favorite_count DESC同时配合分类和标签过滤条件。用 MyBatis-Plus 的 LambdaQueryWrapper 写条件构造器代码会很简洁但要注意动态 SQL 的拼接顺序这种查询很容易被传参组合搞出各种边界问题写完之后一定要多测几种组合比如只按标签筛选和同时按分类标签时间范围筛选。2.4 互动模块评论、点赞、收藏互动数据是内容平台的粘性来源。这三个功能在数据表设计上有细微差别评论属于多对多内容实体数据量会持续增长独立建表点赞和收藏本质上都是用户对某篇文章的动作记录也各建一张表并配合文章表里的冗余计数。为什么计数要用冗余字段而不是每次COUNT(*)这是非常现实的问题。早期数据量小频繁COUNT(*)也没啥感觉但文章列表页每一次渲染都要统计每篇文章的点赞数、收藏数、评论数数据量一上来这三条子查询叠加会把数据库拖垮。所以我在文章表里直接放了like_count、favorite_count、comment_count三个字段每次用户点赞或取消点赞时在事务里同时更新这张计数表和用户点赞记录表。这个方案有个必须注意的细节并发。如果两个人同时点赞两个请求同时读到like_count 10然后都执行1最后结果变成 11 而不是 12。解决方式是在更新语句里用原子操作UPDATE article SET like_count like_count 1 WHERE id ?这条 SQL 是原子的数据库层面会处理好并发增量不会出现覆盖问题。取消点赞时同理用-1但要注意减的时候做个兜底判断别让计数变成负数。评论功能相对直接文章详情页底部加载评论列表发布评论后通过事件总线刷新列表。评论支持回复但实现时只做了一层回复某条评论的展示逻辑没有做无限嵌套楼中楼一方面是为了控制复杂度另一方面也是因为这个功能在移动端上效果并不好。如果你想扩展可以加一个parent_id字段表示父评论渲染时前端自己处理缩进和递归。3. 前后端接口设计与数据流转3.1 RESTful 接口规范与数据返回格式统一前后端分离开发接口约定就是两个人的合同。如果连返回格式都没统一前端写几十个页面每个页面都要重复处理res.data.data.data这种嵌套结构工作量会成倍增加而且极易出错。我和后端约定了一套统一返回格式{ code: 0, msg: success, data: {} }code为 0 表示业务成功非 0 表示失败msg给用户展示提示信息data放真正的业务数据。所有接口不管成功失败都按这个结构返回。这带来一个明显好处前端 Axios 响应拦截器里只需要判断一次code成功就把data直接返回给业务代码失败统一弹错误提示业务代码里不用每次写if (res.code 0) { ... } else { ... }这种冗长的判断。接口命名上沿用 RESTful 风格资源用名词动作用 HTTP 方法POST /api/auth/register注册POST /api/auth/login登录GET /api/articles?categoryId1page1攻略列表POST /api/articles发布攻略PUT /api/articles/{id}编辑攻略DELETE /api/articles/{id}删除攻略POST /api/articles/{id}/like点赞前端所有接口请求都通过api/模块里的函数发出页面组件里永远不出现axios.get(http://xxx)这种写法。比如文章模块的接口封装长这样// api/article.js import request from /utils/request export const getArticleList (params) request.get(/api/articles, { params }) export const getArticleDetail (id) request.get(/api/articles/${id}) export const createArticle (data) request.post(/api/articles, data) export const updateArticle (id, data) request.put(/api/articles/${id}, data) export const deleteArticle (id) request.delete(/api/articles/${id}) export const likeArticle (id) request.post(/api/articles/${id}/like)这样一来接口路径的修改范围被限制在一个文件里页面里如果重构了直接替换对应方法。多人协作时按模块拆 api 文件谁负责用户模块就维护user.js谁负责文章模块就维护article.js代码冲突概率大减。3.2 基于 Vue Router 的路由权限与菜单设计前端的路由设计决定了用户能不能顺畅地走到他要去的页面。我的路由体系是静态路由 动态路由结合的方式。登录页、注册页、首页、攻略详情页这种所有用户都能访问的页面放在静态路由表里用户中心和后台管理页面则需要登录部分管理页面还需要管理员权限。后台管理这一块我采用静态路由 按钮级权限控制而不是后端动态下发菜单。因为后台页面数量不多用动态路由反而增加复杂度。管理员页面通过路由meta.roles标记{ path: /admin, component: () import(/layout/DefaultLayout.vue), meta: { roles: [ADMIN] }, children: [ { path: articles, component: () import(/views/admin/ArticleManage.vue) }, { path: users, component: () import(/views/admin/UserManage.vue) } ] }路由守卫再补一个角色判断router.beforeEach((to, from, next) { const userStore useUserStore() if (to.meta.roles !userStore.roles.includes(...to.meta.roles)) { next(/403) } })这里踩过一个印象很深的坑角色信息怎么同步到前端。一开始我用 localStorage 存用户信息包括角色字段但后端管理员改了用户权限之后前端的 localStorage 里还是旧角色导致出现用户被降级后依然能访问后台这种安全问题。后来统一改成每次刷新页面时在路由守卫里调用一次获取用户信息的接口从后端拉最新的用户数据再渲染页面这样角色变动能及时生效代价只是多一次网络请求完全可以接受。关于路由懒加载Vue Router 4 里component: () import(...)就是按需加载。如果想进一步优化首屏加载速度常规做法是对路由组件做 Webpack/Vite 级代码分割每个页面一个 chunk。但这里有个权衡页面切得非常碎每个 chunk 都很小反而会增加请求数。一般建议是按一级路由拆分首屏和主要页面单独打包其他页面合并成一个 chunk。3.3 状态管理里该放什么不该放什么Pinia 作为状态管理库很好用但它不是万能的全局变量仓库。很多新手容易犯的错误是把表单的临时输入框值、弹窗开关状态、接口返回的一次性数据全都store化最后状态管理比组件本身还复杂调试起来非常痛苦。我把状态管理只用于三类数据用户信息登录状态、用户名、头像、角色全局都要用。应用配置侧边栏折叠状态、主题颜色涉及多个组件的 UI 状态。跨页面共享的数据比如详情页进入编辑页时需要传递文章原始内容用 Pinia 比用路由 query 传一长串参数要干净得多。在 Vue 里computed和watch的使用也有讲究。computed的核心定位是根据已有响应式数据派生新数据它具备缓存能力依赖不变就不会重新计算比如文章列表的筛选结果、购物车总价、用户头像拼接完整 URL。watch适合响应数据变化而执行副作用的场景比如搜索框输入防抖后请求接口或者路由参数变化后刷新文章列表。一个典型的computed使用场景是搜索筛选const filteredArticles computed(() { if (!searchKeywords.value) return articles.value return articles.value.filter(a a.title.includes(searchKeywords.value)) })这个computed只依赖articles和searchKeywords当中任何一方变化时会重新计算但数据没变时不会触发多余的计算性能比methods里每次调用都重新跑一遍高得多。很多人用 Vue 一段时间之后才理解computed的精髓其实简单说就是模板里尽量别写复杂表达式需要推导的数据就用computed你会看到一个更干净的模板和更稳定的运行效果。4. 项目打包、zip 压缩包与部署上线实战4.1 Vite 构建配置与前端的打包产出开发做完后进入部署环节这一块是很多新人最容易卡住的地方。前端用 Vite 构建核心配置在vite.config.js里需要关注几个关键项// vite.config.js export default defineConfig({ plugins: [vue()], base: /, server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }, build: { outDir: dist, assetsDir: assets, sourcemap: false, chunkSizeWarningLimit: 1500 } })开发环境下server.proxy把/api开头的请求代理到后端8080端口前端代码里写相对路径/api/xxx就行不用关心后端到底部署在哪台机器上。构建产物在dist目录下VITE_API_BASE这个环境变量在构建时可以指向线上后端的地址比如https://api.example.com。如果你用的不是 Nginx 部署而是纯静态托管记得把base改成对应的子路径否则资源会全部 404。构建命令很简单npm run build跑完之后dist目录里就是一堆静态文件html、js、css、图片这些文件拿到任何静态服务器上都能直接跑。前端构建产物不依赖 Node.js 环境只要是个能提供 HTTP 服务的软件Nginx、Caddy甚至 Python 的http.server就能托管。后端打包也有讲究。Spring Boot 项目用 Maven 打成 jar 包mvn clean package -DskipTests这个 jar 包自带内嵌 Tomcat运行时只需要java -jar就能起服务不需要额外装 Tomcat。打包过程中如果遇到依赖冲突、测试用例失败等问题-DskipTests可以跳过测试执行不是跳过测试编译要看具体需求。生产环境里我一般写一个deploy.sh脚本把构建、停服、启动这几步串起来避免手动操作遗漏。4.2 从 lw.zip 到服务器解压、修复与部署项目名称叫lw.zip这是当时做完整理之后压缩分发的一个归档包。ZIP 格式是跨平台分发项目最常见的打包方式但每次解压这种东西总会遇到各种玄学问题我把真实踩过的坑和解决方法都整理在这里。如果你拿到一个 zip 压缩包在 Linux 服务器上解压最基础的命令是unzip lw.zip如果系统提示unzip: command not found说明没装 unzip 工具Debian/Ubuntu 上执行apt install unzipCentOS/RHEL 则是yum install unzip还有压缩命令zip工具本身也需要安装。打包一个目录到压缩包zip -r lw.zip lw/-r表示递归打包整个目录不加-r只会打包空目录这是新手最容易踩的坑。想把lw.zip解压到指定目录unzip lw.zip -d /opt/project/真正让我印象深刻的是一次解压报错file is not a zip file。这个错误的原因通常有两种。第一种是文件本身根本不是 ZIP 格式比如下载到的其实是一个 HTML 错误页服务器返回的 404 页面只不过扩展名叫.zip。用file命令可以确认file lw.zip如果输出显示HTML document而不是Zip archive data那基本可以确定是个假 zip。第二种是文件在传输过程中被截断了下载工具支持断点续传吗网络不稳定时下载一半就中断文件虽然以.zip结尾但已经损坏。还有一种更隐蔽的报错invalid zip archive: could not find eocd。EOCD 是 ZIP 格式的结尾目录记录它通常位于文件末尾如果文件被截断或者人为修改过EOCD 就会找不到解压工具就会报这个错。碰到这种情况可以先尝试用zip -FF来修复zip -FF damaged.zip --out repaired.zip这个命令会尝试扫描整个文件把还能识别的数据恢复出来生成一个新的repaired.zip然后再尝试unzip repaired.zip。当然如果文件缺损太严重修复也是徒劳我的建议始终是回到源头重新下载或者重新导出这是最省时省力的方案。ZIP 文件还有一个老生常谈的问题密码。如果lw.zip设置了密码但记不起来了千万别急着找什么暴力破解工具。先确认是不是项目方提供的默认密码比如123456、www.xxx.com。确实没办法的时候再考虑用专用密码恢复工具但成功率取决于密码长度和复杂度很长很复杂的密码基本等于无解只能回头去问文件提供者。4.3 Nginx 部署与前后端联调疑难杂症前端构建好的dist目录放在服务器/opt/web下用 Nginx 配置一个静态站点同时把/api路径反向代理到后端 jar 服务的端口比如 8080。典型的 Nginx 配置如下server { listen 80; server_name game-guide.example.com; root /opt/web; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里最关键的就是try_files $uri $uri/ /index.html这一行它解决的是 Vue Router history 模式下的刷新 404 问题。因为前端路由是虚拟的你访问/article/1时服务器上并没有这个文件Nginx 会兜底返回index.html然后由 Vue Router 接管路径。如果这行没配在非首页的路径刷新就会直接 404。配置改完后记得重载 Nginxnginx -s reload前后端联调阶段最常见的报错是跨域。只要后端没开 CORS前端请求http://localhost:8080/api/xxx就会报Cross-Origin Request Blocked。开发环境下我用 Vite proxy 解决了生产环境下靠 Nginx 反向代理实现了同源所以实际上不需要后端额外开启 CORS这样也避免了一些安全隐患。另一个常见问题出现在项目的依赖管理上。把 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 config set registry https://registry.npmmirror.com然后再执行npm install。有个细节要注意.gitignore文件一定要把node_modules和dist排除掉不然哪天提交代码不小心把几万个依赖文件推上去仓库直接爆炸。前端调试的时候Vue Devtools 是刚需。Vue 3 的 Devtools 插件直接去浏览器应用商店搜索 Vue.js devtools 安装即可安装后浏览器工具栏会多出 Vue 图标打开项目页面就能在开发者工具里看到组件树和状态数据。如果你用的是 Chrome应用商店无法访问的话也可以去官方 GitHub 仓库下载离线包通过开发者模式的加载已解压的扩展程序方式安装。排查问题的时候组件树和 Pinia 状态面板是最好用的两个调试入口。5. 常见问题与避坑指南5.1 问题速查表下面这张表整理了我开发、部署这个项目过程中遇到的典型问题按现象 - 原因 - 解法的格式罗列排查问题的时候建议对照着看。问题现象常见原因解决方案解压报file is not a zip file文件损坏或根本不是 zip 格式用file命令确认类型重新下载/导出解压报could not find eocdZIP 文件被截断或 EOCD 记录缺失用zip -FF damaged.zip --out repaired.zip修复不行就重新获取刷新页面 404Nginx 没配置try_files加上try_files $uri $uri/ /index.html请求接口跨域前端域名和后端域名不一致且后端没开 CORS开发用 Vite proxy生产用 Nginx 反代登录后跳转丢失目标页路由守卫没记录 redirect登录页拼接?redirect登录成功后router.push(redirect)富文本内容样式错乱没有还原编辑器内容的 CSS 样式在.article-content里补充对应选择器样式提交数据后列表不刷新事件通信或者状态同步不到位用事件总线或刷新接口保持页面和 store 数据一致npm install卡死默认 npm 源在国外速度慢切换到 npmmirror 镜像后端java -jar启动失败端口被占用8080 端口被其他进程占用lsof -i:8080查占用进程换端口或 kill 旧进程Vue Devtools 不显示组件树插件版本和 Vue 版本不匹配卸载后装 Vue 3 对应版本插件5.2 几个让我印象深刻的教训第一个教训是后端接口返回时间问题。前端列表页用created钩子请求数据但如果组件复用时比如从英雄分类切到版本分类created只在首次创建时执行第二次切换同一组件不会重新触发导致列表数据停留在上一次的分类。解决方式是监听路由参数变化或者用路由key强制组件重建。Vue Router 里给router-view加:key$route.fullPath是最省心的一种方式但副作用是会重建组件如果页面里有需要保留的状态得考虑缓存方案。第二个教训和 KeepAlive 有关。keep-alive会让页面组件缓存切走再回来时不会重新拉数据这对浏览进度是有好处的但如果页面里有el-table且表格已被滚动从别的页面切回来时表格就停在滚动后的位置体验很别扭。我碰到的情况是文章列表页很长用户滚到底部切走再切回来表格还在底部想回到顶部需要费劲往上滚。解决办法是监听activated钩子在页面重新激活时调用scrollTo(0, 0)如果有需要保持的滚动状态再特殊处理。第三个教训是环境变量管理。把后端地址直接写死在代码里的人很常见万一前后端部署在不同环境每次上线都要改代码重新打包非常痛苦。我在项目根目录建了.env.development、.env.production和.env.test三份文件通过import.meta.env.VITE_API_BASE读取这样换环境不用动代码只需要在构建时切换环境变量。这是最基础但收益最高的工程化习惯。第四个教训关于登录状态和用户信息不一致。一开始我把用户对象直接存到 localStorage后来发现后端改了用户昵称或头像前端还是旧信息而且权限变更也不会即时生效。后来改成只存 token用户信息在全局状态里维护每次刷新页面时重新拉取/api/user/info接口保证拿到后端最新的数据。虽然多一次网络请求但避免了用户被禁封了还能正常操作的风险。5.3 如果你想在此基础上继续扩展做完这个平台后我陆续收到了不少扩展思路整理几个适合逐步迭代的方向。第一个是前端加个版本更新公告模块把每个版本的英雄和装备调整用时间线的方式展示这比靠用户在评论区讨论版本要高效得多。我后来在社区版上加了一个简易的版本时间线页面配合后端定时拉取官方数据接口效果不错。第二个是内容审核流。MVP 里面文章提交直接发布但如果有真实运营需求肯定要走草稿 - 提交审核 - 审核通过 - 上线的流程。这个改动主要是后端加状态字段前端编辑页和后台管理页一起配合工作量可控但对平台内容质量的提升是巨大的。第三个是移动端适配。目前前端的布局是响应式的但主要是为桌面端优化手机端浏览体验一般。建议单独做一套移动端布局或者直接考虑使用 uni-app 之类跨端方案毕竟 MOBA 玩家用手机浏览攻略的比例非常高。第四个是数据统计。可以给作者提供文章浏览量趋势、点赞来源分析这类图表本质上是在文章浏览记录上做聚合查询用 ECharts 画折线图和柱状图后端加一个统计接口即可。这些数据对作者的正反馈很重要也是很多内容平台后期运营的核心抓手。此外用户反馈里比较集中的需求是按装备组合筛选攻略和攻略收藏到自定义分组这类功能后续可以做成优先级较高的迭代项。从我实际做这个项目的经历来看真正花时间的不在页面 UI而在那些看不见的细节路由守卫的逻辑、token 过期的处理、富文本内容的安全过滤、部署时的各种环境问题。这些细节决定了一个项目是能跑通的 Demo还是能拿出去见人的作品。如果你也在用 Vue 做类似的内容平台项目希望这篇记录能帮你跳过一部分坑哪怕只是少走一点弯路都很值得。最后再分享一个小技巧项目做完后一定要自己从零走一遍打包、解压、部署的完整流程并且在这个过程中记录每一处报错和解决办法。你的lw.zip传到别人手里能不能顺利跑起来完全取决于这一步有没有做扎实。本文还有配套的精品资源点击获取
返回列表