干这一行最怕的不是项目做不出来,而是项目一做完就不知道该拿它怎么办。个人博客这个实训题拿到手的时候,我第一反应是“这题我熟”,但真正带着大家从零到一跑完一遍之后才发现,越是这种看起来人人都会做的题目,越能把基本功、工程思维和防坑经验全逼出来。这篇博客就好好把个人博客从设计、技术选型到实现、部署的完整路径拆一遍,把我踩过的坑和总结的经验一并放进来,做个实实在在的参考。
1. 项目整体设计与技术选型思路
1.1 实训题怎么拆:个人博客不只是“写文章”
个人博客听起来是个很小的项目,很多人的第一反应是“不就是前台展示文章、后台发文章嘛”,实际上把它当实训项目来做时,它覆盖的知识点远比想象中多。一个完整的个人博客至少要包含:文章的创建与编辑、分类标签管理、评论互动、搜索、阅读量统计、后台登录鉴权、前端响应式页面,以及最后的上线部署。拆开之后你会发现,这其实就是一个简化版的 CMS 内容管理系统,几乎涵盖了 Web 开发最典型的所有场景。
所以做实训的选题,别只盯着“实现了个博客”这个结果,要看到它背后的价值:它要你处理列表与详情的数据流转,处理文本编辑和富文本渲染,处理一对多、多对多的关系建模,还要处理文件上传、登录会话、接口安全、部署配置这些平时单点练习很难串起来的内容。真正跑通这个闭环,你的 Web 基础就算立住了。
1.2 技术栈到底怎么选:别盲目追新,要立足实训目的
实训项目的技术选型,我的建议是“以能完整实现、能独立讲解、能快速部署”为优先,而不是以“看起来高大上”为优先。我们这轮走的是“Java Spring Boot + Vue 3 + MySQL”的组合,放在国内实训场景里非常主流,找工作写简历也够典型:后端是成熟的 MVC 架构,前端是组件化开发,数据库侧有完整的关系模型,整套体系学下来,基本上就是企业里中小型 web 工程的微缩版。
我在对比方案的时候也认真考虑过 Flask + SQLite、纯 PHP、甚至 Next.js 全栈方案。Flask 的问题是 SQLite 在并发和复杂查询的教学演示上偏弱,且部署时需要额外折腾 WSGI;纯 PHP 虽然部署极简单,但项目结构和代码规范容易写飞;Next.js 全栈足够现代,可是对实训者要求太高,很多人会被 SSR、API Route 这些概念绕晕。Spring Boot 的生态规范、注解式开发、大量现成教程,意味着你在遇到问题时能找到足够多的参考,这对实训项目来说非常重要。
1.3 先立原则:哪些功能必须做,哪些必须砍
做实训项目最忌讳“什么都想加”。我们在正式开始前,先把功能清单分成三类:基础必备、加分可选、坚决不做。基础必备是文章 CRUD、分类标签、评论、登录、统计访问量;加分可选是 Markdown 编辑器、代码高亮、RSS 订阅、分页搜索;坚决不做的是即时聊天、在线写作协同、多用户权限体系这些超出实训体量的功能。
砍掉多余功能的理由很简单:实训项目的周期通常是几周,你有限的时间应该花在把核心链路打磨扎实,而不是把五个功能做出五个半成品。控制项目体量也是工程能力的体现,能克制住需求蔓延,比堆功能更能体现成熟度。
2. 核心功能模块拆解与难点预判
2.1 文章模块:不止是“存进去、取出来”
文章模块是博客的心脏,但又最容易做浅。很多人的实现方式就是一张表存标题和正文,然后列表页 select 一下,详情页再 select 一下,完事。这么写确实能跑,但距离 “可用的博客”还有距离,因为你还得考虑:列表页要不要显示摘要?摘要是从正文里截取还是单独存字段?文章是 markdown 格式存还是 HTML 格式存?需不需要定时发布?要不要草稿箱?
我的做法是文章表里预留summary字段,发布时不传就自动从正文前 160 个字符截取。正文统一存 Markdown 原文,展示层再做渲染,这样既方便编辑,也方便未来换主题或做 api 输出。草稿状态用status区分,0 草稿、1 发布、2 回收站。一开始就预留这些字段,后面加功能就不用改表结构,这是我在多次项目里被“后期加字段”支配之后总结出来的经验。
2.2 分类和标签:多对多关系是绕不开的坎
分类和标签是博客最常见的组织方式,它们之间的差异适合拿出单独说一下:分类是层级结构,通常一对一,文章只属于一个分类;标签是扁平结构,一篇文章可以打多个标签,一个标签也能挂多篇文章,这是典型的多对多关系。
多对多关系的正确建模方式是三张表,一张文章表、一张标签表、一张关联表。关联表里就存两列:article_id和tag_id,联合起来作为主键。很多初学者会把标签直接塞进文章表的一个字段里,用逗号分隔,等到后面想按标签筛选文章时,就会发现 SQL 写得极其痛苦。我见过不止一个实训同学在这里返工,一次是查询想用like模糊匹配,结果把所有“包含这个子串”的文章全揪出来了;另一次是改标签名时要遍历所有文章去更新字符串,原地爆炸。
2.3 评论模块:自关联表和审核状态不能少
评论模块是博客里被严重低估的部分。新手最容易忽略的是评论的层级结构,也就是“楼中楼”。如果你只需要一级评论,那就简单得多,父评论字段为空就行;但通常你要支持回复某条评论,所以评论表需要设计一个parent_id自关联字段,为 0 表示顶级评论,不为 0 则表示回复的是哪条评论。
更重要的一个点:审核状态。实训项目虽然不上真实生产环境,但我在设计时强制加入了status字段,0 待审核、1 已通过、2 已驳回。这样做的原因是,评论区一旦放开,垃圾评论就是必须面对的问题,哪怕是实训做给自己看,也要培养这个意识。前台只展示审核通过的评论,后台管理员可以看到全部评论并操作审核,这套逻辑做进去之后,你的评论模块才称得上“完整”。
2.4 管理后台:个人博客里技术含量最高的部分
管理后台经常被当成“随便写写”的部分,但它的技术含量其实比前台高得多。前台大多数时候是“查”数据,后台则是“增删改查”全套,还要处理登录状态、文件上传、表单校验、权限控制等等,复杂度一下子翻倍。
以文章管理为例,你要做一个可用性说得过去的编辑页面,至少得处理:富文本/Markdown 编辑器接入、图片上传与回显、标签多选、分类树选择、定时发布时间设置、草稿保存。每一项拆开都没那么难,但组合在一起,就极其考验你对表单数据模型、异步接口、组件通信的理解。后台是“吃功夫”的部分,实训答辩时老师最喜欢深问的,往往也是这里。
3. 数据库设计与后端接口实现
3.1 五张核心表的结构:先想清楚关系再动手
这一步决定了项目能走多远。我最终使用的表结构分为五张核心表加两张关联表,具体字段如下:
文章表article:id、title、summary、content、status、category_id、view_count、create_time、update_time、publish_time
分类表category:id、name、description
标签表tag:id、name
文章标签关联表article_tag:article_id、tag_id,联合主键
评论表comment:id、article_id、nickname、content、parent_id、status、create_time
用户表user:id、username、password_hash、avatar、create_time
设计要点有三个。第一,密码字段存的是 BCrypt 哈希,后面加用户注册也不用担心明文泄露问题;第二,文章和分类是“多对一”,所以把category_id放在文章表里,而不是单独建一张关联表;第三,文章和标签是多对多,所以我坚持使用关联表而不是冗余字段。你不需要一开始设计得极其复杂,但这几个基础关系一定要从一开始就是对的。
3.2 统一返回体和接口格式:早期想不到,后期哭着补
实训项目最大的“开发效率杀手”之一是接口返回格式不统一。一开始大家图省事,有的接口返回{ "code": 0, "data": ... },有的直接返回一个数组,还有的出错时返回{ "message": "error" },前端解析时得挨个接口写特殊处理,改到一半就想骂人。
我的建议是一开始就定死一个全局返回结构:code、message、data三个字段。成功时code为 0,失败时返回非 0 错误码,data统一放核心数据。前端写一个request.js封装 axios 拦截器,所有请求都走同一套逻辑。这样一旦遇到后端报错、token 过期、权限不足这些情况,前端就能统一处理,不用每个页面重复写判断。这个习惯适用于你将来所有的 Web 项目,越早养成本越低。
后端返回代码示例(Spring Boot 的实际写法大概是这样):
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(0); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }接口设计上还需要遵循一个原则:列表接口必须分页。博客列表用page和pageSize两个参数,返回数据中带上total,这样前端才能渲染正确的分页组件。不要等到文章数超过一页才来加,一开始就做成分页,后面省很多事。
3.3 登录认证选型:JWT 还是 Session
评价一个实训项目的“工程含量”,随便问他一句“你怎么做登录认证”就能看出来。Session 和 JWT 都可以用,但我更推荐 JWT,原因很务实:前后端分离场景下 JWT 天然无状态,后端不用维护 Session 存储,移动端比例大的场景下,JWT 也可以直接复用同一套接口。
JWT 的核心逻辑是用户登录成功后,后端签发一个带过期时间的 token 给前端,前端每次请求把 token 放在请求头Authorization里,后端通过拦截器校验 token 是否有效,并从 token 中解析用户信息。附带的好处是,你可以把用户 id、用户名放进去,后续接口里直接用,省得每次查数据库。
代码层面,Spring Boot 里做这种校验的关键是 HandlerInterceptor,核心逻辑大致如下:
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !JwtUtil.verify(token)) { response.setStatus(401); return false; } Integer userId = JwtUtil.getUserId(token); request.setAttribute("userId", userId); return true; } }我踩过一个坑:前端 axios 请求时如果不加拦截器,token 不会自动带上去,后端就一直收不到,所以前端必须设置请求拦截器。顺手封装一下,所有接口就都能带上 token,这块 90% 的联调问题都是从这里来的。
4. 前端页面实现要点
4.1 页面路由与组件结构:先把导航想明白
前端的总体结构要有清晰的导航路径,博客前台大致是:首页文章列表、分类页、标签页、文章详情页、关于页;管理后台是:登录页、后台布局页、文章管理页、分类标签管理页、评论管理页。我建议前台和后台路由彻底分开,各自有独立的布局组件,这样两个系统之间互不干扰,代码结构也清爽得多。
Vue 3 中可以使用 Vue Router 的嵌套路由来实现这个结构。父路由/admin下挂一个布局组件,子路由渲染到布局的<router-view />里。后台登录之后用路由守卫判断是否有 token,没有就被踢回登录页。这样你在开发时只需要关注单个页面内的逻辑,而导航、布局、鉴权的统一逻辑是全局维护的。
4.2 编辑器的正确选择:Markdown 是实训最优解
输入框里写正文的方式不是不行,而是你很快就想要加粗、加标题、插图片的排版能力,这时候就得选编辑器。我强烈推荐 Markdown 编辑器,比如md-editor-v3或v-md-editor,原因有两条:一是 Markdown 文本存储轻、渲染解析库成熟,二是你日后如果需要迁移内容,Markdown 文件本身就是最通用的文本格式,不会锁死在某个编辑器逻辑里。
编辑器需要配合marked或markdown-it做展示渲染,代码高亮可以用highlight.js。这里要提醒一个容易踩的坑:编辑器和渲染层必须使用同一种 Markdown 语法风格,不然你在后台预览没问题,到了前台渲染却对不上。尤其是代码块语言标记、表格写法、图片相对路径解析,这四处最容易出现前后台不一致。
我在实训中遇到过一个很经典的图片问题:上传的图片被存到后端本地磁盘的upload/目录,Markdown 里写入的是相对路径,本地开发时前端跨域导致图片加载不出来。视情况不同,你可以用 Nginx 做代理,让前端访问/upload/时直接转发到后端,也可以后端把图片转成 base64 存库。我的推荐是:本地开发阶段,在 Vue 的 vite 配置里加一个 proxy,把/upload代理到后端;生产环境则统一交给 Nginx 处理静态资源映射。
4.3 管理后台的表格页方案:不用高级组件也能做得专业
后台最核心的页面是“文章管理页”,本质上就是一个表格 + 搜索 + 分页 + 操作按钮。如果你用的组件库是 Element Plus,实现这个页面其实非常模块化。el-table负责展示,el-pagination负责分页,顶部放一个搜索框支持按标题模糊查询,操作列放编辑、删除、置顶三个按钮。看起来简单,但“查询条件和分页参数如何联动”才是这里核心的知识点。
我的实现思路是维护一个query响应式对象,包含page、pageSize、keyword、status等字段。每次搜索按钮点击时,把page重置为 1,再拉接口;切换分页时只改page,不发额外请求以外的逻辑。数据返回时后端给total字段,前端把它赋给分页组件的total,这样整个表格页的状态管理就闭环了。一个干净利落的表格页,背后就是你处理“筛选条件、分页状态、请求数据、刷新视图”四者关系的能力,这个思路换到任何中后台项目都能用。
5. 部署与上线实用指南
5.1 前后端分离项目部署:Nginx + 后端进程就够用
实训项目的部署并不需要上容器、上 DevOps 那些重东西,一台最基础的云服务器 + Nginx + 一个 Java 进程就够了,把流程跑通才是关键。我的部署方案是:前端构建后的静态文件放在服务器/var/www/blog目录,nginx 直接指过去;后端打成 jar 包放到/opt/blog,用nohup java -jar blog.jar启动;Nginx 做两件事,一是把/api开头的请求反向代理到http://localhost:8080,二是把图片路径/upload也代理过去。
这里给一个具体可用的 Nginx 配置片段,直接抄作业就行:
server { listen 80; server_name your-domain.com; root /var/www/blog; index index.html; location /api/ { proxy_pass http://127.0.0.1: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://127.0.0.1:8080/upload/; } location / { try_files $uri $uri/ /index.html; } }try_files那行尤其关键,它保证了前端路由在刷新页面时不会 404,否则你在/article/123刷新一下就会被 Nginx 误判成请求不存在的文件,直接白屏。
5.2 跨域、端口、防火墙:部署前必查的三件事
实训项目部署时最常见的问题是“本地跑得好好的,上线就废了”,原因基本就集中在三件事上。第一,前端请求地址必须改成服务器域名或 IP,不能再写localhost;第二,后端启动端口要被防火墙放行,或者干脆只通过 Nginx 暴露 80 端口,后端不直接对外开放;第三,跨域配置要确认正确,如果你用了 Spring Boot 的@CrossOrigin注解,而部署时又用 Nginx 做了代理,那开发环境需要的跨域支持在生产环境其实已经不需要了,但旧的跨域配置可能反过来干扰请求头。
我给实训同学的标准检查顺序是:先curl http://localhost:8080/api/article/list验证后端本地通不通,再curl http://your-server.com/api/article/list验证服务器端口和防火墙,最后用浏览器打开前端页面看控制台报错。按照这个顺序排查,基本能在十分钟内定位 90% 的部署问题。
5.3 什么时候才需要上 Redis:先别急着加中间件
动不动就上 Redis 是实训项目里最常见的过度设计。个人博客的访问量在实训阶段根本达不到需要 Redis 做缓存的程度,MySQL 完全扛得住。我曾经见过一个实训同学给博客加了 Redis 缓存、消息队列、ES 搜索引擎,结果光是配置这些中间件就花了两天时间,最后答辩时连核心业务都讲得磕磕绊绊。
我的判断标准很简单:当数据量达到一万篇文章、接口响应明显变慢的时候,再来讨论优化方案不迟。实训项目更重要的是把业务链路跑通、把部署流程跑熟。阅读量计数器这种操作,直接用UPDATE article SET view_count = view_count + 1 WHERE id = ?一条 SQL 搞定,不需要发明轮子。把缓存、搜索这些中间件留到后续扩展阶段,配合真实的性能问题进行优化,才算用到了刀刃上。
6. 常见问题速查表与项目复盘
6.1 高频问题速查:从报错到排查思路
我在带这个实训项目期间,把大家反复踩到的坑汇总成了下面的对照表,每一条都是真实发生过的:
| 现象 | 根因 | 解决方案 |
|---|---|---|
| 前端请求接口 404 | 后端接口路径和前端不一致 | 打开浏览器 Network 面板比对 URL,统一为/api前缀 |
| 列表数据能查到,但图片打不开 | 图片访问路径被前端 dev server 拦截 | 配置 vite proxy 代理/upload到后端 |
| 登录成功后请求仍返回 401 | 前端 axios 没在请求头带 token | 检查 axios 请求拦截器是否配置了Authorization |
| 前端刷新页面白屏 404 | Nginx 没配try_files | 在location /中加入try_files $uri $uri/ /index.html; |
| 文章正文字数太长保存失败 | MySQL 默认text类型容量不够 | 把content字段改为longtext,或改后端接收参数大小限制 |
| 标签筛选文章结果不准 | 标签关系未走关联表查询 | 使用关联表 join 查询,禁止用字段存放标签字符串 |
| 点击阅读量发现没变化 | 阅读量接口没加事务重试 | 确认更新语句是view_count + 1,而不是先查再写死 |
如果遇到控制台出现的是语法错误或某个组件库的报错,我的建议是先别急着看百度,准确把报错信息复制到文本里,理清楚在哪个文件、哪个步骤触发的,再带着具体路径去搜。盲目搜索的不仅搜不到答案,还会让你越绕越远。
6.2 答辩和总结:实训项目的“交付力”从哪来
实训项目的最终评价不只靠代码本身,还要靠你的表达逻辑。个人博客虽然功能常规,但如果被问到“为什么这样设计”时答不上来,项目分一样会受影响。这里给一套自我提问清单:为什么文章和标签是多对多而不是直接在文章表写死?评论为什么要设计审核状态?JWT 过期时间是多久、过期后前端怎么处理?这些问题的答案都能在设计阶段找到出处,所以请务必在开发时把每个关键决策记录到 README 里。
更建议你在项目里写一个README.md,里面包含:项目简介、技术栈说明、功能清单、表结构说明、启动部署步骤、常见问题解答。这一份文档的价值有两个:它既是帮助老师快速理解项目的入口,也是你将来回顾项目时的第一手资料。实训项目可能只陪你几周,但这套“技术选型有依据、调试有记录、部署有脚本”的工作方式,才是真正能带走的东西。
6.3 再深入一步:个人博客的多种扩展方向
实训结束不代表这个项目就没有价值了,个人博客本身就是极其适合持续迭代的载体。比较自然的扩展方向有三个:第一个是把静态资源上传改成对接阿里云 OSS,消除本地磁盘依赖;第二个是增加站内搜索,小规模可以先考虑 MySQL 的全文索引,文章量上来后再考虑更专业的搜索引擎;第三个是支持主题切换和自定义页面,把前端从前台页面扩展到可配置化。
另外还有一个我自己很推荐的小功能:写作统计。在后台增加一个“数据概览页”,展示本月文章数、评论数、总阅读量,用几个图表把数据可视化。这个功能开发成本不高,却能让项目的完整度再上一个台阶,答辩时图表一摆,直观且有说服力。
个人博客项目看着简单,实际上每一个模块都有值得深挖的“为什么”。把基础链路走扎实,把工程习惯养好,它完全可以成为你从学生切换到开发者思维的第一个转折点。我始终认为,项目不在大,而在于你真正理解了每一行代码背后的选择逻辑,这才是实训真正的意义所在。