30款热门AI模型一站整合DeepSeek/GLM/Qwen 随心用限时 5 折。 点击领海量免费额度最近在带几个刚入行的朋友做项目发现一个挺有意思的现象很多人把“从零开始”理解成了“从安装软件开始”。他们花大量时间在环境配置、依赖安装、版本对齐上等真正开始写业务逻辑时已经精疲力尽甚至忘了最初要解决什么问题。这让我想起几年前自己带的一个博客管理项目。当时的目标很明确做一个能发布文章、管理分类、处理评论的后台加上一个干净的前台展示页面。技术栈选了当时比较主流的 Spring Boot 和 Vue 3。听起来是个标准的“新手练手项目”对吧但真正做下来才发现从“项目能跑起来”到“项目能稳定运行、便于维护、前后端协作顺畅”中间隔着好几道需要主动跨越的坎。这些坎往往不是技术本身有多难而是工程化思维和流程设计的缺失。今天我想抛开那些按部就班的“第一步点这里第二步点那里”的教程和你聊聊当我们说“从零带做一个 Spring Boot Vue 3 的博客管理项目”时我们真正在构建什么它绝不仅仅是一个技术栈的拼装练习而是一次完整的、从想法到可运行产品的微型工程实践。我会围绕几个核心判断来展开第一项目的价值不在于功能列表而在于你如何设计数据流动和状态管理第二前后端分离的关键不是技术选型而是清晰的职责边界与协作契约第三 “详细教学”的真正含义是带你理解每一个配置、每一行代码背后的“为什么”而不仅仅是“怎么做”。下面我们就沿着这条主线把项目拆开来看。1. 先想清楚我们要做一个什么样的“博客管理”很多人一上来就打开 IDE 创建项目然后开始建表、写实体类、写接口。这就像盖房子不打地基先开始砌墙。对于博客系统在写第一行代码之前我们需要先回答几个更根本的问题。1.1 核心数据模型文章不仅仅是标题和内容博客的核心是文章Post。但一个用于管理的博客系统文章实体远不止title和content两个字段。我们需要考虑状态管理文章是“草稿”、“已发布”还是“已删除”这决定了前台能否看到以及后台管理的筛选逻辑。分类与标签这是两种不同的组织方式。分类Category通常是树状或扁平的一篇文章通常属于一个分类标签Tag是平面的、多对多的用于更灵活的内容关联。元信息发布时间、更新时间、作者、浏览量、是否置顶、是否允许评论等。这些字段直接影响前台展示和后台排序筛选。内容分离content字段是存储原始的 Markdown/富文本还是同时存储渲染后的 HTML这取决于你的渲染策略是在后端还是前端。一个经过初步思考的Post实体类设计思路以 Java 为例可能包含以下核心字段// 这是一个概念模型并非完整代码 public class Post { private Long id; private String title; private String summary; // 摘要用于列表页展示 private String content; // 原始内容如 Markdown private String htmlContent; // 渲染后的 HTML可选 private PostStatus status; // 枚举DRAFT, PUBLISHED, DELETED private Boolean top; // 是否置顶 private Boolean allowComment; private Integer viewCount; private Long categoryId; private Long authorId; private LocalDateTime createTime; private LocalDateTime updateTime; // 关联对象非数据库字段用于查询展示 private Category category; private User author; private ListTag tags; }为什么先设计模型因为数据模型是业务的基石。它决定了后端 API 的设计、前端组件的结构甚至影响用户体验。比如有了summary字段列表页就不需要截取content性能更好且更可控。1.2 前后端分离下的 API 设计契约在前后端分离的架构中后端提供 API前端消费 API。API 就是双方的契约。设计 API 时要避免几个常见坑避免过度封装不要为了“省事”而设计一个“万能”接口比如/api/post/saveOrUpdate根据有无 ID 判断是新增还是修改。这会让接口职责不清晰且不利于前端状态管理。更清晰的做法是POST /api/posts用于创建PUT /api/posts/{id}用于更新。统一响应格式建立一个通用的响应包装类。这不仅能规范返回数据还能统一处理异常。// 后端响应包装类示例 Data public class RT { private Integer code; // 状态码如 200成功500错误 private String msg; // 提示信息 private T data; // 业务数据 public static T RT ok(T data) { RT r new R(); r.setCode(200); r.setMsg(success); r.setData(data); return r; } // 可以补充 error, fail 等方法 }分页查询标准化列表接口几乎都需要分页。设计一个通用的分页请求参数和分页结果对象。// 分页请求参数 Data public class PageParam { private Integer pageNum 1; private Integer pageSize 10; private String orderBy; // 排序字段 // 可以扩展其他查询条件 } // 分页结果 Data public class PageResultT { private Long total; // 总记录数 private ListT list; // 当前页数据列表 }使用 RESTful 风格尽量使用 HTTP 方法GET, POST, PUT, DELETE和资源路径来表达操作意图让 API 更直观。例如GET /api/posts- 获取文章列表GET /api/posts/{id}- 获取单篇文章POST /api/posts- 创建文章PUT /api/posts/{id}- 更新文章DELETE /api/posts/{id}- 删除文章这一步的价值在于在编码之前你和你的前端伙伴或者未来的自己就已经对“数据如何交换”达成了共识。这能极大减少联调时的沟通成本。2. 后端构建Spring Boot 不只是自动配置使用 Spring Initializr 创建项目很简单勾选 Web、MyBatis、MySQL 等依赖即可。但让项目结构清晰、易于扩展需要一些额外的设计。2.1 项目分层与包结构清晰的职责边界避免所有类都堆在同一个包下。一个推荐的分层包结构如下src/main/java/com/yourdomain/blog ├── config // 配置类数据源、拦截器、WebMvc等 ├── controller // 控制器层接收请求调用Service返回结果 ├── service // 业务逻辑层接口 │ └── impl // 业务逻辑层实现 ├── mapper // 数据访问层MyBatis Mapper接口 ├── model // 数据模型 │ ├── entity // 实体类对应数据库表 │ ├── dto // 数据传输对象用于API入参、出参 │ └── vo // 视图对象用于前端展示可能组合多个实体 ├── common // 通用工具、常量、异常定义、统一响应等 └── BlogApplication.java // 启动类为什么分层每一层有明确的职责Controller只负责参数校验、请求转发、响应封装。不包含业务逻辑。Service核心业务逻辑所在地。事务管理通常在这一层。Mapper只负责与数据库交互执行最基础的 CRUD。DTO/VO用于在不同层之间传递数据避免实体类直接暴露给前端也方便做字段转换和过滤。2.2 集成 MyBatis-Plus减少重复的 CRUD 代码MyBatis-Plus 是一个强大的 MyBatis 增强工具。对于博客这类标准 CRUD 较多的系统它能极大提升开发效率。依赖引入在pom.xml中添加依赖。配置在application.yml中配置数据源和 MyBatis-Plus 相关属性如逻辑删除字段、分页插件等。mybatis-plus: global-config: db-config: logic-delete-field: deleted # 全局逻辑删除字段名 logic-delete-value: 1 # 逻辑已删除值 logic-not-delete-value: 0 # 逻辑未删除值 configuration: map-underscore-to-camel-case: true # 自动转换下划线到驼峰 # 指定 mapper.xml 位置如果使用 mapper-locations: classpath*:/mapper/**/*.xml使用让实体类继承BaseMapperService 层接口继承IService实现类继承ServiceImpl。这样就能直接使用内置的save,updateById,getById,page等方法。// Mapper 接口 public interface PostMapper extends BaseMapperPost { // 可以在此定义自定义的复杂查询方法 PagePostVO selectPostPage(PagePostVO page, Param(query) PostQueryDTO query); } // Service 接口 public interface PostService extends IServicePost { // 扩展业务方法 PageResultPostVO getPostPage(PageParam pageParam, PostQueryDTO query); PostDetailVO getPostDetailById(Long id); }注意虽然 MyBatis-Plus 很方便但对于复杂的关联查询如文章关联分类、标签你可能仍需编写自定义的 XML 或注解 SQL。不要试图用它的条件构造器解决所有问题合适的工具用在合适的地方。2.3 全局异常处理与统一响应这是保证 API 友好性和健壮性的关键。通过ControllerAdvice或RestControllerAdvice注解创建一个全局异常处理器。RestControllerAdvice public class GlobalExceptionHandler { // 处理业务异常 ExceptionHandler(BusinessException.class) public RString handleBusinessException(BusinessException e) { log.error(业务异常{}, e.getMessage(), e); return R.fail(e.getCode(), e.getMessage()); } // 处理系统异常兜底 ExceptionHandler(Exception.class) public RString handleException(Exception e) { log.error(系统异常, e); return R.error(系统繁忙请稍后再试); } // 可以补充处理参数校验异常Validated、权限异常等 }这样无论在 Controller、Service 还是其他地方抛出异常最终都会以统一的R格式返回给前端前端可以依据code进行统一处理。3. 前端构建Vue 3 的 Composition API 与工程化前端我们选择 Vue 3 和 Vite。Vite 提供了极快的启动和热更新速度非常适合开发。3.1 项目初始化与基础架构使用 Vite 创建项目非常简单npm create vuelatest。在项目初始化时根据提示选择需要的特性如 TypeScript, Router, Pinia。创建完成后重点关注以下目录结构src/ ├── api/ // 所有后端 API 请求封装 ├── assets/ // 静态资源 ├── components/ // 公共组件 ├── composables/ // 组合式函数Vue 3 特色 ├── router/ // 路由配置 ├── stores/ // 状态管理Pinia ├── types/ // TypeScript 类型定义 ├── utils/ // 工具函数 ├── views/ // 页面组件 ├── App.vue └── main.ts关键点在api/目录下按模块组织 API 请求函数。使用axios或fetch进行封装并配置请求拦截器添加 token和响应拦截器处理统一错误。// utils/request.ts - axios 封装示例 import axios from axios; const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量读取 timeout: 10000, }); // 请求拦截器 service.interceptors.request.use( (config) { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }, (error) { return Promise.reject(error); } ); // 响应拦截器 service.interceptors.response.use( (response) { const res response.data; // 假设后端统一返回 { code, msg, data } if (res.code 200) { return res.data; // 直接返回业务数据 } else { // 业务错误统一提示 ElMessage.error(res.msg || Error); return Promise.reject(new Error(res.msg || Error)); } }, (error) { // HTTP 状态码错误如 401, 404, 500 ElMessage.error(error.message || Request Error); return Promise.reject(error); } ); export default service;3.2 状态管理用 Pinia 管理文章列表状态对于博客管理后台文章列表页可能涉及复杂的查询条件分类、标签、状态、关键词和分页。使用 Pinia 来管理这些状态可以让组件逻辑更清晰。定义 Store// stores/post.ts import { defineStore } from pinia; import { getPostPage } from /api/post; import type { PostQuery, PageResult, PostVO } from /types/post; export const usePostStore defineStore(post, { state: () ({ postList: [] as PostVO[], total: 0, queryParams: { pageNum: 1, pageSize: 10, title: , categoryId: undefined, status: undefined, } as PostQuery, loading: false, }), actions: { async fetchPostList() { this.loading true; try { const result: PageResultPostVO await getPostPage(this.queryParams); this.postList result.list; this.total result.total; } catch (error) { console.error(获取文章列表失败, error); } finally { this.loading false; } }, resetQuery() { this.queryParams { pageNum: 1, pageSize: 10, title: , categoryId: undefined, status: undefined, }; }, }, });在组件中使用!-- PostList.vue -- script setup langts import { usePostStore } from /stores/post; import { storeToRefs } from pinia; const postStore usePostStore(); const { postList, total, queryParams, loading } storeToRefs(postStore); // 加载列表 const loadData () { postStore.fetchPostList(); }; // 搜索 const handleSearch () { queryParams.value.pageNum 1; // 搜索时回到第一页 loadData(); }; // 分页变化 const handlePageChange (pageNum: number) { queryParams.value.pageNum pageNum; loadData(); }; onMounted(() { loadData(); }); /script使用 Pinia 的好处将数据获取和状态管理逻辑从组件中抽离使组件更专注于视图渲染和用户交互。同时状态在多个组件间共享和同步变得非常简单。3.3 动态路由与权限控制管理后台通常需要根据用户角色如管理员、编辑动态加载不同的菜单和路由。这可以通过以下步骤实现定义路由元信息在路由配置中使用meta字段标记路由所需的权限。// router/routes.ts const routes [ { path: /post, component: Layout, meta: { title: 文章管理, requiresAuth: true, roles: [admin, editor] }, children: [ { path: list, component: () import(/views/post/PostList.vue), meta: { title: 文章列表 } }, { path: edit/:id?, component: () import(/views/post/PostEdit.vue), meta: { title: 编辑文章 } }, ], }, // ... 其他路由 ];路由守卫在全局路由守卫中检查用户登录状态和权限。// router/index.ts router.beforeEach((to, from, next) { const token localStorage.getItem(token); const userRoles getUserRoles(); // 从 store 或 localStorage 获取用户角色 // 检查是否需要认证 if (to.meta.requiresAuth !token) { next(/login); return; } // 检查角色权限 if (to.meta.roles) { const hasRole to.meta.roles.some(role userRoles.includes(role)); if (!hasRole) { next(/403); // 无权限页面 return; } } next(); });动态生成菜单根据用户权限从完整的路由表中过滤出有权限访问的路由生成侧边栏菜单。4. 前后端联调与部署从开发环境到生产环境当后端 API 和前端页面都开发得差不多时就进入联调阶段。然后是如何让项目在服务器上跑起来。4.1 联调环境配置与跨域问题开发时前端运行在http://localhost:5173后端运行在http://localhost:8080浏览器会因为同源策略阻止请求。解决方法后端解决推荐在 Spring Boot 中配置 CORS。Configuration public class WebConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) // 针对哪些路径 .allowedOrigins(http://localhost:5173) // 允许的前端地址 .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) // 允许携带 cookie 等凭证 .maxAge(3600); } }前端代理Vite在vite.config.ts中配置代理将 API 请求转发到后端。export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, // rewrite: (path) path.replace(/^\/api/, ) // 可选重写路径 }, }, }, });这样前端代码中请求/api/posts实际上会被转发到http://localhost:8080/api/posts。4.2 部署两种常见模式模式一前后端分离部署推荐前端运行npm run build生成静态文件dist目录将其部署到 Nginx 或对象存储如阿里云 OSS上。后端打包成可执行的 JAR 文件mvn clean package在服务器上通过java -jar blog-backend.jar运行。需要配置好生产环境的数据库连接等信息通过application-prod.yml。Nginx 配置关键点需要将前端路由如/,/post,/about指向index.html同时将/api开头的请求代理到后端服务。server { listen 80; server_name yourdomain.com; location / { root /path/to/your/frontend/dist; try_files $uri $uri/ /index.html; # 支持前端路由 } location /api/ { proxy_pass http://localhost:8080; # 后端服务地址 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }模式二前后端打包在一起将前端构建出的静态文件dist目录下的所有文件复制到 Spring Boot 项目的src/main/resources/static目录下。然后打包后端。这样后端服务同时提供 API 和静态页面。优点部署简单一个 JAR 包搞定。缺点前后端耦合任何前端改动都需要重新打包整个后端项目不适合频繁迭代的前端。4.3 数据库配置与基础数据初始化在生产环境数据库配置通常通过环境变量或外部配置文件指定而不是硬编码在application.yml中。# application-prod.yml spring: datasource: url: ${DB_URL:jdbc:mysql://localhost:3306/blog_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai} username: ${DB_USERNAME:root} password: ${DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver启动时可以通过java -jar blog.jar --spring.profiles.activeprod指定使用生产配置。此外项目第一次部署时需要初始化数据库表结构和基础数据如管理员账号。这可以通过以下方式实现Flyway 或 Liquibase数据库版本管理工具通过 SQL 脚本管理表结构的创建和变更。手动执行 SQL 脚本在项目根目录或资源目录下提供schema.sql和data.sqlSpring Boot 启动时会自动执行需配置spring.sql.init.modealways生产环境慎用。5. 超越功能把项目变成可复用的经验一个博客项目做完如果只停留在“它能跑”那价值就大打折扣了。真正的收获是把这次实践中的模式、决策和踩过的坑沉淀成未来可以复用的经验。5.1 代码生成器的思考很多教程会教你用若依RuoYi这类框架的代码生成器。它能快速生成 Controller、Service、Mapper、Vue 页面等代码对于标准增删改查CRUD功能效率极高。但是你需要明白它生成的是“模板代码”适用于简单的单表操作。一旦业务逻辑复杂涉及多表关联、复杂校验、特定工作流生成的代码往往需要大量修改。它是一把双刃剑对于新手能快速看到成果建立信心。但过度依赖会让你失去深入理解每一层代码如何协作的机会。我的建议第一次做可以手动敲一遍。在敲的过程中你会遇到各种问题如字段类型映射、JSON 序列化、日期处理、事务注解等解决这些问题的过程就是学习。第二次、第三次再做类似功能时再考虑使用代码生成器来提升效率此时你也有能力去理解和定制生成的代码。5.2 从“项目做完”到“项目可用”一个可用的项目除了核心功能还需要考虑很多“非功能性”需求日志使用 SLF4J Logback合理设置日志级别记录关键操作和异常信息便于排查问题。接口文档使用 Swagger/OpenAPI 自动生成 API 文档。在 Controller 上添加Operation、Parameter等注解让前端和后端自己都能随时查阅。参数校验在 Controller 的入参 DTO 上使用Validated注解和NotBlank、Size等校验注解避免无效数据进入业务层。安全性密码加密存储用户密码时务必使用 BCrypt 等强哈希算法切勿明文存储。SQL 注入防护使用 MyBatis-Plus 的条件构造器或#{}预编译语句可有效防止。XSS 防护对用户输入的富文本内容如文章内容进行适当的过滤或转义或者在前端渲染时使用安全的渲染方式。性能数据库索引为常用的查询字段如category_id,status,create_time建立索引。缓存对于不常变的数据如文章分类、热门文章列表可以考虑使用 Redis 进行缓存。前端优化组件懒加载、图片懒加载、API 请求合并等。5.3 下一步可以做什么如果你已经完成了基础版本可以尝试以下方向深化理解全文搜索集成 Elasticsearch为博客文章提供更强大的搜索功能。评论系统设计一个更完善的评论模型支持楼中楼回复、审核、通知等。文件上传集成阿里云 OSS 或 MinIO实现图片、附件上传和管理。任务调度使用 Spring 的Scheduled或 Quartz定时执行任务如清理临时文件、生成站点地图等。容器化部署编写 Dockerfile将前后端分别容器化使用 Docker Compose 编排体验更现代的部署方式。编写单元测试为 Service 层的关键业务方法编写单元测试使用 JUnit Mockito保证代码质量。回过头看一个 Spring Boot Vue 3 的博客项目与其说是一个技术栈的练习不如说是一个完整的软件开发生命周期微缩模型。你经历了需求分析博客需要什么、设计数据模型、API、实现编码、调试、部署甚至后续的优化思考。把这个过程中的每一个决策点——为什么这样分层、为什么选这个组件、如何解决跨域、如何管理状态——都想清楚并形成自己的 checklist 和方法论这才是“从零带做”这个过程中比代码本身更重要的收获。下次当你再启动一个新项目时你脑子里浮现的将不再是一堆零散的技术名词而是一套清晰、可执行的构建流程。 30款热门AI模型一站整合DeepSeek/GLM/Qwen 随心用限时 5 折。 点击领海量免费额度