资讯动态

SpringBoot+Vue前后端分离博客实战:4小时真实开发复刻

发布时间:2026/10/14 1:26:45 来源:尧图企业网站定制
1. 项目概述这不是“速成课”而是一次真实开发节奏的完整复刻“Java大牛带你4小时开发一个SpringBootvue前后端分离博客项目”——这个标题里藏着三个关键信号时间锚点4小时、角色信任Java大牛、技术栈明确SpringBoot Vue。它不是教你怎么“搭个架子”而是模拟一位有五年以上全栈经验的工程师在接到一个轻量级内部知识沉淀系统需求后从零启动、不返工、不调试到崩溃、最终交付可运行原型的真实工作流。我带过十几期类似主题的实战训练发现新手最常卡在“不知道哪些步骤可以跳过、哪些必须死磕”。比如有人花90分钟配MyBatis-Plus的代码生成器却在Vue路由守卫里卡住3小时也有人把精力全放在美化首页动画结果登录态校验逻辑漏了JWT刷新机制。这4小时本质是对开发优先级的肌肉记忆训练数据库设计只建5张表用户、文章、分类、标签、评论接口只实现12个核心CRUD含分页、搜索、软删除前端只做3个视图首页列表、详情页、后台管理所有非核心功能如富文本编辑器、图片云存储、SEO优化全部用占位符或Mock数据绕过。它适合两类人刚学完SpringBoot基础想验证能力边界的后端新人以及会写Vue组件但没跑通过真实API联调的前端同学。你不需要记住所有注解含义但必须清楚为什么Transactional不能加在Service层私有方法上为什么Vue的v-model在表单里比手动绑定input更安全——这些不是考点而是上线前被测试同事揪出来的真问题。2. 整体架构设计与技术选型逻辑拆解2.1 为什么坚持“前后端分离”而非传统MVC很多初学者看到“博客项目”第一反应是用Thymeleaf写模板但这次我们强制采用分离架构原因很实际降低协作摩擦成本。某公司曾让3个实习生合作开发内部文档系统后端用SpringBoot写接口前端用Vue写页面结果两周内80%的会议时间都在解决“为什么我传的JSON字段名是驼峰后端接收却是下划线”这类问题。分离架构把责任切得更干净后端只管数据正确性比如文章发布时间必须早于当前时间、接口响应格式统一所有成功返回{code:200,data:{},msg:ok}、错误码语义清晰401未登录、403无权限、500服务异常前端只管用户体验加载状态提示、表单校验反馈、页面跳转动效。这种分工让双方能并行推进——后端写完用户注册接口时前端已经用Mock.js模拟出注册成功后的跳转逻辑。更重要的是它倒逼你建立契约意识在项目启动前双方必须敲定一份OpenAPI 3.0格式的接口文档哪怕只是用Swagger UI自动生成的而不是等前端写到一半才发现后端返回的分类列表里少了个articleCount字段。我见过太多团队因为省略这一步导致联调阶段每天改10版接口最后连Git提交记录都变成“修复字段名拼写错误”。2.2 SpringBoot版本选择为什么锁定2.7.x而非3.x当前SpringBoot 3.x已全面拥抱Jakarta EE 9这意味着所有javax.*包名要改成jakarta.*。表面看只是字符串替换但实际踩坑无数某实验室用Shiro做权限控制升级到3.x后发现RequiresPermissions注解失效查源码才发现Shiro 2.x依赖的javax.servlet.Filter在新版本里已被移除。而我们的博客项目需要快速验证业务逻辑没时间处理这类底层兼容问题。SpringBoot 2.7.x基于Spring Framework 5.3.x仍是企业级项目的事实标准它对JDK 8支持稳定主流中间件MySQL驱动、Redis客户端、MyBatis适配成熟。更重要的是它的自动配置机制更“透明”——当你在application.yml里写spring.redis.hostlocalhost框架会自动注入RedisTemplate实例而不用像3.x那样手动配置LettuceClientConfigurationBuilder。实测下来用2.7.18版本初始化一个包含Web、JPA、Redis、Mail Starter的项目mvn clean package耗时稳定在28秒内而3.1.0版本平均要41秒。这13秒看似微小但在4小时开发窗口里意味着你能多跑3轮完整构建-测试循环。所以我们的pom.xml里明确锁定parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent提示如果本地Maven仓库没有该版本执行mvn dependency:get -Dartifactorg.springframework.boot:spring-boot-starter-parent:2.7.18提前拉取避免构建时卡在下载环节。2.3 Vue选型为什么用Vue 2.7而非Vue 3Vue 3的Composition API确实更优雅但它的响应式原理Proxy替代Object.defineProperty带来一个隐蔽陷阱无法监听对象属性的动态添加。比如你在博客后台新增一个“是否置顶”字段Vue 2中用this.$set(article, isTop, true)能触发视图更新而Vue 3的reactive()默认不支持。我们的博客项目需要频繁扩展文章元数据比如后期加“阅读时长”“推荐指数”用Vue 2.7最后一个兼容Options API的版本能最大限度复用现有生态Element UI组件库无需重写Vue Router 3的嵌套路由语法更直观Vuex状态管理的学习曲线更平缓。更重要的是Vue 2.7仍获得官方安全更新支持某金融公司用它支撑着日均百万PV的客户门户稳定性经过严苛验证。我们用Vue CLI 4.5创建项目它生成的webpack配置对CSS Modules支持更友好——当你要给文章列表页和详情页写不同样式的.content类时不会出现样式全局污染。执行vue create blog-frontend --default后立即执行vue add element集成UI库比手动配置babel-plugin-component节省至少15分钟。2.4 数据库与ORMH2内存库为何比MySQL更适合教学场景很多人质疑“用H2做博客数据库太假”但真实开发中80%的业务逻辑错误与数据库无关。比如文章发布接口返回50090%概率是Controller层没处理好空指针而不是MySQL连接超时。H2内存库的优势在于启动零延迟jdbc:h2:mem:testdb连接字符串执行后数据库瞬间就绪、无需安装服务不用折腾MySQL的root密码重置、SQL语法完全兼容支持LIMIT、OFFSET分页和MySQL一致。我们用Flyway做数据库版本控制每次启动时自动执行V1__init.sql建表语句这样即使删掉整个target目录重新编译数据结构也能一键恢复。对比MySQL方案你需要提前装好服务端、配置my.cnf文件、创建数据库用户、授权访问权限光是解决“Access denied for user rootlocalhost”这个报错新手平均耗时22分钟。而H2方案从创建项目到看到第一条INSERT INTO article日志全程不超过90秒。当然它只用于开发验证——上线前我们会提供一份mysql-schema.sql脚本把H2的DDL语句转换为MySQL兼容格式这个转换过程本身也是重要的工程实践。3. 核心模块实现与关键细节解析3.1 后端SpringBoot项目骨架搭建与分层规范创建SpringBoot项目时我们刻意避开IDEA的图形化向导坚持用Spring Initializr官网start.spring.io生成ZIP包。原因很简单向导会默认勾选一堆用不到的Starter如Spring Boot DevTools、Lombok而官网界面能让你看清每个依赖的实际作用。我们只勾选5个核心项Spring Web、Spring Data JPA、H2 Database、Validation、Lombok。生成后解压用VS Code打开比IDEA启动快3倍立刻修改pom.xml添加MyBatis-Plus依赖dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency注意版本号必须与SpringBoot 2.7.x匹配MyBatis-Plus 3.5.x系列是最后一个支持JDK 8的版本。接着创建标准包结构controller只处理HTTP协议相关逻辑如参数校验、响应包装、service业务主干如“发布文章需校验分类是否存在”、mapper数据库操作用Select注解写原生SQL比XML更直观、entity实体类用TableName(blog_article)指定物理表名。这里有个关键细节所有Controller方法的返回类型必须是ResultT泛型类而不是ResponseEntity或MapString,Object。我们定义Result类包含code、data、msg三个字段并在ControllerAdvice全局异常处理器中统一拦截RuntimeException返回Result.fail(系统繁忙)。这样做的好处是前端同学只要写一套axios拦截器就能处理所有接口的错误码不用每个API单独写if (res.code ! 200) { alert(res.msg) }。实测下来这个设计让前端联调时间缩短40%因为他们不再需要反复问后端“这个错误码代表什么业务含义”。3.2 前端Vue项目路由与状态管理的极简实现Vue项目初始化后我们不做任何UI美化先用vue-router搭起最小可行路由。router/index.js里只定义3个路径/首页展示文章列表、/article/:id详情页用$route.params.id获取ID、/admin后台需登录态保护。重点在于路由守卫的实现——不用beforeEach全局守卫而是用beforeEnter独享守卫{ path: /admin, component: () import(/views/Admin.vue), beforeEnter: (to, from, next) { if (localStorage.getItem(token)) { next() } else { next(/login) } } }为什么因为全局守卫要判断每个路由而我们的项目只有1个需要鉴权的页面。独享守卫更精准且避免了next()调用时机错误导致的白屏问题比如在异步请求未完成时就调用了next()。状态管理同样走极简路线不用Vuex而是用Vue 2.7的provide/injectAPI。在main.js里创建一个store对象const store { state: { userInfo: null, articles: [] }, mutations: { setUserInfo(user) { this.state.userInfo user } } } Vue.prototype.$store store这样所有组件都能通过this.$store.state.userInfo访问数据比Vuex的mapState写法少写6行代码。对于博客这种读多写少的场景这种“伪状态管理”足够可靠——毕竟用户一天最多发3篇文章状态变更频率远低于电商购物车。3.3 前后端联调跨域与接口契约的落地技巧跨域问题永远是新手第一道坎。我们不用CrossOrigin注解它会让每个Controller方法都暴露CORS头而是在WebMvcConfigurer中统一配置Configuration public class WebConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:8080) // 前端开发服务器地址 .allowCredentials(true) .maxAge(3600); } }关键点在于allowedOrigins必须写具体域名不能用*否则allowCredentials会失效。前端axios配置里withCredentials: true必须和后端allowCredentials(true)严格对应否则浏览器会静默丢弃带Cookie的请求。接口契约方面我们用Swagger2生成文档但不依赖它的UI界面。在pom.xml里添加dependency groupIdio.springfox/groupId artifactIdspringfox-swagger2/artifactId version2.9.2/version /dependency然后在SwaggerConfig.java中配置扫描包路径。启动项目后访问http://localhost:8081/swagger-ui.html能看到所有API的入参、出参、状态码说明。但真正的价值在于把这个页面截图发给前端同学他们就能按图索骥写调用代码不用再问“文章列表接口返回的数据结构长什么样”。我们甚至把Swagger的JSON定义导出为openapi.json用swagger-codegen-cli生成TypeScript接口定义文件让前端直接import { ArticleApi } from ./api连手写interface Article { title: string; content: string; }都省了。3.4 安全加固JWT令牌与权限控制的轻量级实现博客项目不需要RBAC基于角色的访问控制那么复杂我们用JWT实现两级权限普通用户可读文章、评论、管理员可增删改文章。JWT生成不依赖第三方库用jjwt-api和jjwt-impl两个轻量包dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency登录成功后后端生成TokenString token Jwts.builder() .setSubject(user.getUsername()) .claim(role, user.getRole()) // 存储角色信息 .setExpiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, blog-secret-key) .compact();关键细节密钥blog-secret-key不能硬编码在代码里而要从application.yml读取jwt: secret: ${BLOG_JWT_SECRET:blog-secret-key} expire: 86400这样上线时可通过环境变量BLOG_JWT_SECRET覆盖避免密钥泄露。前端把Token存localStorage每次请求在Header里带上Authorization: Bearer xxx。后端用OncePerRequestFilter拦截所有/api/**请求解析Token并设置SecurityContextHolder。权限校验用PreAuthorize(hasRole(ADMIN))注解比在Service层写if (!user.isAdmin()) throw new AccessDeniedException()更符合Spring Security规范。实测下来这套方案比Shiro节省300行代码且JWT的无状态特性让水平扩展更简单——加一台服务器不用同步Session数据。4. 实操全流程与关键节点详解4.1 第1小时环境准备与后端骨架搭建0:00-1:00打开终端执行curl https://start.spring.io/starter.zip -o blog-backend.zip直接下载初始项目比网页点击快。解压后进入目录用VS Code打开。第一步不是写代码而是改application.ymlspring: datasource: url: jdbc:h2:mem:testdb driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop show-sql: true properties: hibernate: format_sql: true重点看ddl-auto: create-drop——每次应用重启都会重建表结构确保你不会被残留数据干扰。接着创建entity/Article.javaData TableName(blog_article) public class Article { TableId(type IdType.AUTO) private Long id; private String title; private String content; private LocalDateTime createTime; private LocalDateTime updateTime; private Long categoryId; }这里Data来自Lombok避免手写getter/setterTableName指定物理表名防止MyBatis-Plus默认用类名article而我们希望是blog_article。执行mvn compile验证编译通过此时H2控制台已可用访问http://localhost:8081/h2-console输入JDBC URLjdbc:h2:mem:testdb即可查看实时表结构。这一步耗时约25分钟但奠定了后续所有开发的稳定性基础——很多人的项目卡在第2小时根源就是第1小时没配好数据库连接。4.2 第2小时核心接口开发与Swagger文档生成1:00-2:00创建controller/ArticleController.java用RestController标注RestController RequestMapping(/api/articles) public class ArticleController { Autowired private ArticleService articleService; GetMapping public ResultListArticle list(RequestParam(defaultValue 0) Integer page, RequestParam(defaultValue 10) Integer size) { return Result.success(articleService.list(page, size)); } PostMapping public ResultArticle save(RequestBody Valid Article article) { return Result.success(articleService.save(article)); } }注意Valid注解触发JSR-303校验需要在Article类的字段上加NotBlank(message标题不能为空)。ArticleService实现类里list()方法用MyBatis-Plus的Page对象public ListArticle list(Integer page, Integer size) { PageArticle pageObj new Page(page, size); return articleMapper.selectPage(pageObj, null).getRecords(); }执行mvn spring-boot:run启动服务访问http://localhost:8081/swagger-ui.html能看到GET /api/articles接口的在线文档。点击“Try it out”输入page0、size5执行后返回空数组[]——这是预期结果证明接口链路通畅。此时前端同学可以开始写调用代码不用等后端把所有接口写完。我们刻意把save接口放在第2小时实现因为它是后续所有功能如后台发布文章的前提。实测发现新手常在这里犯错把RequestBody写成RequestParam导致接收不到JSON数据。解决方案是在Controller方法里加一行日志log.info(Received article: {}, article)如果日志打印出null基本确定是注解用错了。4.3 第3小时Vue前端页面开发与Mock数据联调2:00-3:00前端项目用vue create blog-frontend创建后先安装axiosnpm install axios。在src/utils/request.js里封装请求import axios from axios const request axios.create({ baseURL: http://localhost:8081/api, timeout: 5000 }) request.interceptors.response.use( response response.data, // 统一取data字段 error Promise.reject(error) ) export default request这个封装让所有API调用只需写request.get(/articles)不用重复处理response.data。接着写首页组件views/Home.vuetemplate div h2文章列表/h2 div v-forarticle in articles :keyarticle.id h3{{ article.title }}/h3 p{{ article.content.substring(0, 100) }}.../p router-link :to/article/${article.id}查看详情/router-link /div /div /template script import request from /utils/request export default { data() { return { articles: [] } }, async created() { const res await request.get(/articles) this.articles res.data } } /script关键点created()钩子中用async/await发起请求比mounted()更早触发v-for必须用:key绑定唯一标识否则Vue会复用DOM元素导致显示错乱。此时启动前端npm run serve访问http://localhost:8080能看到空白列表——因为后端还没存数据。我们用H2控制台手动插入测试数据INSERT INTO blog_article (title, content, create_time, update_time, category_id) VALUES (我的第一篇博客, Hello World!, NOW(), NOW(), 1);刷新页面文章即刻显示。这比写后端测试类快得多30秒内完成数据准备。4.4 第4小时前后端联调与部署打包3:00-4:00最后1小时聚焦真实联调。前端在Admin.vue里写发布文章表单template form submit.preventhandleSubmit input v-modelform.title placeholder标题 / textarea v-modelform.content placeholder内容/textarea button typesubmit发布/button /form /template script import request from /utils/request export default { data() { return { form: { title: , content: } } }, methods: { async handleSubmit() { await request.post(/articles, this.form) this.$message.success(发布成功) this.$router.push(/) } } } /script后端ArticleController的save()方法接收数据MyBatis-Plus自动映射到Article对象。启动前后端登录后台发布文章观察浏览器Network面板POST /api/articles返回200响应体包含id字段证明数据已入库。此时打开H2控制台执行SELECT * FROM blog_article确认数据存在。最后打包后端执行mvn clean package生成target/blog-backend-0.0.1-SNAPSHOT.jar前端执行npm run build生成dist/目录。用Nginx部署时把dist/目录设为root后端jar包用java -jar启动配置Nginx反向代理location /api/ { proxy_pass http://localhost:8081/; proxy_set_header Host $host; }这样前端静态资源和后端API共用一个域名彻底解决跨域问题。整个流程走通后你会发现自己对SpringBoot的自动配置、Vue的响应式原理、HTTP协议细节的理解比看10篇教程都深刻。5. 常见问题排查与独家避坑指南5.1 后端高频问题速查表问题现象可能原因排查命令/操作解决方案启动时报Failed to configure a DataSourceapplication.yml中datasource配置缺失或格式错误检查yml缩进用在线YAML校验工具验证确保spring.datasource.url等字段顶格冒号后有空格Swagger页面404SpringBoot版本与Swagger2不兼容访问http://localhost:8081/v2/api-docs看是否返回JSON升级Swagger2到2.9.2或降级SpringBoot到2.6.x文章列表返回空数组但H2里有数据MyBatis-Plus的selectPage未查到记录在Mapper接口方法上加Select(SELECT * FROM blog_article)测试检查TableName值是否与H2表名完全一致大小写敏感JWT解析失败Invalid signatureToken密钥前后端不一致用在线JWT解码网站验证Token前端Authorization头中的Token必须与后端生成时用的密钥相同注意遇到NoSuchMethodError异常90%是Jar包冲突。执行mvn dependency:tree -Dverbose | grep xxx查找冲突依赖用exclusion排除低版本包。5.2 前端典型故障处理新手在npm run serve时最常遇到Module not found: Error: Cant resolve vue这通常是因为package.json里vue版本写成了^3.0.0Vue 3而项目用的是Vue 2.7。解决方案执行npm install vue2.7.14 --save强制降级。另一个高频问题是路由跳转后页面空白检查router/index.js里的component路径是否正确——import(/views/Home.vue)中的符号必须存在它指向src/目录如果写成./views/Home.vue会导致路径解析失败。还有人抱怨表单提交后页面没反应其实是因为form默认会触发页面刷新必须加submit.prevent阻止默认行为这个.prevent修饰符不能漏掉。5.3 联调阶段致命陷阱最大的坑是时间戳格式不一致。后端用LocalDateTime前端JavaScript的Date.now()返回毫秒数直接传给后端会报JSON parse error。解决方案前端提交前格式化const article { title: this.form.title, content: this.form.content, createTime: new Date().toISOString().slice(0, 19).replace(T, ) // 2023-01-01 12:00:00 }后端Article类的createTime字段加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解。另一个隐形杀手是中文乱码H2数据库URL要加DB_CLOSE_DELAY-1参数否则关闭连接时可能丢失中文数据。application.yml里写成spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY-15.4 性能优化的早期埋点虽然博客项目QPS不高但养成性能习惯很重要。我们在ArticleController的list()方法上加ResponseStatus(HttpStatus.OK)避免Spring MVC默认返回200之外的状态码在ArticleMapper接口的selectPage方法上加Select(SELECT id,title,content,create_time FROM blog_article ORDER BY create_time DESC LIMIT #{size} OFFSET #{current})用原生SQL替代MyBatis-Plus的selectPage减少一层对象转换开销。实测10万条数据分页查询原生SQL比selectPage快120ms。这些优化在4小时开发中看似多余但它们塑造了工程师对性能的敏感度——就像老司机开车时永远会看后视镜不是因为马上要用而是肌肉记忆。6. 项目延展与工程化进阶路径这个4小时项目只是起点。如果想把它变成生产级系统下一步要做的不是堆功能而是补工程能力。比如增加单元测试用JUnit 5写ArticleServiceTest用DataJpaTest注解启动内存数据库测试save()方法是否正确插入数据。覆盖率不用追求100%但核心业务逻辑如“发布文章时分类ID不存在则抛异常”必须有测试覆盖。CI/CD方面用GitHub Actions配置自动化流水线每次push代码自动运行mvn test、npm test、mvn package失败时发邮件告警。部署环节把后端jar包构建成Docker镜像用docker-compose.yml定义MySQL、Redis、Nginx服务一条命令docker-compose up -d启动全栈环境。这些动作单个都不难但组合起来就构成了现代软件工程的基础设施。我自己维护的一个开源博客项目就是从这个4小时原型起步三年间逐步接入了Elasticsearch全文检索、MinIO对象存储、Sentinel流量控制现在支撑着日均2万UV的访问量。关键不在于技术多炫酷而在于每一步都解决了一个真实痛点当初加Elasticsearch是因为MySQL的LIKE %关键词%查询在10万数据时慢到无法忍受加MinIO是因为H2内存库根本存不了图片。所以别急着学新技术先把你手上的博客项目跑满一周记录下它在哪一刻卡住了、哪次请求超时了、哪个功能用户反馈不好——那些真实的痛点才是你技术成长的坐标原点。

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

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

免费获取报价 →
↑