Spring Boot与Vue 3全栈开发实战:从零搭建博客系统
30款热门AI模型一站整合DeepSeek/GLM/Qwen 随心用限时 5 折。 点击领海量免费额度这类项目最值得先看的不是功能列表而是能不能在普通开发环境里从零开始、一步步把前后端都跑起来并且理解每个配置和代码块的作用。很多教程要么只讲后端要么只贴前端代码中间的环境、数据库、跨域、打包部署这些衔接点经常一笔带过导致新手跟着做总卡在奇怪的地方。我建议把整个流程拆成四个阶段环境准备与项目初始化、后端服务搭建与核心配置、前端项目开发与联调、以及最后的打包部署与问题排查。下面我会按这个顺序结合 Spring Boot 和 Vue 3 的常见实践把每个环节的关键步骤、参数含义和容易踩的坑都过一遍。1. 环境准备别急着创建项目先把这些基础条件确认好很多人一上来就用 IDE 的向导创建 Spring Boot 或 Vue 项目结果跑到一半发现 JDK 版本不对、Node 版本冲突、数据库连不上。所以第一步不是写代码而是把运行环境、工具版本和网络条件先对齐。1.1 开发工具与运行环境清单本地开发通常需要以下组件版本尽量选长期支持LTS或项目明确要求的JDK: Spring Boot 2.7.x 推荐 JDK 8 或 11Spring Boot 3.x 则需要 JDK 17 或以上。在命令行执行java -version确认。Maven: 用于管理 Spring Boot 项目的依赖和构建。执行mvn -v确认版本如 3.6.3。确保settings.xml中的镜像源如阿里云 Maven 镜像配置正确能加速依赖下载。Node.js npm: Vue 3 开发环境的基础。执行node -v和npm -v确认。Vue 3 通常需要 Node.js 14.18 / 16。建议使用nvmWindows 可用 nvm-windows管理 Node 版本避免全局安装冲突。IDE: IntelliJ IDEA社区版或旗舰版或 VS Code。IDEA 对 Spring Boot 支持更好VS Code 配合 Volar 插件是 Vue 3 开发的常见选择。数据库: 根据项目需要准备。MySQL是最常见的搭配确保已安装如 MySQL 8.0并启动了服务。记得提前创建好一个用于本项目的空数据库例如blog_db。浏览器: 用于调试前端建议 Chrome 或 Edge并安装 Vue Devtools 插件。注意不要使用过新或过旧的版本。例如Spring Boot 2.7.18 与 JDK 17 可能存在一些兼容性警告而 Vue 3 在 Node.js 18 的某些版本下也可能有构建问题。如果教程或项目模板指定了版本优先遵循。1.2 项目初始化两种主流创建方式的选择创建项目有两种常见路径适用于不同场景方式一使用 Spring Initializr 和 Vite 分别创建这是最清晰、最能理解结构的方式适合学习。后端: 通过 start.spring.io 或 IDEA 内置的 Spring Initializr 创建。选择项目类型Maven Project。Language: Java。Spring Boot: 选择一个稳定版本如 2.7.18。填写 Group如com.example和 Artifact如blog-backend。依赖至少选择Spring Web用于 REST API、Spring Data JPA操作数据库、MySQL Driver数据库连接。还可以根据需要加Lombok简化代码、Spring Security安全等。生成并下载项目用 IDEA 打开。前端: 在另一个独立的目录下使用 Vite 创建 Vue 3 项目。npm create vuelatest blog-frontend根据提示选择需要的特性通常必选TypeScript、Vue Router、Pinia状态管理。Element Plus或Ant Design Vue等 UI 库可以现在装也可以后续按需安装。进入项目目录安装依赖cd blog-frontend npm install。方式二使用一体化项目模板如若依这种方式能快速得到一个具备基础功能用户管理、权限、代码生成的后台系统适合想快速搭建原型或专注于业务开发的场景。从 Gitee 或 GitHub 克隆若依前后端分离版RuoYi-Vue项目。后端是一个完整的 Spring Boot 项目前端是一个集成了 Vue 3、Element Plus 等的项目。按照其文档导入数据库脚本修改后端配置文件中的数据库连接信息分别启动后端和前端。对于从零学习我更推荐方式一。虽然步骤多一点但你能清楚地知道每个文件是怎么来的依赖是如何引入的对后续排查问题和自定义开发更有帮助。2. 后端搭建核心是配置、实体、接口与联调后端项目创建好后目录结构是标准的 Maven 项目。核心工作集中在src/main/java代码、src/main/resources配置和pom.xml依赖。2.1 数据库连接与 JPA 配置这是后端能跑起来的第一个关键点。打开src/main/resources/application.properties或application.yml配置数据库连接。application.yml 示例 (更清晰):server: port: 8080 # 后端服务启动端口 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/blog_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root # 你的数据库用户名 password: yourpassword # 你的数据库密码 jpa: hibernate: ddl-auto: update # 启动时根据实体类自动更新表结构生产环境慎用或改为 validate show-sql: true # 控制台打印SQL调试用 properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect # 指定方言关键参数解释ddl-auto:create每次启动删表重建数据会丢update更新表结构常用但复杂变更可能失败validate只校验实体与表是否匹配none什么都不做。开发阶段用update方便上线前务必改为validate或none并通过 Flyway/Liquibase 管理数据库变更。show-sql: 设为true后控制台会打印 JPA 执行的 SQL是调试和优化查询的利器。dialect: 必须与你的数据库版本匹配否则可能遇到语法错误。2.2 创建数据实体与 Repository假设我们要做一个简单的博客系统先创建Article文章实体。package com.example.blogbackend.entity; import lombok.Data; import javax.persistence.*; import java.time.LocalDateTime; Entity Data // Lombok 注解自动生成 getter, setter, toString 等 Table(name article) // 指定表名 public class Article { Id GeneratedValue(strategy GenerationType.IDENTITY) // 主键自增 private Long id; Column(nullable false, length 200) private String title; // 文章标题 Lob // 大文本字段 Column(columnDefinition TEXT) private String content; // 文章内容 Column(nullable false) private String author; // 作者 Column(name create_time, updatable false) private LocalDateTime createTime; // 创建时间 Column(name update_time) private LocalDateTime updateTime; // 更新时间 // 在持久化前自动设置时间 PrePersist protected void onCreate() { createTime LocalDateTime.now(); updateTime LocalDateTime.now(); } PreUpdate protected void onUpdate() { updateTime LocalDateTime.now(); } }然后创建 Repository 接口继承JpaRepository即可获得基础的增删改查方法。package com.example.blogbackend.repository; import com.example.blogbackend.entity.Article; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; Repository public interface ArticleRepository extends JpaRepositoryArticle, Long { // 可以在这里定义自定义查询方法例如根据标题模糊查询 // ListArticle findByTitleContaining(String keyword); }2.3 编写 Service 与 ControllerService 层处理业务逻辑Controller 层暴露 REST API。Service 示例:package com.example.blogbackend.service; import com.example.blogbackend.entity.Article; import com.example.blogbackend.repository.ArticleRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.Optional; Service public class ArticleService { Autowired private ArticleRepository articleRepository; public ListArticle getAllArticles() { return articleRepository.findAll(); } public OptionalArticle getArticleById(Long id) { return articleRepository.findById(id); } public Article saveArticle(Article article) { return articleRepository.save(article); } public void deleteArticle(Long id) { articleRepository.deleteById(id); } }Controller 示例 (RESTful API):package com.example.blogbackend.controller; import com.example.blogbackend.entity.Article; import com.example.blogbackend.service.ArticleService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; RestController RequestMapping(/api/articles) // API 路径前缀 public class ArticleController { Autowired private ArticleService articleService; GetMapping public ResponseEntityListArticle getAllArticles() { return ResponseEntity.ok(articleService.getAllArticles()); } GetMapping(/{id}) public ResponseEntityArticle getArticleById(PathVariable Long id) { return articleService.getArticleById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } PostMapping public ResponseEntityArticle createArticle(RequestBody Article article) { // 这里可以添加参数校验如使用 Valid Article savedArticle articleService.saveArticle(article); return ResponseEntity.ok(savedArticle); } PutMapping(/{id}) public ResponseEntityArticle updateArticle(PathVariable Long id, RequestBody Article article) { // 简单实现先查是否存在再更新。实际应考虑乐观锁等。 if (!articleService.getArticleById(id).isPresent()) { return ResponseEntity.notFound().build(); } article.setId(id); // 确保ID一致 Article updatedArticle articleService.saveArticle(article); return ResponseEntity.ok(updatedArticle); } DeleteMapping(/{id}) public ResponseEntityVoid deleteArticle(PathVariable Long id) { if (!articleService.getArticleById(id).isPresent()) { return ResponseEntity.notFound().build(); } articleService.deleteArticle(id); return ResponseEntity.noContent().build(); } }2.4 解决跨域问题CORS前后端分离项目前端运行在localhost:5173Vite 默认后端在localhost:8080浏览器会因为同源策略阻止请求。需要在后端配置 CORS。全局 CORS 配置类:package com.example.blogbackend.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class CorsConfig { Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) // 针对所有 /api 开头的路径 .allowedOrigins(http://localhost:5173) // 允许的前端地址 .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true); // 如果需要传递 Cookie 等凭证 } }; } }现在启动 Spring Boot 应用运行BlogBackendApplication类的 main 方法。访问http://localhost:8080/api/articles应该能看到一个空数组[]因为数据库还没数据。这说明后端 API 已经就绪。3. 前端开发从页面组件到调用后端 API前端项目使用 Vite Vue 3 TypeScript 的组合。核心是src/目录下的组件、路由和状态管理。3.1 安装 UI 库与 HTTP 客户端为了快速搭建界面我们安装 Element Plus 和 Axios。# 在 blog-frontend 目录下执行 npm install element-plus axios npm install -D element-plus/icons-vue # 图标库在main.ts中全局引入 Element Plusimport { createApp } from vue import App from ./App.vue import router from ./router import ElementPlus from element-plus import element-plus/dist/index.css import * as ElementPlusIconsVue from element-plus/icons-vue const app createApp(App) // 注册所有图标 for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } app.use(router) app.use(ElementPlus) app.mount(#app)创建src/utils/request.ts来封装 Axios 实例统一处理请求和响应import axios from axios const request axios.create({ baseURL: http://localhost:8080/api, // 后端 API 基础地址 timeout: 10000 // 超时时间 }) // 请求拦截器 request.interceptors.request.use( config { // 可以在这里统一添加 token 等请求头 // const token localStorage.getItem(token) // if (token) { // config.headers.Authorization Bearer ${token} // } return config }, error { return Promise.reject(error) } ) // 响应拦截器 request.interceptors.response.use( response { // 对响应数据做点什么 return response.data }, error { // 对响应错误做点什么比如统一提示错误信息 console.error(请求失败:, error) return Promise.reject(error) } ) export default request3.2 创建博客文章管理页面创建src/views/ArticleListView.vue用于展示文章列表和操作。template div classarticle-list el-button typeprimary clickhandleCreate stylemargin-bottom: 20px; el-iconPlus //el-icon 新建文章 /el-button el-table :dataarticleList border stylewidth: 100% el-table-column propid labelID width80 / el-table-column proptitle label标题 / el-table-column propauthor label作者 width120 / el-table-column propcreateTime label创建时间 width180 template #defaultscope {{ formatDate(scope.row.createTime) }} /template /el-table-column el-table-column label操作 width200 template #defaultscope el-button sizesmall clickhandleEdit(scope.row)编辑/el-button el-button sizesmall typedanger clickhandleDelete(scope.row.id) 删除 /el-button /template /el-table-column /el-table !-- 新建/编辑对话框 -- el-dialog v-modeldialogVisible :titledialogTitle width50% el-form :modelform label-width80px el-form-item label标题 required el-input v-modelform.title placeholder请输入文章标题 / /el-form-item el-form-item label作者 required el-input v-modelform.author placeholder请输入作者 / /el-form-item el-form-item label内容 el-input v-modelform.content typetextarea :rows10 placeholder请输入文章内容 / /el-form-item /el-form template #footer span classdialog-footer el-button clickdialogVisible false取消/el-button el-button typeprimary clicksubmitForm确定/el-button /span /template /el-dialog /div /template script setup langts import { ref, onMounted, computed } from vue import { ElMessage, ElMessageBox } from element-plus import { Plus } from element-plus/icons-vue import request from /utils/request interface Article { id?: number title: string content: string author: string createTime?: string updateTime?: string } const articleList refArticle[]([]) const dialogVisible ref(false) const form refArticle({ title: , content: , author: }) const isEditMode ref(false) const editingId refnumber | null(null) const dialogTitle computed(() { return isEditMode.value ? 编辑文章 : 新建文章 }) // 加载文章列表 const loadArticles async () { try { const data await request.get(/articles) articleList.value data } catch (error) { ElMessage.error(获取文章列表失败) } } // 格式化日期 const formatDate (dateString: string) { if (!dateString) return return new Date(dateString).toLocaleString() } // 打开新建对话框 const handleCreate () { isEditMode.value false form.value { title: , content: , author: } dialogVisible.value true } // 打开编辑对话框 const handleEdit (row: Article) { isEditMode.value true editingId.value row.id! form.value { ...row } dialogVisible.value true } // 提交表单新建或更新 const submitForm async () { if (!form.value.title || !form.value.author) { ElMessage.warning(标题和作者不能为空) return } try { if (isEditMode.value editingId.value) { await request.put(/articles/${editingId.value}, form.value) ElMessage.success(更新成功) } else { await request.post(/articles, form.value) ElMessage.success(创建成功) } dialogVisible.value false loadArticles() // 刷新列表 } catch (error) { ElMessage.error(操作失败) } } // 删除文章 const handleDelete (id: number) { ElMessageBox.confirm(确定要删除这篇文章吗, 提示, { confirmButtonText: 确定, cancelButtonText: 取消, type: warning }).then(async () { try { await request.delete(/articles/${id}) ElMessage.success(删除成功) loadArticles() } catch (error) { ElMessage.error(删除失败) } }).catch(() { // 用户取消 }) } onMounted(() { loadArticles() }) /script3.3 配置路由与导航修改src/router/index.ts将文章列表页面添加到路由中。import { createRouter, createWebHistory } from vue-router import ArticleListView from /views/ArticleListView.vue const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: /, redirect: /articles // 默认重定向到文章列表 }, { path: /articles, name: articles, component: ArticleListView } // 可以在这里添加更多路由如文章详情页、登录页等 ] }) export default router修改src/App.vue添加一个简单的导航和路由视图。template div idapp el-container el-header styletext-align: center; line-height: 60px; background-color: #409EFF; color: white; h1博客管理系统/h1 /el-header el-main router-view / /el-main /el-container /div /template script setup langts // 可以在这里引入全局样式或逻辑 /script style #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #2c3e50; margin: 0; padding: 0; } /style3.4 启动前端并联调确保后端服务localhost:8080正在运行。然后启动前端开发服务器npm run devVite 通常会启动在http://localhost:5173。打开浏览器访问该地址你应该能看到博客管理系统的界面。尝试点击“新建文章”填写内容并提交然后在列表中查看。如果操作成功数据会通过后端 API 保存到 MySQL 数据库并实时刷新列表。联调常见问题排查跨域错误 (CORS): 浏览器控制台出现Access-Control-Allow-Origin错误。检查后端CorsConfig中allowedOrigins是否包含了前端地址如http://localhost:5173。404 错误: 前端请求的 API 路径如/api/articles与后端 Controller 的RequestMapping路径是否匹配检查网络请求的完整 URL。500 错误: 后端服务内部错误。查看 IDEA 或命令行中的后端控制台日志通常会有详细的异常堆栈信息。常见原因数据库连接失败、实体类字段与数据库表结构不匹配、空指针等。前端热更新失效: Vite 开发服务器有时会卡住。尝试重启前端服务 (CtrlC然后npm run dev)。4. 打包部署与生产环境考量本地开发跑通后下一步是考虑如何打包和部署到生产环境如阿里云服务器。这涉及到配置分离、性能优化和部署流程。4.1 后端打包与配置分离Spring Boot 项目使用 Maven 打包。# 在 blog-backend 根目录下执行 mvn clean package -DskipTests打包后会在target/目录下生成一个可执行的 JAR 文件如blog-backend-0.0.1-SNAPSHOT.jar。生产环境配置分离开发时我们把数据库密码等写在application.yml里生产环境绝不能这样。Spring Boot 支持多环境配置。创建application-prod.yml文件存放生产环境配置如正式数据库地址、Redis 配置等。在application.yml中通过spring.profiles.active指定激活哪个配置。生产环境通常通过启动参数指定java -jar blog-backend-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod敏感信息如密码应使用环境变量或配置中心如 Apollo, Nacos管理而不是硬编码在配置文件中。4.2 前端打包与部署Vue 项目打包# 在 blog-frontend 目录下执行 npm run build打包后会在dist/目录下生成静态文件HTML, JS, CSS。前端部署有两种常见方式单独部署: 将dist/目录下的文件上传到 Nginx 或 Apache 等 Web 服务器并配置一个域名或端口如https://blog.yourdomain.com。此时前端需要知道后端 API 的地址可以通过环境变量或构建时注入。例如在 Vite 中可以在.env.production文件中定义VITE_API_BASE_URLhttps://api.yourdomain.com然后在代码中通过import.meta.env.VITE_API_BASE_URL获取。与后端一起部署: 将dist/目录下的文件复制到 Spring Boot 项目的src/main/resources/static/目录下然后重新打包后端。这样前端资源会被打包进 JAR 文件Spring Boot 会同时提供 API 和静态页面服务。这种方式适合小型项目或演示但不利于前后端独立更新和 CDN 加速。4.3 生产环境部署 checklist当项目要上线时除了代码还需要关注以下方面数据库:使用生产环境的数据库如云数据库 RDS不要用本地数据库。关闭 JPA 的ddl-auto: update改为validate或none。所有表结构变更通过 SQL 脚本管理。为数据库连接配置连接池参数如 HikariCP优化性能。安全:API 接口考虑添加认证和授权如集成 Spring Security JWT。对用户输入进行严格的校验和过滤防止 SQL 注入、XSS 攻击。生产环境关闭 Swagger 等调试接口或限制访问 IP。日志:配置日志级别如application-prod.yml中设置logging.level.root: WARN避免输出过多调试信息。将日志输出到文件并配置日志滚动策略便于排查问题。监控与健康检查:Spring Boot Actuator 可以提供应用健康状态、指标等端点方便监控。性能:前端检查打包体积使用代码分割、懒加载、CDN 引入第三方库等方式优化。后端考虑数据库索引、缓存如 Redis、异步处理等优化手段。4.4 常见问题与排查思路即使按照步骤操作也可能遇到问题。这里提供一个通用的排查顺序项目启动失败:看日志: 控制台输出的错误信息是第一步。重点关注Caused by:后面的根本原因。查端口: 端口被占用netstat -ano | findstr :8080(Windows) 或lsof -i:8080(Linux/Mac) 查看。查依赖: Maven 依赖下载是否完整尝试mvn clean install -U强制更新。查配置:application.yml格式是否正确缩进数据库连接信息对不对。前端访问后端 API 失败:看网络请求: 浏览器开发者工具的 Network 标签看请求状态码404, 500, CORS error。核对地址: 前端request.ts中的baseURL是否与后端运行地址一致。核对路径: 前端请求的路径如/articles与后端 Controller 的RequestMapping如/api/articles是否拼接正确。后端日志: 查看后端控制台请求是否收到是否有业务异常数据库操作异常:连接失败: 检查数据库服务是否启动用户名密码是否正确网络是否可达云服务器安全组是否开放端口。表不存在/字段不对: 检查实体类Entity,Table,Column注解配置对比数据库实际表结构。show-sql: true可以帮助查看生成的 SQL。打包后运行异常:JAR 包找不到主类: 检查pom.xml中的spring-boot-maven-plugin配置。静态资源 404: 如果前后端一起打包检查dist文件是否正确复制到了static目录。生产配置未生效: 检查启动命令是否指定了--spring.profiles.activeprod以及application-prod.yml文件是否存在且内容正确。这个从 Spring Boot Vue 3 搭建博客管理项目的流程核心不是记住每一步命令而是理解每个环节为什么这么做以及出了问题该从哪里看。我建议你在自己机器上完整走一遍遇到报错先别急着搜答案按照上面给的排查顺序自己看看日志、核对配置这个过程积累的经验比单纯复制代码有价值得多。 30款热门AI模型一站整合DeepSeek/GLM/Qwen 随心用限时 5 折。 点击领海量免费额度