🚀 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 或 11;Spring 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+。建议使用nvm(Windows 可用 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 vue@latest 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?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/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 JpaRepository<Article, Long> { // 可以在这里定义自定义查询方法,例如根据标题模糊查询 // List<Article> findByTitleContaining(String keyword); }2.3 编写 Service 与 Controller
Service 层处理业务逻辑,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 List<Article> getAllArticles() { return articleRepository.findAll(); } public Optional<Article> 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 ResponseEntity<List<Article>> getAllArticles() { return ResponseEntity.ok(articleService.getAllArticles()); } @GetMapping("/{id}") public ResponseEntity<Article> getArticleById(@PathVariable Long id) { return articleService.getArticleById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @PostMapping public ResponseEntity<Article> createArticle(@RequestBody Article article) { // 这里可以添加参数校验,如使用 @Valid Article savedArticle = articleService.saveArticle(article); return ResponseEntity.ok(savedArticle); } @PutMapping("/{id}") public ResponseEntity<Article> 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 ResponseEntity<Void> deleteArticle(@PathVariable Long id) { if (!articleService.getArticleById(id).isPresent()) { return ResponseEntity.notFound().build(); } articleService.deleteArticle(id); return ResponseEntity.noContent().build(); } }2.4 解决跨域问题(CORS)
前后端分离项目,前端运行在localhost:5173(Vite 默认),后端在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 Plus:
import { 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 class="article-list"> <el-button type="primary" @click="handleCreate" style="margin-bottom: 20px;"> <el-icon><Plus /></el-icon> 新建文章 </el-button> <el-table :data="articleList" border style="width: 100%"> <el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="title" label="标题" /> <el-table-column prop="author" label="作者" width="120" /> <el-table-column prop="createTime" label="创建时间" width="180"> <template #default="scope"> {{ formatDate(scope.row.createTime) }} </template> </el-table-column> <el-table-column label="操作" width="200"> <template #default="scope"> <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button> <el-button size="small" type="danger" @click="handleDelete(scope.row.id)"> 删除 </el-button> </template> </el-table-column> </el-table> <!-- 新建/编辑对话框 --> <el-dialog v-model="dialogVisible" :title="dialogTitle" width="50%"> <el-form :model="form" label-width="80px"> <el-form-item label="标题" required> <el-input v-model="form.title" placeholder="请输入文章标题" /> </el-form-item> <el-form-item label="作者" required> <el-input v-model="form.author" placeholder="请输入作者" /> </el-form-item> <el-form-item label="内容"> <el-input v-model="form.content" type="textarea" :rows="10" placeholder="请输入文章内容" /> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="submitForm">确定</el-button> </span> </template> </el-dialog> </div> </template> <script setup lang="ts"> 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 = ref<Article[]>([]) const dialogVisible = ref(false) const form = ref<Article>({ title: '', content: '', author: '' }) const isEditMode = ref(false) const editingId = ref<number | 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() }) </script>3.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 id="app"> <el-container> <el-header style="text-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 lang="ts"> // 可以在这里引入全局样式或逻辑 </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; } </style>3.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 开发服务器有时会卡住。尝试重启前端服务 (
Ctrl+C然后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.active=prod - 敏感信息(如密码)应使用环境变量或配置中心(如 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_URL=https://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.active=prod,以及application-prod.yml文件是否存在且内容正确。
- JAR 包找不到主类: 检查
这个从 Spring Boot + Vue 3 搭建博客管理项目的流程,核心不是记住每一步命令,而是理解每个环节为什么这么做,以及出了问题该从哪里看。我建议你在自己机器上完整走一遍,遇到报错先别急着搜答案,按照上面给的排查顺序,自己看看日志、核对配置,这个过程积累的经验比单纯复制代码有价值得多。
🚀 30+款热门AI模型一站整合,DeepSeek/GLM/Qwen 随心用,限时 5 折。 👉 点击领海量免费额度