ARTICLE DETAIL

资讯详情

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

Java+Vue仿番茄小说阅读器全栈源码解析:前后端分离到部署避坑

Java+Vue仿番茄小说阅读器全栈源码解析:前后端分离到部署避坑 简介一款基于Java与Vue前后端分离架构的仿番茄小说阅读软件设计源码面向具备Java或Vue基础、希望了解完整Web项目结构的开发者也适合作为毕业设计或课程实践参考。压缩包共674个文件整体55.43MB包含294个Java源文件、101个Vue组件、83个JavaScript脚本、93个SVG图标、32个XML配置、4个SQL数据库脚本以及Maven配置、环境变量、启动脚本和说明文档等辅助文件覆盖后端业务逻辑、前端页面交互、数据持久化与项目部署配置。资源已吸引714人学习下载。读者可获得一套结构清晰的前后端分离项目从entity/controller到Vue组件与路由均有组织同时附带环境配置、批处理启动脚本和数据库初始化语句便于本地运行和二次开发对理解真实企业级小说阅读类应用的模块划分、接口设计及版本管理有一定参考价值。1. 仿番茄小说阅读软件一套能跑通前后端的 JavaVue 设计源码去年帮一个学弟看课程设计选题他本来打算交纯前端静态页面被我拦住了——那种东西在答辩时一问交互逻辑就露馅。后来我们换成了这套基于 Java Vue 的仿番茄小说阅读软件源码前端还原番茄小说的书架、分类、详情和阅读器页面后端用 Java 提供真实接口前后端通过 HTTP 联调整个链路走通之后无论是课程设计还是毕业设计讲起来都有底气。这套源码适合两类人一是需要完整 JavaVue 全栈项目的在校生二是想把 Vue 组件通信、路由守卫和 Spring Boot 接口设计一次练透的初学者。它的核心价值不在于页面像番茄小说而在于它是「设计源码」而不是「静态模板」——数据、状态、进度这些逻辑都是真实跑起来的。2. 先搞清技术选型为什么是 Spring Boot Vue 这套组合2.1 前后端分离是课设答辩的加分项也是这套源码的基本盘这套源码采用的是前后端分离架构前端是 Vue 单页应用负责页面渲染和用户交互后端是 Java 服务负责接口输出和数据组装。相比传统的 Thymeleaf 模板渲染前后端分离的好处是可以把代码分成两个独立工程分别调试前端用npm run dev起本地开发服务后端用 Spring Boot 内嵌的 Tomcat 跑接口两边通过 axios 或者 fetch 通信。很多同学拿到源码后第一反应是「我怎么同时跑起来」这里先给一个整体认知前端工程和后端工程是平级的两个目录没有互相依赖的编译关系。前端发请求的地址一般写在vue.config.js的 devServer 代理配置里或者.env.development环境变量里。后端则通过CrossOrigin注解或者全局 CorsFilter 解决跨域。两者之间只要接口约定一致——路径、请求方式、返回结构——就能各自独立启动。2.2 工程目录拆解前端四个核心模块后端三层结构这套源码的整理逻辑很清楚不是一堆文件堆在一起而是按功能和分层分好了包。我拆完之后大概数了一下前端部分由四个核心模块构成书架页、分类页、详情页、阅读器。这四个模块正好对应小说阅读 App 的主路径用户打开 App 看到书架点进分类找书进详情看简介和章节目录最后打开阅读器阅读。后端的结构是标准的 Controller-Service-DAO 三层外加一个 vo 包放返回给前端的数据对象。实际编码中因为数据量不大很多同学会把业务直接写在 Controller 里我不建议这么干——这套源码里如果 Controller 直接查数据那你答辩时「为什么不分层」这个问题就答不上来。正确的做法是 Controller 只做参数接收和结果封装业务逻辑下沉到 Service数据查询放到 DAO 或者 Mapper 层这样每一层都能独立测试。以下是两端工程的关键目录结构对照着自己项目检查缺什么。frontend/ src/ api/ // 放 axios 请求封装按模块拆文件 router/ // 路由表含路由守卫 views/ bookshelf/ // 书架页 category/ // 分类页 detail/ // 详情页 reader/ // 阅读器 store/ // 如果有全局状态放这里 utils/ // 工具函数和常量 backend/ src/main/java/com/example/novel/ controller/ // 接口入口 service/ // 业务逻辑 mapper/ // 数据访问 vo/ // 返回给前端的 VO 对象 src/main/resources/ application.yml // 端口、数据源配置提示拿到源码后先对照这个目录检查完整性缺 mapper 或缺 vo 包的项目多半是截断了上传跑之前需要补全。2.3 数据怎么来本地 JSON 模拟和 MySQL 真实落库二选一这套源码在数据层面给了两种选择这也是我觉得它设计得比较聪明的地方。默认形态是用 JSON 文件模拟数据后端启动时把 JSON 读进内存接口直接从内存里取——这样无需安装数据库任何机器上都能立刻跑通适合演示和验收。另一种形态是接入 MySQL把 JSON 文件里的数据导入数据表走真正的持久化。如果是课程设计我一般建议先用 JSON 模拟形态跑通全流程等答辩前再把数据源切换成 MySQL这样可以同时展示两套能力。JSON 数据文件的结构决定了你可以把书名、作者、封面、章节内容存到什么精度字段设计得越细前端能展示的内容越丰富。下面是常见的小说表结构设计在 JSON 文件里的最小可用字段列表大致是{ bookId: B1001, bookName: 大明从武当开始签到, author: 佚名, category: 历史, coverUrl: /static/covers/001.jpg, intro: 普通青年穿越到明朝开局一座武当山……, status: 连载, wordCount: 356000, chapterList: [ { chapterId: C100101, chapterIndex: 1, title: 第一章 穿越武当, content: ……这里是章节正文内容…… } ] }每个字段都有它的用处coverUrl决定了前端封面能否正常显示category是分类页筛选项的数据源wordCount可以拿来做书籍卡片右上角的字数标签chapterList是阅读器渲染的原始素材。拿到源码后我建议你先把这个 JSON 结构读一遍再去看前端页面你会突然理解「数据驱动页面」是什么意思——页面上每一个数字和文字都能在这个 JSON 里找到对应来源。3. 前端页面还原从书架到阅读器的 Vue 实现路径3.1 路由配置和 TabBar 导航仿番茄的底部栏是入口仿番茄小说的页面风格核心特征是橙色主色调、卡片式书架布局、底部 TabBar 固定在屏幕底部。这套源码里前端路由用了 Vue Router常规配置包括书架页/bookshelf、分类页/category、我的页面/profile还有一个不带 TabBar 的详情页和阅读器页面。这里有一个实现细节值得注意详情页和阅读器一般不显示底部导航栏所以路由表里要给每个路由单独声明meta字段来控制 TabBar 显隐。const routes [ { path: /bookshelf, name: Bookshelf, component: Bookshelf, meta: { showTabBar: true } }, { path: /category, name: Category, component: Category, meta: { showTabBar: true } }, { path: /profile, name: Profile, component: Profile, meta: { showTabBar: true } }, { path: /detail/:bookId, name: Detail, component: Detail, meta: { showTabBar: false } }, { path: /reader/:bookId/:chapterIndex, name: Reader, component: Reader, meta: { showTabBar: false } } ]meta.showTabBar这个字段在路由跳转时会被携带页面组件里根据route.meta.showTabBar动态 v-if 底部导航栏。这里的参数:bookId和:chapterIndex是路由传参的关键——从书架点击书籍跳到详情页需要知道是哪本书所以传bookId从详情页目录点击章节跳到阅读器需要知道章节位置所以传chapterIndex。这种参数化路由设计是 Vue Router 的基础但也是很多初学者翻车的地方常见错误是把参数放在 query 里而不是 params 里导致刷新后参数丢失。3.2 书架页和分类页卡片列表和筛选是两组独立逻辑书架页在番茄小说里是用户最常停留的界面这套源码的还原度不错——每本书以卡片形式展示封面、书名、作者和阅读进度书架顶部有搜索框。书架页的数据来源是后端接口返回的列表前端拿到数据后需要用v-for循环渲染卡片。这里有一个容易被忽略的细节书架页需要展示「阅读进度」这个进度其实存在 localStorage 里而不是后端返回的原始数据。// 书架页加载书籍列表并合并本地阅读进度 async function loadBookshelf() { const res await getBookshelfApi() const localProgress JSON.parse(localStorage.getItem(readingProgress) || {}) res.data.data.forEach(book { book.progress localProgress[book.bookId] ? localProgress[book.bookId].chapterIndex : 0 }) books.value res.data.data }这段代码展示了前端常见的「接口数据 本地缓存数据」合并逻辑后端返回的是静态的书籍信息而每个用户自己的阅读进度只存在本地。localStorage.getItem(readingProgress)读出来的对象以bookId为 key章节索引为 value合并时直接挂到书籍对象上作为progress字段渲染。参数说明readingProgress是存储的 key建议统一命名后面接的{}是兜底默认值防止首次加载时 getItem 返回 null 导致 JSON.parse 报错。分类页的逻辑跟书架页略有不同它需要按分类过滤。分类一般分男生、女生、出版几个大类每个大类下面有细分标签。前端的筛选逻辑要区分「分类 Tab 切换」和「排序方式切换」前者是重新请求接口后者通常只是前端本地排序。如果是本地排序最简单的实现是拿接口返回的完整列表用 JavaScript 的 sort 方法按点击量、更新时间、字数排一下不需要重新请求后端。3.3 小说详情页章节目录和基本信息是两条数据线详情页是一个信息密度比较高的页面顶部是封面大图和书籍简介中间有「开始阅读」和「加入书架」两个操作按钮下方是完整章节目录列表。这套源码的详情页实现思路是进入页面时先请求书籍详情接口拿到基本信息再请求章节目录接口拿到章节列表两个接口并行发互不等待。async function loadDetail(bookId) { const [detailRes, chapterRes] await Promise.all([ getBookDetailApi(bookId), getChapterListApi(bookId) ]) bookInfo.value detailRes.data.data chapterList.value chapterRes.data.data }这里使用Promise.all解决串行请求的性能问题——如果先等详情回来再发章节请求页面会有一段白屏时间。两个接口同时发出无论哪个先返回数据各自更新到对应的响应式变量。参数说明getBookDetailApi和getChapterListApi都接收bookId作为参数这是详情页路由传递下来的值。为什么要拆成两个接口而不是一个接口全返回原因在后端章节会细说——简单讲是为了后续分页加载章节做铺垫防止一次接口返回几百章的数据导致响应体过大。加入书架按钮的实现方式是先判断这本书是否已经在书架上如果不在调加入书架接口如果在就提示用户已经加过了。这个交互逻辑虽然简单但涉及按钮状态和页面联动面试时经常被追问「如何避免重复提交」——常见做法是加按钮 loading 状态和禁用态请求发出后立刻锁定按钮返回成功再解锁。3.4 阅读器翻页逻辑左右翻页模式和进度记录是关键阅读器是这套源码里技术含量最高的部分。番茄小说的阅读器支持点击左右区域翻页、显示当前章节进度、调节字体大小和背景颜色。这套源码实现翻页是用「内容分页」的思路把一整章的文本按容器高度和当前字体大小切成多页每次点击翻一页。function paginateContent() { const containerHeight readerContainer.value.clientHeight const tempDiv document.createElement(div) tempDiv.style.fontSize fontSize.value px tempDiv.style.width readerContainer.value.clientWidth px tempDiv.style.visibility hidden tempDiv.style.position absolute document.body.appendChild(tempDiv) const paragraphs chapterContent.value.split(/\n/) pages.value [] let currentPage for (const para of paragraphs) { tempDiv.innerHTML currentPage para if (tempDiv.scrollHeight containerHeight) { pages.value.push(currentPage) currentPage para } else { currentPage para } } if (currentPage) { pages.value.push(currentPage) } document.body.removeChild(tempDiv) }这个分页算法的思路是「贪心填满」每段落尝试追加进当前页如果当前页高度超过容器高度就把这一页存入数组并另起新页。clientHeight获取的是阅读器容器的可视高度tempDiv.scrollHeight是临时容器内的实际内容高度。这里用隐藏 div 来测量高度而不直接渲染是为了避免在页面上产生可见的闪烁元素。注意字体大小变化后需要重新调用paginateContent()因为同一段文字在不同字号下占用的高度不同分页结果是完全不一样的。进度记录的实现更简单一些核心是三个数据书籍 id、章节索引、章节内页码。每次翻页时把这三个值写入 localStorage下次打开阅读器时优先跳转到历史位置这个体验直接决定了项目答辩时评委的观感——如果你的阅读器每次打开都从第一章第一页开始说明进度存储完全没实现。4. Java 后端接口设计从 Controller 到 VO 的轻量实现4.1 接口清单书籍列表、章节列表、阅读进度的三条线后端接口设计其实就围绕三个资源展开书籍、章节、进度。书籍资源提供列表和详情两个接口章节资源提供按书籍查询章节列表的接口进度资源提供保存和查询两个接口。整个系统的接口量不需要多但要覆盖前端所有页面的数据需求。接口路径请求方式功能说明核心参数/api/booksGET书籍列表书架、分类共用category、page/api/books/{bookId}GET书籍详情bookId/api/books/{bookId}/chaptersGET章节列表bookId/api/books/{bookId}/chapters/{index}GET单个章节正文bookId、index/api/progress/{bookId}GET获取阅读进度bookId/api/progress/{bookId}POST保存阅读进度bookId、chapterIndex、pageIndex这个接口清单设计的思路是「按页面拆资源」书架页需要的是书籍列表和进度详情页需要的是书籍详情和章节列表阅读器需要的是单个章节内容和进度读写。如果你拿到源码后想自己扩展功能优先从这份接口清单里加字段而不是乱加接口——每增加一个接口就要考虑前后端联调成本、跨域配置、参数校验接口越少系统越稳。4.2 Controller 层代码路径参数和响应包装是规范动作后端接口的 Controller 写法决定了接口的可维护性。这套源码里常见的 Controller 方法签名是接收路径参数或请求参数调用 Service 层处理业务统一返回 Result 对象。Result 对象是整个系统的响应规范一般包含三个字段code状态码、message提示信息、data业务数据。RestController RequestMapping(/api/books) public class BookController { Resource private BookService bookService; GetMapping public ResultListBookVO getBooks( RequestParam(required false) String category, RequestParam(defaultValue 1) int page) { ListBookVO books bookService.getBooks(category, page); return Result.success(books); } GetMapping(/{bookId}) public ResultBookDetailVO getBookDetail(PathVariable String bookId) { BookDetailVO detail bookService.getBookDetail(bookId); return Result.success(detail); } GetMapping(/{bookId}/chapters) public ResultListChapterVO getChapters(PathVariable String bookId) { ListChapterVO chapters bookService.getChapters(bookId); return Result.success(chapters); } GetMapping(/{bookId}/chapters/{index}) public ResultChapterDetailVO getChapter( PathVariable String bookId, PathVariable int index) { ChapterDetailVO chapter bookService.getChapter(bookId, index); return Result.success(chapter); } }这段代码有三个细节值得对照自己项目检查。第一RequestParam(required false) String category表示分类是可选参数不传就返回全量书籍传了就按分类过滤——如果不写required false前端分类页第一次加载就会报 400 参数缺失错误。第二PathVariable String bookId接收的是路由路径里的值比如/api/books/B1001中的B1001这个值最终被透传到 Service 层做数据过滤。第三返回值统一用Result.success()包装如果不包装前端 axios 响应拦截器拿到的数据结构就不一致要么是对象要么是数组前端就没法统一处理了。4.3 Service 层和数据组装为什么详情和目录要拆成两次查询Service 层是整个后端的业务核心。拿书籍详情举例一个getBookDetail方法做的事情是根据bookId找到书籍基本信息同时额外读取这本书的章节目录——但这个目录默认只返回章节标题和索引不返回章节正文。如果把几千字的正文也塞进详情接口接口响应时间会明显变慢前端详情页下拉时也会卡顿。Service public class BookServiceImpl implements BookService { Resource private BookMapper bookMapper; Override public BookDetailVO getBookDetail(String bookId) { Book book bookMapper.findBookById(bookId); if (book null) { throw new BusinessException(404, 书籍不存在); } BookDetailVO vo new BookDetailVO(); vo.setBookId(book.getBookId()); vo.setBookName(book.getBookName()); vo.setAuthor(book.getAuthor()); vo.setCategory(book.getCategory()); vo.setIntro(book.getIntro()); vo.setCoverUrl(book.getCoverUrl()); vo.setWordCount(book.getWordCount()); vo.setStatus(book.getStatus()); vo.setChapterList(book.getChapterList()); return vo; } Override public ChapterDetailVO getChapter(String bookId, int index) { Book book bookMapper.findBookById(bookId); if (book null || index 0 || index book.getChapterList().size()) { throw new BusinessException(404, 章节不存在); } Chapter chapter book.getChapterList().get(index); ChapterDetailVO vo new ChapterDetailVO(); vo.setBookId(bookId); vo.setChapterIndex(index); vo.setTitle(chapter.getTitle()); vo.setContent(chapter.getContent()); return vo; } }Service 层这段代码体现了「单一职责」的分层思想getBookDetail返回的是带目录的书籍信息getChapter单独返回某一章正文。注意book.getChapterList().get(index)这个操作——如果index越界直接抛异常而不是返回 null。异常抛出后由全局异常处理器捕获转换成统一的 Result 结构返回给前端。这样前端 axios 响应拦截器读取到code ! 200时可以直接弹出错误提示而不需要靠解析 HTML 文本判断错误类型。另外要留意一个坑很多源码在从 JSON 文件加载数据时如果使用ClassPathResource读取 resources 目录下的文件部署成 jar 包后路径会失效。这里常见做法是用ClassPathResource.getInputStream()读取流而不是直接new File()因为 jar 包内文件不是一个真实的文件系统路径。这个问题很隐蔽多数人本地 IDE 跑得好好的打包部署后就报文件找不到原因就在这。好到这里前后端主流程已经走了两遍。如果你是照着源码在敲现在应该能跑通「书架 → 详情 → 阅读器 → 保存进度」的完整链路了。那接下来我们把所有可能踩的坑集中摆出来这些坑每一个我都见过不止一个人在群里问过。5. 避坑指南跨域、路由刷新 404、打包进 Spring Boot 的常见问题5.1 前端请求后端接口被拦跨域配置不生效现象前端npm run dev启动在 8080 端口后端跑在 9090 端口前端访问后端接口时浏览器报Access-Control-Allow-Origin错误请求状态码是 CORS error。原因浏览器的同源策略拦截了跨域请求。前端地址是localhost:8080后端接口是localhost:9090两个端口不同视为跨域。很多源码把跨域配置写在 Controller 类上的CrossOrigin注解里但如果你只给一个 Controller 加了其他 Controller 照样报错。解决在后端入口类或者配置类里写一个全局 CORS 配置一次性放行所有接口。Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }参数说明addAllowedOriginPattern(*)用*匹配所有来源注意如果使用addAllowedOrigin(*)和setAllowCredentials(true)同时存在部分浏览器会直接拒绝/**是 Ant 风格路径匹配表示所有接口路径都应用该配置。配置生效后请求会先经过 CorsFilter 再进入 Controller预检请求 OPTIONS 也会被正确处理。5.2 前端路由 history 模式刷新后 404现象本地开发时一切正常但部署到服务器后输入网址访问首页没问题刷新二级页面比如/detail/B1001就出现了 404。生产环境配置用的是 Vue Router 的 history 模式。原因history 模式的路由是前端浏览器端处理的刷新时浏览器向服务器请求/detail/B1001这个真实路径而后端根本没有这个静态资源也没有对应的接口于是返回 404。这是前后端分离部署时最经典的问题。解决在 Spring Boot 里加一个转发规则把非接口路径的请求全部转发到 index.html交给前端路由去匹配。注意只转发非/api开头的路径不要影响后端接口。Controller public class RouteForwardController { RequestMapping(value {path:[^\\.]*}) public String forward(PathVariable String path) { return forward:/index.html; } }这个{path:[^\\.]*}正则的写法很讲究它匹配所有不包含点号的路径因为静态资源如 JS、CSS、图片的路径里必然有.js、.css、.jpg后缀这些请求不会被拦截而路由地址如/detail/B1001没有点号会被转发到前端入口。如果这个正则写错静态资源也会被错误转发页面就会白屏。5.3 vue 打包后的静态资源放进 Spring Boot 的 static 目录现象想把前端构建产物直接放进 Spring Boot 里让后端同时托管前端页面但放进static目录后访问首页出现空白控制台报一堆 JS 资源 404。原因构建产物dist目录中的 JS 和 CSS 文件引用的路径多半是绝对路径/js/app.js而不是相对路径。当静态资源被 Spring Boot 托管后访问路径确实应该是根路径下的/js/app.js但如果没有配置静态资源位置Spring Boot 默认只认classpath:/static/、classpath:/public/等固定目录路径就匹配不上。解决先确认构建时publicPath配置。在vue.config.js中设置publicPath: ./让打包后的资源路径变成相对路径这样放入任意目录都能正常访问。然后把dist目录下的文件全部复制到src/main/resources/static/。// vue.config.js module.exports { publicPath: ./, outputDir: dist, assetsDir: static }参数说明publicPath: ./表示资源引用使用当前路径相对定位适合把前端打包进后端的情况assetsDir: static指定 JS、CSS 等静态资源放在打包产物的static子目录下避免和 index.html 混在一起。如果你用的是 Vite 构建对应的配置项是base: ./效果等同。5.4 axios 响应拦截器把错误提示弹得乱七八糟现象后端故意抛了一个业务异常结果前端不但弹了错误提示还伴随页面崩溃控制台同时输出红色报错。原因axios 的response拦截器里做了统一错误处理但又在具体页面里重复处理了同一个错误。比如拦截器里已经console.error并弹了提示页面里的catch块又弹了一次导致重复提示。更常见的问题是拦截器没有区分 HTTP 状态错误和业务码错误。解决在设计拦截器时把「HTTP 失败」和「业务失败」分开处理。HTTP 2xx 范围内的请求都视为请求成功进入response拦截器后只判断code字段HTTP 状态码非 2xx 的错误在error回调里处理。// axios 拦截器 service.interceptors.response.use( (response) { const res response.data if (res.code ! 200) { // 业务错误后端返回 code 非 200 Message({ message: res.message, type: error }) return Promise.reject(new Error(res.message)) } return res }, (error) { // HTTP 错误网络断开、500、404 等 Message({ message: error.message || 请求失败, type: error }) return Promise.reject(error) } )这段代码好处是前端所有页面的错误提示都是同一套文案规范不用每个页面单独写错误处理。注意Promise.reject后面的错误对象会在页面里的.catch继续传递——如果不想让页面再弹一次提示页面里 catch 住之后不要做重复的 message 弹出只做状态重置即可。5.5 JSON 模拟数据时中文乱码和路径找不到现象后端读取 JSON 数据文件时返回给前端的中文是乱码或者启动时报FileNotFoundException。原因多数是因为编码格式问题。JSON 文件保存时如果是 GBK 编码而 Spring Boot 默认读取的字符集是 UTF-8读取出来自然乱码。路径找不到则是文件放错了位置——JSON 文件放在/resources/json/下没问题如果你用new File(json/books.json)相对路径读取本地跑可能正常打包成 jar 后必挂。解决统一先用 UTF-8 保存 JSON 文件读取时指定字符集同时使用ClassPathResource获取资源流。ClassPathResource resource new ClassPathResource(json/books.json); InputStream inputStream resource.getInputStream(); String jsonText new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);参数说明json/books.json是相对于classpath的路径资源和 jar 一起打包后依然可以定位StandardCharsets.UTF_8显式指定解码字符集避免依赖操作系统默认编码。这里如果你用的是inputStream.readAllBytes()注意这个方法只在 Java 9 可用如果你的项目跑在 Java 8 环境需要换成IOUtils.toString(inputStream, UTF-8)或者手动循环读取。5.6 axios 请求的 baseURL 写死导致部署后接口全部失败现象本地开发时接口请求全部正常但部署到服务器后所有请求都变成 404打开控制台发现请求地址是localhost:9090/api/books。原因前端 axios 实例的baseURL写死了http://localhost:9090。本地开发时浏览器和本机后端确实走这个地址但部署后浏览器在用户电脑上运行localhost指向的是用户自己的机器根本不是你的服务器。解决用环境变量区分开发和生产环境的 baseURL。在项目根目录建.env.development和.env.production两个文件通过VITE_APP_BASE_URL或VUE_APP_BASE_URL读取。# .env.production VUE_APP_BASE_URL/apiaxios 配置改为baseURL: process.env.VUE_APP_BASE_URL并把网关层的/api前缀通过 Spring Boot 的server.servlet.context-path处理这样前后端联调的所有地址都变成相对路径不再跟具体 IP 和端口绑定。6. 再加一层进阶玩法阅读进度服务化、字体调节和目录快速跳转前面的内容已经能支撑你完成一个合格的全栈课设。这一章我们来聊点锦上添花的东西——把一个「能跑」的阅读器变成「好用」的阅读器。虽然标题是设计源码但答辩时评委更关注你的思考深度而不是你写了多少代码。阅读器这部分的三个进阶点阅读进度的服务化存储、字体尺寸实时生效和目录快速跳转每一个都能在答辩时讲出设计理由。第一个进阶点是阅读进度从 localStorage 提升到后端服务。本地存储的缺点是换设备后进度全丢这显然不符合真实产品的设计预期。进阶做法是保存进度时调后端的 POST 接口把bookId、chapterIndex、pageIndex三元组传给 Java 后端后端内存中用一个ConcurrentHashMap维护所有书籍的进度或者更进一步写入数据库。这样换设备后只要登录同一个账号进入详情页时先查询进度接口直接跳转到上次阅读位置。这是一个从「前端 Demo」跨越到「真实系统」的关键改动代码量不大但思路完整的讲一遍评委立刻会觉得你不只是抄了页面。第二个进阶点是字体调节的实时生效。阅读器里常见的设置项有字号大小、行间距、字体类型和背景色。实现逻辑不复杂字号数据放在 Vue 的data或ref里页面上的文本绑定:style{ fontSize: fontSize px }然后监听字号变化重新分页。注意重分页的时机——用户拖动字号滑块的每一帧都在连续触发事件如果每帧都重分页性能撑不住。常见做法是用防抖函数只在用户停止调节 300ms 后才执行一次paginateContent()。调完字号还要记住用户偏好把字体设置写进 localStorage下次打开阅读器直接应用这个细节很加分。第三个进阶点是章节目录的快速定位。长篇小说动辄几百章如果目录是一个从上到下的长列表用户想找第一百章要滑很久。进阶方案是分组显示按卷分组的目录结构或者把章节索引每五十章折叠成一组。前端实现上可以先把章节目录按Math.floor(index / 50)分组每组渲染一个折叠面板。后端如果想配合可以在章节目录接口返回时增加一个volumeName字段实现真正的卷概念。这样讲出来的系统设计是完整闭环的——数据层有卷字段服务层有章节服务前端有折叠分组。最后说一个我自己的血泪经验。以前做小说阅读器联调时我把进度保存逻辑写在了阅读器组件的beforeDestroy生命周期里结果用户从小屏幕切到大屏或者从竖屏切到横屏时组件重新渲染进度被错误地清零了。从那以后我每次做阅读进度相关功能都强制把保存动作从生命周期钩子里拆出来放在用户主动翻页和点击返回时手动触发确保任何意外清理都不影响进度数据。希望这个习惯也能帮到你少踩一次数据丢失的坑。本文还有配套的精品资源点击获取
返回列表