Vue+SpringBoot个人博客项目实战:从零搭建到部署全流程 📅 发布时间:2026/9/1 23:40:19 👁 浏览次数: 简介这是一份面向Web全栈初学者与进阶开发者的VueSpringBoot个人博客项目源码旨在帮助开发者掌握前后端分离架构下的完整开发流程与工程实践。资源共537个文件涵盖98个Java后端业务与配置类、202个Vue组件及逻辑JS、107个HTML模板页、60个CSS样式文件以及PNG、SVG等静态资源整体压缩包仅4.34MB轻量易部署。已有54人下载学习适合用于课程设计、技术面试准备或个人作品集搭建。项目结构清晰包含完整的RESTful API接口、基于Vue Router的前端路由体系、Vuex状态管理、axios请求封装以及Spring Security权限控制、JPA数据持久化等核心模块预览可见nginx.conf配置、editormd富文本编辑器集成、多种CSS主题如ambiance、typo和CodeMirror代码高亮支持体现了真实博客系统的功能完整性与工程规范性。 去年整理移动硬盘的时候我翻出一个名叫“一个基于VueSpringBoot的个人博客项目.zip”的压缩包。那是三年前从零开始写的第一个前后端分离项目压缩包体积不大但里面几乎装下了当时我对完整Web应用的全部理解。后来很多朋友问我个人博客这么简单的东西用WordPress不香吗为什么非要自己造轮子这里先说结论个人博客确实不难但VueSpringBoot的个人博客项目从来不是只为了写博客而是用最低的成本把前端工程化、后端接口设计、数据库建模、权限认证、跨域联调、部署上线这一整条链路亲手打通一遍。Vue负责页面交互和路由控制SpringBoot负责接口服务和业务逻辑两者组合起来几乎覆盖了企业级开发中最常见的基础场景。这篇文章就基于这个项目把从选型、建模、编码到部署的完整过程拆开讲一遍包括我在实战中踩过的坑和后来总结的优化思路适合正在学Vue或SpringBoot的朋友拿来当实战参考也适合准备跳槽的工程师重新梳理自己的技术栈。1. 为什么个人博客值得折腾一套前后端分离1.1 博客只是壳技术栈才是核心价值很多初学者有个误区觉得个人博客功能很少——无非是发文章、看列表、点详情加个标签和评论也就到头了。但如果把博客看作一个业务系统来设计你会发现它麻雀虽小五脏俱全文章管理涉及CRUD和分页标签分类涉及多对多关系用户登录涉及Token鉴权富文本内容涉及XSS安全过滤图片视频涉及文件上传后续还可以扩展全文搜索、点赞收藏、定时发布。这些功能点正好对应了后端开发中最高频的几类需求。我见过不少简历上写着熟悉SpringBoot全家桶的候选人但问起JWT认证流程、拦截器注册顺序、跨域怎么解决、如何做XSS过滤回答往往停在用过、知道有这东西的层面。用VueSpringBoot从零手写一个博客本质上就是把简历上一行行技术名词变成真正跑通的代码这是看教程和做项目之间最本质的区别。1.2 技术选型对比为什么是VueSpringBoot而不是别的做个人博客常见的技术路线其实有好几条我列出当初对比过的方案方案优点缺点适合人群WordPress 主题省事功能全有现成生态二次开发受限难以学到核心知识单纯想快速有博客的人SpringBoot Thymeleaf单应用部署简单服务端渲染SEO好前后端耦合前端体验受限偏后端、不想折腾前端的开发者SpringBoot Vue前后端分离职责清晰贴合现代团队开发模式联调、跨域、部署都要自己处理想系统掌握全栈能力的人Node.js React/Vue全栈都用JS语言统一后端生态和招聘需求不如Java广泛前端转全栈的人我最终选了VueSpringBoot一个很现实的原因是在国内的招聘市场上这个组合的需求量最大。另一个原因是技术成熟度——SpringBoot的自动配置让后端开发效率极高Vue的响应式系统和组件化设计让前端代码维护成本很低两者解耦后我甚至可以单独把前端换成H5页面或者小程序不用动后端一行代码。这种前后端可以独立演进的能力正是企业级开发中非常看重的。1.3 这套组合能覆盖的学习地图实际做下来这个项目帮我打通的知识点包括Vue3的组合式API、Vue Router的路由守卫与参数传递、Pinia状态管理、axios封装与拦截器、Element Plus组件库的使用、SpringBoot的自动配置原理、MyBatis-Plus数据持久化、JWT无状态认证、过滤器与拦截器的区别、统一异常处理、跨域CORS机制、Nginx反向代理、Docker容器化部署。这些点随便抽出一个都是面试常客但只有全部串联在一个真实项目里你才会理解它们各自解决什么痛点。另外提一句做这个项目时SpringBoot已经出到了3.x版本我建议选3.2或3.3这种相对成熟的版本不要盲目追新。网上很多教程还停留在2.x从javax到jakarta包名的迁移对新手来说是完全没有必要踩的坑后面部署时我也会给出稳妥的版本选择。2. 项目骨架从零搭建Vue3 SpringBoot3的工程结构与数据模型2.1 前端工程初始化Vite还是Vue CLI三年前我创建前端项目时用的还是Vue CLI脚手架慢、依赖多启动要等很久。现在新项目我统一推荐Vite启动速度快了一个量级开发体验好很多。创建命令很简单npm create vitelatest blog-front -- --template vue cd blog-front npm install项目生成后按照我们的技术规划安装依赖npm install vue-router4 pinia axios element-plus npm install hls.js marked # 视频播放和Markdown渲染目录结构我习惯这样划分src/ |-- api/ # 按模块拆分的接口请求 |-- assets/ # 静态资源 |-- components/ # 通用组件 |-- router/ # 路由配置 |-- store/ # Pinia状态模块 |-- views/ # 页面组件 |-- utils/ # 工具函数 |-- App.vue |-- main.js2.2 后端工程初始化与依赖清单后端我是在Spring Initializr上生成的JDK版本选17SpringBoot版本选了3.2.x。核心依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.12.5/version /dependency这里我要特别强调一个教训如果你的SpringBoot版本高于3.2mybatis-plus可能还没有对应适配版本启动时会报奇怪的类加载错误。这是很多人在SpringBoot版本太高后遇到问题的根源。我的建议是做项目前先去Maven中央仓库查一下关键第三方依赖对SpringBoot版本的兼容情况再决定选哪个版本。稳定压到一切不要为了一两个新特性付出几天的排错时间。后端目录结构com.example.blog |-- controller/ # 接口层 |-- service/ # 业务逻辑 |-- mapper/ # 数据持久层 |-- entity/ # 实体类 |-- dto/ # 请求响应对象 |-- config/ # 配置类跨域、拦截器、MyBatisPlus |-- common/ # 统一返回、异常处理、常量 |-- filter/ # 自定义过滤器2.3 数据库设计五张核心表怎么划分个人博客的业务场景不需要过度设计但也要保证表结构合理能覆盖正常的用户、内容、分类标签、评论体系。我最终落地的表结构如下CREATE TABLE t_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码BCrypt加密, nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, role varchar(20) DEFAULT USER COMMENT 角色, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE t_article ( id bigint NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL COMMENT 标题, summary varchar(500) DEFAULT NULL COMMENT 摘要, content longtext COMMENT 正文Markdown或富文本, cover varchar(255) DEFAULT NULL COMMENT 封面图, category_id bigint DEFAULT NULL COMMENT 分类ID, view_count int DEFAULT 0 COMMENT 浏览量, status tinyint DEFAULT 1 COMMENT 1-发布 0-草稿, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT文章表;标签是个经典的多对多关系所以需要三张表t_tag、t_article_tag关联表、以及t_category分类表。评论表相对独立一对多挂在文章下面。这套结构稍加扩展就能支撑一个中型内容系统的数据层。关于content字段我一开始用的是longtext存富文本HTML后来切换到Markdown源文本存储前端用marked渲染。这样做的好处很明显长文体积更小样式由前端控制也避免了直接存HTML带来的XSS注入风险面。2.4 多环境配置与统一响应结构项目切环境是基本功。我在application.yml里只保留公共配置分环境的配置放到application-dev.yml和application-prod.yml# application-dev.yml server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/blog?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456后端接口的返回格式必须统一不然前端处理起来会非常痛苦。我定义了一个ResultTpublic class ResultT { private int code; // 200成功非200失败 private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(int code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }同时配合RestControllerAdvice做全局异常处理业务异常统一抛BizException校验失败、参数错误、权限不足都返回规范的错误码而不是把堆栈直接甩给前端。3. SpringBoot后端的功能落点文章管理、鉴权与安全防护3.1 文章接口的设计思路从查询到缓存博客核心接口就是文章的列表和详情。列表接口需要分页条件包括分类、标签、关键词详情接口需要把正文返回同时浏览量加一。MyBatis-Plus的分页插件配置好之后列表接口写起来非常简洁GetMapping(/article/page) public ResultIPageArticleVO page(RequestParam(defaultValue 1) long current, RequestParam(defaultValue 10) long size, RequestParam(required false) String keyword) { PageArticle page new Page(current, size); LambdaQueryWrapperArticle wrapper Wrappers.lambdaQuery(); wrapper.like(StringUtils.hasText(keyword), Article::getTitle, keyword) .eq(Article::getStatus, 1) .orderByDesc(Article::getCreateTime); IPageArticle articlePage articleService.page(page, wrapper); // 转VO脱敏不把content返回给列表接口 return Result.success(convertToVO(articlePage)); }有一个细节很多人忽略列表接口和详情接口返回的字段应该是不同的。列表只需要标题、摘要、封面、发布时间正文和浏览量应该只在详情页返回。如果图省事直接把整个实体返回列表页会多传输很多无用数据也会让前端更不好维护。文章详情页的浏览量高并发下直接update数据库会有一定压力生产环境我建议用Redis做计数缓冲定时批量写回。博客项目流量不大时可以不处理但如果运行一段时间感觉接口响应变慢优先排查点就是这种高频小事务。3.2 JWT登录鉴权过滤器链上的一段自定义逻辑用户登录注册是这个项目里最典型的功能。密码用BCrypt加密存储登录成功后签发JWT前端拿到Token存在localStorage里后续请求在header里带上Authorization: Bearer xxx。JWT的生成用jjwt库核心逻辑public String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 3600_000L)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }服务端验证Token的职责放在一个HandlerInterceptor里在白名单之外的所有接口上生效public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod false) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { try { Claims claims Jwts.parser() .setSigningKey(SECRET_KEY) .build() .parseClaimsJws(token.replace(Bearer , )) .getBody(); Long userId Long.valueOf(claims.getSubject()); UserContext.set(userId); // ThreadLocal保存当前登录用户 return true; } catch (Exception e) { throw new BizException(401, 登录已过期); } } throw new BizException(401, 未登录); } }UserContext用ThreadLocal存当前请求的用户信息请求结束后要记得清空避免线程池复用导致串数据。这一点很容易被新手忽略。拦截器注册到WebMvcConfigurer里同时把登录注册、文章列表这些不用鉴权的接口加到排除清单。3.3 富文本XSS过滤从源头拦截脚本注入这是博客项目里最容易踩雷、也最体现功底的一块。搜索关键词里有一组我特别关注的热词springboot解决pdf xss攻击。说明很多人都在接口安全上栽过跟头。博客文章是富文本内容用户可能直接粘贴带script标签的HTML或者故意构造img srcx onerroralert(1)之类的内容。在SpringBoot后端最基础的做法是写一个XssFilter继承OncePerRequestFilter对请求参数做过滤。但这里有个坑如果使用过滤器把所有HTML标签都过滤掉那Markdown编辑器里的代码块、图片、链接全都会被误伤。所以必须用白名单过滤而不是黑名单过滤。我推荐用jsoup做白名单清洗public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest (HttpServletRequest) request; XssWrapper xssWrapper new XssWrapper(httpRequest); chain.doFilter(xssWrapper, response); } } public class XssWrapper extends HttpServletRequestWrapper { private final Safelist safelist Safelist.relaxed() .addTags(img, video, source, h1, h2, pre) .addAttributes(img, src, alt, width, height) .addAttributes(video, src, controls, width, height); Override public String getParameter(String name) { String value super.getParameter(name); return clean(value); } private String clean(String value) { if (value ! null) { return Jsoup.clean(value, safelist); } return null; } }这里的关键是Safelist.relaxed()允许p、a、img、span等常见排版标签但去掉了script、iframe、onclick、onerror等危险属性和事件处理器。这样既保证了文章排版正常又从源头掐死了存储型XSS。后续我还有一个升级思路对Markdown源文本做白名单校验渲染层再做一遍安全过滤双重保险。在个人博客上做到这一步已经能挡住绝大多数攻击了。3.4 文件与视频资源的上传处理博客里有封面图和视频的需求前端上传走的是/api/upload接口。本地开发时我直接把文件存到服务器的/data/blog/upload/目录然后配置静态资源映射Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file: uploadPath); } }生产环境如果有条件应该使用对象存储服务保存资源然后把上传地址改造成预签名上传或服务端转发。这个改造点在博客项目里属于加分项但对理解文件存储和业务服务分离很有帮助。我在部署章节还会提到使用Nginx直接托管静态资源比让SpringBoot处理大文件更合理。4. Vue前端的关键实现路由、状态管理与媒体播放4.1 路由设计路由守卫、动态标题和参数传递前端路由我用Vue Router 4。个人博客的页面虽然不多但路由设计要考虑到两种访问模式公开的访问者可以浏览文章列表和详情登录用户可以在后台发布文章、管理内容。路由配置示例const routes [ { path: /, name: Home, component: () import(/views/Home.vue) }, { path: /article/:id, name: ArticleDetail, component: () import(/views/ArticleDetail.vue), props: true }, { path: /login, name: Login, component: () import(/views/Login.vue) }, { path: /admin, component: () import(/layout/AdminLayout.vue), meta: { requiresAuth: true }, children: [ { path: write, name: ArticleWrite, component: () import(/views/admin/ArticleWrite.vue) }, { path: list, name: ArticleManage, component: () import(/views/admin/ArticleManage.vue) } ] } ]路由守卫这边除了最常见的没有token跳转登录页我强烈建议在afterEach里同步修改页面的document.title否则浏览器标签页永远固定显示一个博客名经验感会差很多。关于参数传递有个坑要记一下/article/:id这种用params传参的方式刷新页面后参数依然在URL里所以是可靠的做法。但如果是router.push({ name: ArticleDetail, params: { id: 1 } })却不定义路径占位符刷新后参数会丢失这个坑很多新手都会踩。4.2 状态管理需求不大选Pinia而不是Vuex博客项目的全局状态其实不多主要就是用户信息、登录状态、以及文章列表的筛选条件缓存。Vuex的写法太啰嗦mutations、actions、getters每一步都要定义一坨样板代码。Vue3时代直接上Pinia写法接近setup函数简洁太多import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: null }), getters: { isLoggedIn: (state) !!state.token }, actions: { setToken(token) { this.token token localStorage.setItem(token, token) }, logout() { this.token this.userInfo null localStorage.removeItem(token) } } })可能有人会问不用Pinia行不行项目里只有一个用户状态完全可以靠localStorage加一个响应式对象实现。但我不建议你这么做因为Pinia不仅提供状态存储还提供了规范的修改路径和组件间共享机制以后项目复杂起来不用重构。4.3 axios请求封装与登录状态联动axios如果不封装每个页面都要写一遍baseURL、请求头、错误处理代码会非常散。我通常在utils/request.js里创建一个实例import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message) if (res.code 401) { router.push(/login) } return Promise.reject(new Error(res.message)) } return res.data }, error { ElMessage.error(网络请求异常) return Promise.reject(error) } )这里有两个细节值得注意一是baseURL设置成/api开发环境由Vite代理转发到后端生产环境由Nginx反向代理这样前端代码里不出现后端地址部署时灵活度最高二是响应拦截器直接返回res.data页面里拿到的就是业务数据类型不需要层层解包代码会更整洁。4.4 在博客里用hls.js播放m3u8视频搜索热词里有一组vue播放m3u8。如果你要在博客里嵌视频HLSHTTP Live Streaming格式非常常见因为它在网络波动时的自适应码率体验最好。但新版浏览器默认不直接支持HLS流的播放需要引入hls.js。我的做法是写一个视频播放组件template video refvideoEl controls classvideo-player/video /template script setup import { ref, onMounted, watch } from vue import Hls from hls.js const props defineProps({ src: { type: String, required: true } }) const videoEl ref(null) function playVideo() { if (!videoEl.value) return // 原生支持则直接播放 if (videoEl.value.canPlayType(application/vnd.apple.mpegurl)) { videoEl.value.src props.src return } if (Hls.isSupported()) { const hls new Hls() hls.loadSource(props.src) hls.attachMedia(videoEl.value) } } onMounted(playVideo) watch(() props.src, playVideo) /script需要注意hls.js的初始化要等video元素挂载完成后进行而且如果src是动态变化的必须销毁之前的hls实例再重建否则会出现播放器黑屏或串流的问题。我常在组件卸载时手动调用hls.destroy()避免内存泄漏。5. 前后端联调阶段最容易踩的坑与排查链路5.1 跨域问题的三种解法与选择前端跑在localhost:5173后端跑在localhost:8080第一道拦路虎就是跨域。浏览器安全策略会拦截跨源请求这里有三种常规解法第一种是后端加CORS配置用CrossOrigin注解或者CorsFilter。优点是前端不用做任何事缺点是生产环境如果前端域名和多后端服务之间需要精细控制配置会比较繁琐而且等于把跨域策略的开关交给了每个后端服务。第二种是开发环境用Vite代理// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })第三种是生产环境用Nginx反向代理让前后端在同一个域名下location /api/ { proxy_pass http://localhost:8080/; }我的最终方案是开发环境用Vite代理生产环境用Nginx代理后端不写任何CORS配置。这样前后端代码里都看不到跨域相关的脏东西域名和端口的事全部交给网关层解决最干净。5.2 node_modules和依赖环境问题的完整排查链路联调阶段有个高频报错让我印象特别深新拉下来的项目执行npm run dev终端直接报The project can not fond node_modules, you can: 1. use npm install -g vue/cli。很多人第一反应是重新安装Vue CLI实际上根本不是这个问题。这个报错的根因是项目依赖根本不完整node_modules目录缺失或损坏。我的排查顺序固定如下先检查node_modules是否存在不完整直接删掉重装rm -rf node_modules package-lock.json npm install。确认Node版本是否匹配。Vite 5要求Node 18如果你机器上还是Node 14装完依赖也会启动报错。检查npm源是否通畅必要时切换到国内镜像源。查看package.json里是否有本地文件路径依赖有些人的项目里dependencies会写file:../xx这种换一台机器就废。记住前端依赖报错第一步永远是清理后重装而不是在网上搜某个具体错误信息然后打补丁重装能解决百分之八十的环境问题。5.3 接口联调时的黑盒排错法前后端分离后联调阶段的报错往往是一个接口前端拿不到数据。新手容易慌我自己的排错方法分四层每层都有明确证据链第一层看浏览器的Network面板。确认请求是否发出、URL是否正确、状态码是多少。如果请求根本没发出多半是前端拦截器或路由问题如果状态码404先看后端有没有这个接口再确认路径拼接是否正确。第二层看后端日志。SpringBoot的启动日志和请求日志会打印SQL语句和异常堆栈。404可能是路径不对500就要去翻异常定位到具体哪一行代码。第三层加细粒度日志。在关键接口的入口和出口各加一行log.info打印参数和返回值。这个方法土但非常有效能快速确定问题在逻辑层还是在数据层。第四层查SQL。MyBatis-Plus默认打印SQL开启logging.level.com.example.blog.mapperdebug后能看到执行的SQL语句参数和结果一目了然。如果是SQL语法问题、字段映射问题在这层就能发现。这套方法我用了很多年从没失手过。5.4 两个高频面试关联问题computed和自动配置原理项目做完之后我把自己学到的东西整理成了面试答案其中有两个问题几乎每次面试都会被问到而且都和这个项目直接相关。第一个是Vue的computed和watch区别。computed是有缓存的计算属性只有当它依赖的响应式数据发生变化时才会重新计算适合做从已有状态派生新状态的场景比如文章列表按标签过滤watch是监听器适合在数据变化时执行副作用操作比如搜索框输入变化后重新请求接口。在博客项目里文章搜索我建议用watch监听keyword变化并调用接口标签过滤则用computed在纯前端完成正好能体现两者的区别。第二个是SpringBoot的自动配置原理。简单版本SpringBoot通过EnableAutoConfiguration导入AutoConfiguration.imports文件里声明的所有配置类然后由配置类上的条件注解如ConditionalOnClass、ConditionalOnMissingBean决定哪些配置生效。比如Classpath里有DataSource类同时你引入了mybatis-plus-boot-starterSpringBoot就会自动配置MyBatis-Plus的SqlSessionFactory。面试官如果再追问哪里看到的我会直接说出这个项目里JWT配置类上没有加条件注解、导致启动后自动注册的全过程。有过真实调试经验的人回答出来的可信度完全不一样。6. 部署上线从本地打包到Docker一键起服务6.1 前端构建与Nginx配置要点前端代码写完执行npm run build生成dist目录静态文件交给Nginx托管。Nginx配置我坚持写两份一份是普通部署的nginx.conf一份是Docker镜像里内置的。server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; # 处理Vue Router history模式刷新404 location / { try_files $uri $uri/ /index.html; } # 反向代理后端接口 location /api/ { proxy_pass http://backend:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态上传资源 location /upload/ { proxy_pass http://backend:8080/upload/; } }try_files $uri $uri/ /index.html这一行是history模式路由必须配置的否则浏览器路由一刷新就404这是个人博客部署中最常见的坑。6.2 后端打包与启动脚本SpringBoot项目打包用Mavenmvn clean package -DskipTests生成blog.jar后单机部署最简单的启动命令java -jar blog.jar --spring.profiles.activeprod生产环境我不建议nohup java -jar裸跑推荐加一个启动脚本管理进程至少也要做开机自启。好在这个项目用Docker部署省掉了进程管理的麻烦。6.3 Docker Compose编排nginx app mysqlDocker部署是我在这个项目里觉得收获最大的一部分。我写了三个Dockerfile加一个docker-compose.yml实现了一条命令起全部服务。后端镜像的DockerfileFROM maven:3.9-eclipse-temurin-17 AS build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests FROM eclipse-temurin:17-jre COPY --frombuild target/blog.jar /app/blog.jar WORKDIR /app EXPOSE 8080 ENTRYPOINT [java, -jar, blog.jar, --spring.profiles.activeprod]前端镜像的DockerfileFROM node:18-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build FROM nginx:alpine COPY --frombuild /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80然后docker-compose一把梭version: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: blog volumes: - mysql-data:/var/lib/mysql restart: always backend: build: ./backend depends_on: - mysql environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/blog restart: always frontend: build: ./frontend ports: - 80:80 depends_on: - backend restart: always这里的关键点是后端配置的数据库地址写的是服务名mysql而不是localhost因为容器之间是通过Docker内部网络通信的。这个细节不搞清楚部署阶段会一直报数据库连接失败。6.4 上线后的监控与备份习惯博客上线后可以不用像企业系统那么重但数据是无价的。我养成了两个习惯也建议你参考第一是MySQL自动备份。写一个简单的crontab任务每天凌晨用mysqldump把数据库导出保留最近7天的备份文件0 2 * * * mysqldump -u root -ppassword blog /backup/blog_$(date \%Y\%m\%d).sql find /backup -name *.sql -mtime 7 -delete第二是关注日志空间。SpringBoot默认把所有输出打到控制台Docker部署时如果不做日志轮转几个月后宿主机磁盘会被撑爆。我在docker-compose里特意加上了日志限制或者用logrotate定期清理这个成本很低但能避免大事故。部署完成后这个基于VueSpringBoot的个人博客项目才真正意义上闭环了。我后来又在它的基础上扩展过一个后台评论管理功能前后端各加了几个接口和页面整个改动用时不到半天全程没有破坏原有功能这让我切实体会到良好工程结构的力量。如果你也想拿这个项目练手建议按我上面的顺序走一遍不要跳步尤其不要跳过部署部分——本地能跑和上线能跑完全是两码事。本文还有配套的精品资源点击获取