简介:这是一套面向Web全栈初学者与进阶开发者的Vue+SpringBoot个人博客项目源码,聚焦前后端分离架构实践,帮助开发者系统掌握现代Java Web与前端工程化开发流程。资源共537个文件,涵盖98个Java后端业务与配置类、202个Vue组件及逻辑JS、107个HTML模板页、60个CSS样式文件,以及SVG/ICO等静态资源,整体压缩包仅4.34MB,轻量易读。已有54人学习下载,适合用于课程设计、技术面试准备或个人作品集搭建。项目结构规范,含完整RESTful API接口、Vuex状态管理、Vue Router路由控制、axios请求封装、Spring Security权限校验及JPA数据访问层;预览可见nginx.conf部署配置、editormd富文本编辑器集成、多种CSS主题(如ambiance.css、typo.css)和动画支持,体现真实博客系统的功能完整性与工程可维护性。 前段时间整理硬盘,翻出一个名为“一个基于Vue+SpringBoot的个人博客项目.zip”的压缩包,解压后看着里面熟悉的前后端目录,突然觉得这种“看起来人人都会做、但真正做完整并不容易”的练手项目,其实特别适合写一篇完整的复盘。
个人博客在技术上不算新鲜,但它的覆盖面非常广:前端要处理路由、状态管理、列表渲染、交互体验;后端要处理鉴权、分页、文件上传、数据库设计;再加上前后端联调和部署,整个流程走完,基本能摸清一个商业项目的完整链路。如果你正在学Vue和SpringBoot,想做毕设,或者想给简历加一个拿得出手的实战项目,这个项目模板都能直接拿来用。
这篇文章我不会只贴代码,而是会把你从零到一搭这个项目时最容易卡住的环节,比如版本选择、目录结构、JWT鉴权、跨域代理、Docker部署、各种诡异报错,全部摊开讲清楚。每个选择背后都会解释为什么这么做,踩过的坑也会单独整理成速查表。内容比较多,建议先收藏再慢慢看。
1. 动手前先把技术选型和模块结构定明白
1.1 为什么是Vue加SpringBoot,而不是其他组合
很多新手一上来就问“用什么框架好”,其实个人博客这种体量的项目,核心诉求是快速落地、结构清晰、方便扩展,Vue加SpringBoot恰好在这三点上都很稳。
Vue的优势在于渐进式,你不需要一开始就引入全家桶,可以先只用vue-router做页面跳转,等需要全局共享用户信息时再加Vuex或Pinia。它的响应式机制和组件化写法对个人开发者非常友好,写一个文章列表页不需要太多模板代码。而SpringBoot的价值在于“约定大于配置”,内置Tomcat、自动配置数据源、依赖启动器一拉就齐,不需要像传统SSM那样写一堆XML。个人博客的接口数量不多,但涉及文章、分类、标签、评论、用户等多个资源,SpringBoot的分层结构恰好能把职责理清楚。
还有一个现实原因:招聘市场上Vue和SpringBoot的需求量大,面试常考,做这个项目的过程本身就是在帮自己过一遍高频考点,这点后文提到的一些细节里会体现。
1.2 项目功能模块与页面结构梳理
拿到标题里的zip之前,先别急着写代码,把功能模块盘清楚。我做的这个项目把功能拆成了三块:
- 前台展示:文章列表、文章详情、分类筛选、标签聚合、关于页面、站内搜索。
- 后台管理:管理员登录、文章发布与编辑、分类和标签维护、个人资料设置。
- 公共支撑:图片上传、JWT鉴权拦截、全局异常处理、日志记录。
前台是门户,追求的是加载速度和阅读体验;后台是工具,追求的是操作效率;公共支撑是底子,决定项目安不安全、稳不稳定。这种拆法还有一个好处,就是前后端接口可以直接按模块划分,比如前台接口统一用/blog前缀,后台接口统一用/admin前缀,后文鉴权拦截器就靠这个前缀区分哪些接口需要登录。
1.3 技术栈清单与版本选择建议
版本问题是我最想先说的,因为我在这个项目上被“版本太高”坑过两次。先交代一下当时用的完整技术栈:
- 前端:Vue 2.6(用vue-cli 4构建,没用Vite,主要考虑当时项目脚手架稳定)、vue-router 3、Vuex 3、axios、element-ui、marked(渲染Markdown)、highlight.js(代码高亮)。
- 后端:SpringBoot 2.3.7、JDK 8、MyBatis-Plus 3.4、MySQL 5.7、JWT(jjwt 0.9.1)、Swagger(knife4j)。
- 部署:前端Nginx、Docker Compose编排后端与MySQL。
如果你是现在新建项目,我建议用Vue 3加Vite加Pinia,后端用SpringBoot 2.7或3.x加JDK 17。但有一个原则要记住:不要无脑追最新版本。SpringBoot 3.x要求JDK 17以上,如果你的机器装的是JDK 8,那老老实实用SpringBoot 2.x,否则一启动就报UnsupportedClassVersionError,排查半天才发现是JDK不匹配,纯浪费时间。热词里提到“springboot版本太高”,大概率就是这个原因。
2. 后端SpringBoot核心功能实现
2.1 工程初始化与IDEA建项目时的几个坑
后端工程我推荐用IDEA的Spring Initializr创建,但热词里有人提到“idea新建项目没有springboot 3.4.3选项”,这里解释一下:Spring Initializr页面显示的版本是它默认的联网版本,不是固定不可变的。你可以在创建后直接改pom.xml里的parent版本号,前提是对应版本的依赖都能在中央仓库拉到。
没有合适的版本选项时还容易漏掉一个小地方——IDEA的Server URL默认是start.spring.io,国内网络环境下拉取慢或失败很常见。我一般会先手工在pom.xml里把依赖写好,再让Maven去拉。用阿里云镜像仓库能解决大多数拉包失败问题:
<repositories> <repository> <id>aliyun</id> <url>https://maven.aliyun.com/repository/public</url> </repository> </repositories>pom.xml里的核心依赖就五样:spring-boot-starter-web、spring-boot-starter-validation、mybatis-plus-boot-starter、mysql-connector-java、jjwt。如果要做Swagger,再加上knife4j-spring-boot-starter。代码尽量少用lombok的@Data,虽然省事,但面试里经常被问“你知道@Data的坑吗”,搞清getter/setter和equals/hashCode的生成规则再决定用不用也不迟。
2.2 数据库设计与表关系
个人博客的数据量级不会很大,但表结构一定要设计清楚,不然写到后面会乱。我建的表有这些:
| 表名 | 主要字段 | 说明 |
|---|---|---|
| t_user | id, username, password, nickname, avatar, create_time | 管理员账号,字段不要带plain密码校验逻辑 |
| t_article | id, title, summary, content, cover_image, category_id, create_time, update_time, status | 文章主体,status用于草稿和发布 |
| t_category | id, category_name, description | 分类,一个分类对应多篇文章 |
| t_tag | id, tag_name | 标签,纯列表 |
| t_article_tag | article_id, tag_id | 文章和标签的多对多关联表 |
| t_comment | id, article_id, nickname, content, create_time | 评论,先做游客评论,登录评论属于扩展 |
文章表和分类表用外键逻辑关联,但物理上我没建外键约束。原因很简单:个人博客读写压力不大,外键约束会影响插入删除效率,而且后台删除分类时需要自己写逻辑判断“该分类下是否有文章”,这比数据库抛外键异常更好处理。MyBatis-Plus里,表名的下划线命名会自动映射到实体类的驼峰属性,配合逻辑删除字段还可以做到删除文章后用户端自动不可见。
2.3 文章接口怎么分页、怎么返回统一结构
后端接口设计最忌讳每个接口返回格式不一致。我在项目里封装了一个统一返回体Result,里面包含code、message、data三个字段。code为200表示成功,401表示未登录,500表示服务端异常。前端axios拦截器拿到code后统一处理,不用每个页面重复写错误提示。
文章列表接口是核心中的核心,我用了MyBatis-Plus的Page分页插件,前端传入pageNum和pageSize,后端返回总条数和当前页数据。这里有几个细节值得注意:
- 列表页不需要返回文章正文content,只返回summary即可,减少网络传输量。
- 按分类和标签筛选时,用LambdaQueryWrapper的eq方法拼接条件,避免判断空条件时的尴尬。
- 排序用create_time倒序,同时加一个status等于1的过滤条件,保证草稿不会出现在前台。
public Result getArticlePage(Integer pageNum, Integer pageSize, Long categoryId, Long tagId) { Page<Article> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Article::getStatus, 1) .eq(categoryId != null, Article::getCategoryId, categoryId) .orderByDesc(Article::getCreateTime); articleMapper.selectPage(page, wrapper); return Result.success(page); }先按条件过滤再排序,是MyBatis-Plus最常见的写法。关键是eq方法第一个参数传布尔值,条件不成立时自动忽略这个条件,避免了手写if判断的重复代码。
2.4 登录鉴权与JWT无状态认证
个人博客一般就一个管理员,但鉴权逻辑不能省,因为这是面试必问的点。我用的方案是JWT加拦截器:用户登录成功后,后端生成一个包含userId和username的token返回给前端,前端存到localStorage里,每次请求在请求头加Authorization字段,后端拦截器校验token有效性。
JWT由三部分组成:Header、Payload、Signature。Header指明加密算法,Payload放用户信息,Signature用密钥对前两部分签名。这样做的好处是服务端不需要保存session,天然适合前后端分离,也方便以后扩展成多端登录。
public String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim("userId", userId) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }密码存储我用了MD5加盐,严格来说SHA-256加盐更安全,或者直接用BCrypt,但个人博客的威胁模型有限,MD5加随机盐已经比明文存储强太多。重点是盐不能写死,最好每个用户独立生成一个盐,存储在用户表里。密码校验时先取出盐,再对输入密码加盐哈希比较。
拦截器实现上要注意,注册/登录接口要放行,/admin/**的后台接口必须校验,/blog/**前台接口可以放行。如果是SpringBoot 2.x,自定义拦截器实现HandlerInterceptor,在preHandle里校验token,再通过WebMvcConfigurer注册拦截路径。
2.5 接口文档与Swagger集成
后端接口写完,最好接一个Swagger,省得每次联调都让前端同事来问“这个接口参数叫什么”。我用knife4j,它对Swagger的UI做了增强,界面比原版好看不少,而且在SpringBoot 2.3.7下兼容性稳定。
配置的时候注意几件事:
- 要写Docket的select路径,只扫描Controller包的接口,不要扫到不需要暴露的内部类。
- 开启注解后,Controller方法上要写@ApiOperation标注接口含义,类上写@Api描述模块。不写的话Swagger页面全是英文方法名,等于没配。
- 如果集成了JWT,可以在Swagger里配置全局token参数,方便直接在文档页面调试需要登录的接口。
热词里提到“springboot解决pdf xss攻击”,虽然场景不同,但文章内容预览也可能遇到XSS问题。尤其是后台富文本编辑器提交的内容,如果直接在前端v-html渲染,很容易被注入恶意脚本。我的处理办法是后端做一层过滤,把script标签、onload事件等危险内容转义。前端也要对用户输入内容做一次清理,双重保障。
3. 前端Vue核心功能实现
3.1 环境准备:npm、vue-cli、node_modules报错
前端部分先从环境说起。热词里有一条“the project can not found node_modules”,这几乎是每个Vue新人都会遇到的坑。原因很简单:项目的依赖是记录在package.json里的,但实际的node_modules文件夹不会跟着项目一起提交。你从git或压缩包拿到项目后必须自己执行npm install,把依赖安装到本地。
npm install本身也有坑。默认源是npmjs官方源,国内下载速度慢、容易超时。我的做法是先永久切换源到淘宝镜像:
npm config set registry https://registry.npmmirror.com然后再执行npm install,基本一分钟左右能装完。如果已经安装了依赖但node_modules损坏,优先删除整个依赖目录再重装:
rm -rf node_modules npm cache clean --force npm install前端工程创建推荐几个方式:Vue 2项目用vue create命令,Vue 3项目推荐用npm create vite。vue create会自动帮你装好vue-router和Vuex,但版本选择时要注意:Vue 2对应vue-router 3,Vue 3对应vue-router 4,两者API差别不小,别混着来。
3.2 路由设计与权限控制
前端路由是整个项目的骨架。前台页面按模块拆分,后台页面单独放在/admin路径下。路由懒加载是必做的优化,每个路由组件单独打包成chunk,首屏只加载首屏需要的文件:
const ArticleDetail = () => import('@/views/ArticleDetail.vue')路由守卫我用在登录校验。只有路由meta里配置requiresAuth为true的页面才需要检查token,比如后台管理页。没有token就跳转到登录页,这个逻辑写在全局前置守卫里,比每个页面单独判断省事得多。
vue-router还有一个高频考点是路由传参。列表页跳到详情页,通常用动态路由传id,访问路径是/article/123。详情页里通过this.$route.params.id读取。这里要注意一个坑:从文章列表连续点击不同文章时,组件实例会被复用,不会重新走created钩子。解决办法是在文章详情组件里watch $route对象的变化,变化时重新拉取数据。
3.3 文章列表与详情页的数据交互
文章列表页的核心是axios请求后端接口。我封装了一个request.js,统一设置baseURL和请求拦截器。请求拦截器里读取localStorage中的token,加到请求头;响应拦截器里统一处理后端返回的code,如果code是401就清除本地token并跳转登录页。
service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config })列表页的组件通信要处理好分页。我用了el-pagination组件,页码变化时重新调用接口,接口返回的total用于渲染总条数。搜索筛选和分类筛选共用同一个请求方法,参数变化时把页码重置为1,否则会出现“你在第三页搜了个关键词但列表是空的”这种低级bug。
详情页除了展示文章内容,还有上一篇下一篇和评论列表。评论提交我用了一个表单校验:评论内容长度在1到200之间,昵称必填。前端校验只是用户体验,后端Controller同样要用@Validated做参数校验,防止绕过前端直接curl提交脏数据。
3.4 后台管理:富文本编辑器与图片上传
后台管理页面是后台管理能力的集中体现,我实现了三个核心功能:登录、文章发布、分类维护。
登录页用el-form做表单校验,提交时调用登录接口,成功后把token和用户昵称存到localStorage,再跳转到后台首页。文章发布页用mavon-editor作为Markdown编辑器,因为它同时支持编辑和预览,而且是中文界面。编辑器的内容直接绑定到文章的content字段,通过v-model双向同步到表单对象里。
图片上传是后台最容易被低估的功能。前端用el-upload组件,上传时要设置name字段和后端接口一致,否则后端拿不到文件。上传成功后后端返回图片URL,前端把URL插入到编辑器当前光标位置,这样文章里就能正常显示图片。
<el-upload action="/admin/upload" name="file" :on-success="handleUploadSuccess"> <el-button>上传图片</el-button> </el-upload>后端接收MultipartFile文件后,要做三件事:校验文件类型(只允许jpg、png、gif、webp)、限制文件大小(我设为5MB)、生成不重复的文件名(用UUID加时间戳)。文件保存路径可以放在服务器本地或OSS,个人项目保存到服务器本地通过nginx映射访问就够了。
3.5 computed、watch与keep-alive的实战区别
热词里反复出现“vue computed”,说明这是前端面试的必考点。在实际项目里,computed用来做派生状态,它依赖的响应式数据变化时会自动重新计算,而且有缓存。比如文章详情页的阅读时长,依赖content内容计算得到:
computed: { readingTime() { const words = this.article.content.replace(/[#*`\-\s]/g, '').length return Math.max(1, Math.ceil(words / 300)) } }watch则适合处理异步或开销较大的操作,比如监听路由参数变化重新拉文章详情。两者并不是完全替代关系,computed不能执行异步操作,watch可以。
热词里还有“vue keep-alive切换路由子组件el-table滚回头部”,这个坑我也踩过。后台文章列表有几十条数据,你往下滚动到了第20条,切到别的菜单再切回来,表格滚动位置回到了顶部。原因很简单:组件被销毁后重新创建,DOM是全新的。解决办法是用keep-alive缓存组件状态,但直接用keep-alive缓存整个后台页面又会导致表格数据不是最新的。折中方案是只在列表页缓存,activated钩子里重新拉取数据:
activated() { this.fetchList() }要注意的是,如果列表数据不需要每次切换都刷新,就别在这里拉接口,否则缓存等于白做。
4. 前后端联调、部署与问题排查
4.1 开发环境联调:跨域代理配置
前后端分离项目最烦的就是跨域问题。浏览器同源策略会拦截不同端口号下的请求,前端开发服务器运行在8080端口,后端运行在9090端口,直接请求必然跨域。两种解决方法我都用过,推荐优先用Vue的devServer代理:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:9090', changeOrigin: true } } } }这样前端代码里请求/api/blog/article,开发服务器会把请求转发到http://localhost:9090/api/blog/article。浏览器看请求是同源的,就不会拦截。这个方案不需要后端配合,开发环境最方便。但生产环境就不能靠它了,生产环境直接由Nginx把/api前缀的请求反向代理到后端服务。
如果你接手的是一个已经部署的项目,后端接口地址和前端页面地址不同域名,那就得在后端加跨域配置:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:8080") .allowedMethods("*") .allowedHeaders("*"); } }CORS配置最注意allowedOrigins不能写*同时又想带cookie,这样会被浏览器拒绝。个人博客如果没有跨域登录需求,建议不要开启allowCredentials,避免增大安全面。
4.2 Docker部署SpringBoot后端
部署这块我觉得是整个项目里含金量最高的部分,因为很多人写代码很溜,但一到部署就发怵。我用了Docker Compose统一管理后端和数据库,先说后端最简Dockerfile:
FROM openjdk:8-jre-alpine COPY target/blog-server.jar /app.jar ENTRYPOINT ["java", "-jar", "/app.jar"]构建的时候先执行Maven打包:
mvn clean package -DskipTests docker build -t blog-server .如果数据库也容器化,docker-compose.yml里把后端和MySQL编排在一起,方便一键启动。有一个细节要注意:后端连接数据库的地址不能写成localhost,因为容器内的localhost指向容器自己,要写成服务名(比如db),Docker Compose会做DNS解析:
version: '3' services: db: image: mysql:5.7 environment: - MYSQL_ROOT_PASSWORD=123456 - MYSQL_DATABASE=blog_db volumes: - ./mysql-data:/var/lib/mysql server: build: . ports: - "9090:8080" depends_on: - db environment: - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/blog_db热词提到“docker部署springboot项目”,我补充一个真实经验:容器启动时SpringBoot会等MySQL就绪,但depends_on只控制启动顺序,不控制MySQL是否可连接。如果后端启动比MySQL快,会报数据库连接失败。解决方案是在后端启动命令里加一个等待脚本,比如sh -c "until nc -z db 3306; do sleep 1; done; java -jar /app.jar"。或者更省事,让SpringBoot数据源增加自动重连配置,失败后重试几次。
4.3 Nginx部署Vue前端
前端构建产物是纯静态文件,用Nginx提供服务最合适不过。构建命令:
npm run build构建完之后dist目录就是最终产物,把它上传到服务器,Nginx配置一个server块指向该目录。这里的关键难题是vue-router的history模式。前端路由路径是/article/123,但服务器上并不存在这个物理文件,直接刷新页面会404。解决办法是配合try_files把请求回退到index.html:
server { listen 80; server_name your-domain.com; root /var/www/blog/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这样前端路由刷新后Nginx会找到index.html,由前端路由继续处理。/api前缀的请求反代到后端服务,实现前后端在同一域名下互通。
4.4 高频报错与排查清单
前端和后端联调过程中,我整理了高频报错排查表,按影响程度排序:
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 启动SpringBoot报UnsupportedClassVersionError | JDK版本太低 | 升级JDK到对应版本,或降级SpringBoot版本 |
| 页面白屏且控制台报SyntaxError | 前端代码压缩后语法不兼容低版本浏览器 | 配置browserslist,用babel转译ES6 |
| 文章接口返回401 | token未携带或token过期 | 检查前端请求拦截器,重新登录 |
| npm install报network/socket错误 | 默认源连接超时 | 切换镜像源,重试 |
| 后端接口返回500空指针 | 数据库表字段与实体类不对应 | 检查驼峰映射配置和字段名 |
| 图片上传失败 | nginx的client_max_body_size限制太小 | 调整为大于5m |
| 刷新前端页面404 | history路由未配置try_files | 添加try_files回退到index.html |
| 评论区昵称乱码 | 数据库字符集不是utf8mb4 | 建库时指定charset=utf8mb4 |
有几个排查技巧值得单独说。后端日志永远看完整堆栈,不要只看第一行错误信息,往往真正原因在Caused by里。前端排查用Vue Devtools可以实时看组件状态和Vuex数据。热词里提到“vue devtools插件下载”,这个插件确实重要,装好之后能直接看到响应式数据和路由状态,排查问题效率翻倍。如果Chrome商店下不了,也可以用源码构建或找离线安装包,但要注意来源安全。
4.5 安全加固与性能优化清单
个人博客虽然流量不大,但安全底线不能丢。我在项目上线前做了这几件事:
- 密码传输加一层简单的前端加密,避免明文走HTTP传输。虽然不是绝对安全,但至少不能让抓包的人一眼看到明文密码。
- 后端全局过滤XSS脚本,对请求参数中的script、iframe、onerror等关键字进行转义。文章内容在后端渲染成HTML时也做一次过滤,防止存储型XSS。
- 对管理后台接口做简单的防暴力破解:连续输错5次密码,锁定该IP十分钟。用AOP加Reids计数器就能实现,不用太复杂。
- 接口层校验入参长度和格式,防止提交超大content打爆数据库。
性能优化方面,个人博客最有效的三板斧是:文章列表接口开启MyBatis-Plus分页,并且只查需要的字段;静态资源(图片、JS、CSS)交给Nginx直接返回,配置缓存头;详情页用浏览器缓存,文章不常更新可以设置一小时强缓存。Redis缓存我一开始没加,因为当时的流量完全用不上,后来访问量上来了才在文章详情接口加了缓存,逻辑也很简单:查Redis,没有就查数据库后写入,更新文章时删除对应缓存。
这套流程跑下来之后,我自己最大的感受是:真正费时间的不是写CRUD,而是解决环境配置、版本兼容和联调细节。你会发现很多问题不是代码逻辑难,而是“诶为什么这里会这样”的环境问题。耐心一点,把排查思路整理成文档,下次遇到就能秒杀。
最后再分享一个项目扩展的小技巧:博客做出来之后,可以顺手给它加一个RSS订阅、sitemap.xml生成、全站搜索关键词统计,这些功能不复杂,但都很适合写进简历的“项目亮点”里。面试官问你项目的时候,能把这些细节讲清楚,比堆技术名词加分得多。
本文还有配套的精品资源,点击获取