ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

全栈博客系统实战:从技术选型到部署上线的完整指南

全栈博客系统实战:从技术选型到部署上线的完整指南 聊到全栈项目练手博客系统几乎是我见过最适合把前端、后端、数据库、部署一次串起来的项目没有之一。你不需要去理解一个陌生的业务规则因为博客的每个功能你作为一个读者早就用过无数遍写文章、发评论、存草稿、改标签。做这样一个全栈博客系统会逼着你把整个技术链路完整走一遍而且最终交付的是一个有真实用途的产物不是那种一删就完事的练习Demo。这篇博文整理的是我从零开始做全栈博客系统整个过程中的设计思路、技术选型、落地代码和踩坑记录适合那些已经学完语言基础、但还不太清楚怎么开始第一个完整项目的人参考。1. 这个项目到底值不值得做1.1 为什么博客系统是“全栈”的完美试验田很多人学完前端学后端最后发现自己只会写碎片会用Vue画一个列表页会写几个登录接口但真要独立做一个网站时脑子里一团浆糊。博客系统恰好把全栈所需的核心板块全部覆盖了前端交互、后端接口、数据库持久化、文件上传、用户认证、评论互动、部署上线。它就像一个微缩版的内容平台每一个模块都是真实的业务场景而不是教科书里的玩具案例。打个比方如果你一门心思只写接口那你永远不知道一个正常的前端页面是怎么消费这些数据的如果你只做前端你也不知道接口返回的JSON到底怎么产生的。博客系统逼着你同时站在两端看问题文章从编辑器里写出来到存进数据库再到被读者打开渲染成一个完整页面这条完整链路会让你对整个体系有真正的体感。做完这个项目你再去接触电商系统、社区论坛、企业官网会发现很多模块的思路都是相通的。更关键的是博客系统的需求边界足够清晰不像“做一个商城”那样需要纠结一大堆SKU、库存、支付回调这些业务复杂度。博客的核心场景就是“写”和“读”这让你可以把注意力集中在技术实现本身而不是被业务规则淹没。如果你已经有了一些编程基础正愁不知道该做什么练手项目博客系统是我个人最推荐的起点。1.2 需求定位先想清楚你要什么动手写代码之前我建议你先花半天时间把需求列出来而不是直接打开IDE就建工程。博客系统可大可小小到只有一个文章列表加详情页大到支持多用户、评论通知、草稿箱、标签管理、全文搜索、访客统计。功能太多做不完功能太少又没有学习价值所以第一步是把需求按优先级拆成版本。第一版本是最小可用产品必须包含文章发布、文章列表、文章详情、Markdown渲染。这四条是博客的骨架先把它们打通意味着你已经拥有一个能“写、存、读”的闭环。第二阶段往上加交互层评论、标签、归档、搜索。第三阶段做管理能力用户登录、后台发布、草稿箱、文件上传。更后面的东西比如RSS订阅、站点地图、访问统计、文章置顶都属于锦上添花。我建议你和最初的我一样不要一上来就想要全部功能先做一个能给自己日常记笔记用的产品再慢慢迭代。下面这个表格是我当时整理需求时用的对照表它能帮助你一眼看到每个模块对应的技术难点心里大概有个底功能模块涉及技术点难度文章发布富文本/Markdown编辑器、表单校验、图片上传中文章列表与详情分页、路由、Markdown渲染、代码高亮低评论系统用户认证、关联查询、防XSS、防刷中上用户登录注册JWT或Session、密码加密、权限拦截中标签与分类多对多关系、SQL关联查询中后台管理前端路由守卫、后端接口鉴权中部署上线Nginx、域名、HTTPS、数据库迁移中这里我要强调一句项目做出来的第一版一定不要追求大而全而是要把“文章从发布到展示”这条主干路径做到足够顺滑。你后面遇到的所有高级功能本质上都是在这条主路上挂分支主干通畅了挂分支才有意义。2. 技术选型我为什么选这组方案2.1 前后端不分离 vs 前后端分离选哪个做全栈选技术栈时第一个绕不开的问题就是到底用传统服务端渲染还是目前主流的前后端分离。传统方案的代表是JSP、Thymeleaf、模板引擎后端把页面模板和数据组装好之后直接返回给浏览器一套完整HTML前后端分离则是后端只提供纯JSON接口前端用Vue、React这类框架单独构建页面。两种方案都能做出博客但适用场景完全不同。传统服务端渲染的明显优势是首屏快、对搜索引擎友好因为页面内容直接出现在HTML里不需要浏览器再跑一遍JavaScript去动态渲染。缺点则是前后端代码耦合在同一个工程里开发时前端改一下样式后端开发也要跟着启动整个项目协作体验比较差。如果这个博客主要是你自己写给自己看传统方案完全够用而且部署简单。但我的选择是前后端分离因为我的目标不仅仅是“做一个能用的博客”而是“做一个能展示自己全栈能力的项目”。分离式架构下前端工程和后端工程可以独立开发、独立部署接口文档也能沉淀下来这更接近目前主流团队的协作模式。你如果学会这一套找工作或者接手公司项目时会更快上手。不过我也要说清楚分离式项目如果对SEO有强需求后期要补很多方案比如预渲染或瀑布流式客户端渲染。个人博客还好你如果打算让搜索引擎大量收录这个痛点后面有的是活儿做。2.2 数据库与ORMMySQL加MyBatis-Plus还是JPA文章、用户、评论、标签这些数据有明显的关联关系我毫不犹豫选了MySQL。有些人觉得MongoDB这种文档数据库更灵活存文章结构很自由。但博客的评论和文章之间存在强关联用户和文章之间存在归属关系如果统统塞进文档里后面做列表、做统计、做聚合查询都会非常别扭。关系型数据库这种“表结构清晰、事务可靠、查询能力稳定”的特性完全压过了“动态字段”带来的那点便利。ORM框架我最终选了MyBatis-Plus原因很简单可控。它的CRUD封装能帮我省掉大量重复的单表操作同时复杂查询我又能直接写SQL不至于像JPA那样遇到坑时一头钻进黑盒里。JPA开发效率确实高尤其在对象模型和表结构非常匹配的时候但它的懒加载、N1问题、一级缓存和二级缓存这些概念对初学者来说太重了。MyBatis-Plus的思路更直白需要简单的直接调用现成方法需要复杂的自己写XML里的SQL语句。这种“中间路线”对我这种既想快速落地又想理解每一步原理的人来说最舒服。当然这不是说Java系就是唯一答案。现在很多全栈项目也用Node.js的Express或者NestJSPython的FastAPI也是很好的选择。选型没有绝对的对错关键是你要清楚你的语言生态里哪个ORM最顺手。我的建议是如果你以后想往企业级开发走Java加MyBatis-Plus的组合值得体验一次如果你更看重开发效率和轻量部署Node.js全栈也完全能做出一个漂亮的博客系统。怕就怕什么都想要今天用这个框架明天换那个最后项目烂尾。2.3 Markdown编辑器与渲染博客的灵魂博客系统里有三样东西决定用户愿不愿意长期用写文章是否顺手、打开文章是否够快、代码片段是否清晰。其中Markdown处理和渲染是最核心的一环。前端编辑器我推荐直接用成熟组件而不是自己从头做一个。个人项目完全没有必要在这里重复造轮子选择Vditor、Toast UI或者单纯的textarea加markdown-it都可以。我自己的实现用的是textarea加markdown-it的方案因为组件依赖少渲染逻辑完全掌握在自己手里。这里有一个非常关键的决策点后端到底应该存原始Markdown文本还是存转好的HTML。我见过有人为了省事前端提交的时候直接把markdown转成HTML字符串存库结果后面想换编辑器、想改主题样式、想做全文搜索的时候全傻眼了。正确做法是数据库里存原始Markdown文本展示的时候再根据页面主题动态渲染HTML。原始文本是最可靠的数据资产需要什么格式随时能生成如果你只存HTML想反推原始Markdown就非常麻烦。代码高亮是我强烈建议第一版就要加的没有高亮的代码块基本等于一堆黑压压的纯文本阅读体验非常差。我在前端渲染流程里引入了highlight.js配合当前的博客主题做了一版高亮样式实测下来代码块阅读体验提升非常明显读者看技术文章时确实离不开这个。2.4 用户认证与权限到底需要多复杂博客系统至少会有一个管理员自己如果开放注册还需要普通用户身份的区分。用户认证我推荐用JWT或者Session加Redis两者各有优劣。JWT的优点是无状态后端不需要存会话信息水平扩展时非常方便缺点是踢人难、登出逻辑麻烦token一旦泄漏在过期之前很难远程吊销。传统Session的优点是服务端可以随时删除会话配合Redis也能做到多实例共享缺点是需要额外维护会话存储。个人博客项目的流量和管理员数量都很有限其实用哪种方案都行我更推荐先理解清楚它们的原理再动手。我自己的选择是JWT加HttpOnly Cookie前端把token塞进Cookie里浏览器每次请求自动带上这样前端JavaScript拿不到token能降低被XSS脚本窃取的风险。网上很多教程喜欢把token存在localStorage里然后通过Authorization请求头发送这样写起来确实直观但安全性要差一点因为只要页面上有一个渲染漏洞攻击者就能把token偷走。这个细节我建议全栈开发者在第一次做项目时就养成良好习惯。权限控制上最重要的一句话前端隐藏按钮不等于后端安全。也就是说页面上你可以根据用户角色决定显示“编辑”还是“删除”但后端接口必须再次校验角色否则别人绕过前端直接调你的删除接口照样可以删数据。我在写后台接口时专门定义了一个拦截器对所有带权限要求的接口做统一校验而不是在每一个Controller方法里重复判断。3. 实操过程从数据库表到一条文章发布的完整链路3.1 数据库表设计文章的“骨架”我们直接上手设计数据库表。博客最核心的表是文章表它的字段设计要注意几个容易踩坑的点标题、摘要、正文、状态、发布时间、更新时间、作者ID。状态字段建议用字符串而不是布尔值因为一篇文章的状态至少有三个草稿、已发布、已删除布尔值根本无法表达。摘要字段不要随手省略列表页如果直接截取正文字段性能会很差而且会把Markdown语法的原始字符暴露出来十分影响观感。用户表要包含用户名、邮箱、密码哈希、角色、创建时间。关键点是密码绝对不允许明文存储至少要用BCrypt这类自带盐值的算法做哈希我在项目里用的就是Spring Security自带的BCryptPasswordEncoder。标签表很简单字段就是名称和别名。文章和标签是多对多关系所以还需要一张关联表单独存储文章ID和标签ID。下面是一段简化后的核心建表SQL你可以直接当作项目初始版本参考CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL UNIQUE, password_hash VARCHAR(100) NOT NULL, role VARCHAR(20) NOT NULL DEFAULT USER, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE article ( id BIGINT PRIMARY KEY AUTO_INCREMENT, author_id BIGINT NOT NULL, title VARCHAR(200) NOT NULL, summary VARCHAR(500) NOT NULL DEFAULT , content MEDIUMTEXT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT DRAFT, published_at DATETIME NULL, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted TINYINT NOT NULL DEFAULT 0, INDEX idx_author (author_id), INDEX idx_status_published (status, published_at) ); CREATE TABLE tag ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE, slug VARCHAR(100) NOT NULL UNIQUE ); CREATE TABLE article_tag ( article_id BIGINT NOT NULL, tag_id BIGINT NOT NULL, PRIMARY KEY (article_id, tag_id) );索引设计这一块要啰嗦两句。文章列表页最常见的排序是“已发布且按发布时间倒序”所以我在status和published_at两个字段上建了联合索引。个人博客的数据量通常不大索引不要加太多每张表多那么三五个索引反而会拖慢写入速度。删除策略我用的是逻辑删除也就是deleted字段标记而不是物理DELETE这样就算误删了文章也能抢救回来。3.2 后端接口把写文章变成一次HTTP请求数据库表设计好之后下一步就是把对文章的操作映射成接口。REST风格是我一直在用的方式核心接口基本是固定的几套动作获取文章列表、获取文章详情、新建文章、更新文章、删除文章再加上用户注册和登录。这里有个容易犯的错误新建文章和更新文章要分开处理新建时没有ID更新时必须校验文章是否存在且当前用户是否有权限操作。下面是关键接口的整理动作请求方式路径说明获取已发布文章列表GET/api/articles?page1size10只返回已发布文章获取文章详情GET/api/articles/{id}根据ID返回文章内容新建文章POST/api/articles需要登录身份为作者更新文章PUT/api/articles/{id}需要登录校验作者身份删除文章DELETE/api/articles/{id}逻辑删除用户注册POST/api/auth/register用户名、邮箱、密码用户登录POST/api/auth/login成功后返回JWT发布评论POST/api/articles/{id}/comments可开放也可仅登录用户可用后端示例我用Spring Boot的Controller写法来展示逻辑非常简单接收请求、校验参数、调用Service处理业务、返回结果。RestController RequestMapping(/api/articles) public class ArticleController { PostMapping public ResultArticleVO create(RequestBody Valid ArticleCreateRequest request) { ArticleVO vo articleService.create(request); return Result.success(vo); } PutMapping(/{id}) public ResultArticleVO update(PathVariable Long id, RequestBody Valid ArticleUpdateRequest request) { ArticleVO vo articleService.update(id, request); return Result.success(vo); } }实际开发时我会在Service层加事务注解因为保存文章的同时还要同步文章和标签的关联关系任何一步失败都应该把整批操作回滚。这里有个新手极容易踩的坑先插文章拿到自增ID再插文章和标签的关联关系如果第二步抛异常而事务没生效就会留下脏数据。所以事务一定要加在“跨多张表操作”的方法上。3.3 前端页面从Markdown源码到渲染页面的路径前端我用的是Vite加Vue 3路由设计很简单/是首页列表/article/:id是文章详情/admin是后台管理。很多人做前端时容易陷入“拼命堆组件”的状态但我建议路由和页面结构尽量保持清晰一个页面只负责一个核心职责。首页是列表页它只需要请求接口拿到文章标题、摘要、发布时间然后渲染成卡片列表详情页才是重头戏因为它要拉取Markdown文本并完成渲染。Markdown渲染是整个详情页的核心链路我的前端实现逻辑可以分为四步先从后端拿到原始Markdown字符串然后调用marked或者markdown-it把它解析成HTML再经过DOMPurify做白名单过滤最后交给highlight.js处理代码块高亮插入页面DOM。这四步缺一不可。为什么必须加DOMPurify这一步因为markdown允许你写原始HTML标签如果某个用户写了恶意脚本你直接把渲染结果放进页面里那就是一个现成的XSS攻击入口。个人博客如果只有自己发文章风险还小一旦开放评论功能这类过滤就是硬性门槛。下面这段代码就是我项目里封装的渲染函数核心逻辑一目了然import { marked } from marked; import DOMPurify from dompurify; import hljs from highlight.js; function renderMarkdown(raw) { const dirtyHtml marked.parse(raw); const cleanHtml DOMPurify.sanitize(dirtyHtml); const container document.createElement(div); container.innerHTML cleanHtml; container.querySelectorAll(pre code).forEach((block) { hljs.highlightElement(block); }); return container.innerHTML; }这段逻辑放在浏览器端执行是有代价的用户打开文章时首先要等接口返回数据然后在浏览器里做一次完整解析。如果想要首屏更快可以后续做成服务端渲染或者在后端预先把渲染结果缓存下来。但我第一次做的版本选择了浏览器端渲染因为它最容易调试、最直观也足够满足个人博客的访问压力。3.4 用户系统与评论博客的互动层用户系统和评论功能放在一起做是因为评论天然依赖登录信息。注册登录的界面别做得花里胡哨用户名、邮箱、密码三个字段就够了剩下的核心都在后端逻辑里。注册时先判断用户名和邮箱是否已经存在然后对密码做哈希处理最后插入用户表。登录时根据用户名找到用户用BCrypt校验密码校验通过后生成JWT把它写入HttpOnly Cookie。评论功能的难点反而不在存储而在数据关联。评论表里需要存文章ID、用户ID、评论内容、父评论ID和创建时间。评论列表加载时要根据文章ID先查出所有评论再把评论和用户头像、用户名绑定起来如果评论量大了还要处理分页和无限嵌套。我的第一版只做了单层评论没有做嵌套回复因为嵌套评论的数据结构复杂度会明显上升。对于个人博客单层评论加一个时间排序完全够用。写评论接口时我特别提一个点一定要做用户身份校验而不是把用户ID放在前端传过来的参数里。也就是说后端从Cookie或请求头里解析出当前登录用户而不是信任请求体里的userId字段否则别人可以伪造参数替任何人发评论甚至删评论。这种身份主体应从会话上下文中拿而不是从可变的请求参数里拿属于全栈接口设计的基础素养。4. 上线部署与性能优化本地跑通只是开始4.1 部署方案对比服务器、Docker还是平台挂载本地跑通项目以后很多人会松一口气但对我们做全栈的人来说真正让系统变成完整项目的是把它部署到公网让朋友或者读者可以访问。部署方案我体验过三种直接在服务器上用Nginx加进程守护跑前后端、用Docker Compose编排所有服务、用云平台直接托管。三种方式适合不同阶段没有绝对好坏。如果你只有一台轻量云服务器并且想尽快看到效果最省事的方式是后端应用打成jar包在服务器上用systemd托管前端构建后的静态文件交给Nginx直接托管。前端页面里所有API请求走相对路径Nginx再把特定路径反向代理到后端端口。这套方案的好处是非常直观任何一部分出问题都能直接查看日志和进程状态。缺点是以后想迁移环境得重新配置一遍。如果你和我一样玩过一阵之后嫌手动配置太繁琐可以切到Docker Compose。用一份docker-compose.yml把MySQL、Redis、后端、前端Nginx全部编排起来服务器上一条命令启动全部服务迁移和备份都很舒服。Docker的缺点是有学习成本但你一旦习惯了镜像和容器部署项目就变得像搭积木一样。我的建议是先用手动部署理解每一层发生了什么再上Docker这样踩坑的时候你能准确判断问题在应用层还是容器层。无论用哪种方式有几个通配的部署细节必须注意数据库的密码不要硬编码在代码里要用环境变量管理HTTPS证书可以通过Let‘s Encrypt免费申请配置好自动续期公网服务器上的防火墙策略必须收敛只暴露80、443和SSH端口。这些细节决定了你的博客能不能稳定跑上一年而不出幺蛾子。4.2 我踩过的性能坑静态资源、缓存、数据库连接个人博客在低并发下基本不会出现性能问题但有两个机制性的坑容易让项目显得很业余。第一个是静态资源全都走后端应用。前端构建出来的几个MB的JS文件、CSS文件如果都经过后端应用读取再返回应用每次都要消耗线程处理这些IO并发一大马上变慢。正确方案是让Nginx直接托管前端静态目录图片也尽量走单独路径或者对象存储后端只负责输出JSON接口。这一条改动对响应速度的提升几乎是立竿见影的。第二个坑是N1查询。文章列表页里每篇文章要显示几个标签初学者很可能写成一个循环先查文章列表再根据文章ID一条条查标签。如果一页有十篇文章数据库就要执行十一条查询。文章少的时候无感文章一旦过百页面延迟会非常明显。我的做法是把文章的ID收集起来用一条IN查询把所有文章的标签查出来然后在内存里完成组装把数据库查询次数从十一降到了二。这个思路在所有列表开发里都通用。除了这两个问题我还在第一版里加入了Redis缓存文章详情页的渲染结果缓存十分钟热门标签列表缓存一小时。设置缓存时要注意缓存失效和文章更新的同步否则用户编辑完文章之后页面还是旧内容体验会非常割裂。最简单的方案是文章更新接口成功时主动删除对应缓存下次有人访问时再重新生成。缓存这东西用不好比不用还糟糕宁可先别急着加。5. 常见问题与排查技巧实录5.1 我遇到过的四个疑难问题速查表做全栈博客过程中有些问题非常典型单独靠报错信息往往很难定位原因。这里我把自己遇到过的以及身边朋友做类似项目时反复踩的坑整理成一张速查表建议你直接保存下来现象产生原因解决办法首页刷新后404前端用了History路由Nginx没有配置fallbackNginx中添加try_files指令让未匹配路径回退到index.htmlMarkdown里代码块没有高亮highlight.js初始化时没有扫描动态渲染的内容渲染完成后调用hljs.highlightElement或者使用vue等框架的钩子图片上传后访问报404静态资源映射路径和实际存储路径不一致检查后端是否注册了本地或对象存储的访问映射以及权限登录后接口偶尔报401JWT过期时间设置太短或服务端时钟不一致明确token有效期并在前端响应拦截器里做统一刷新处理评论里中文显示乱码MySQL表或连接字符串字符集不对数据库统一设置为utf8mb4JDBC连接串加characterEncodingUTF-8这里我想单独说一讲首页刷新404这个坑因为它真的会让很多人卡住至少一天。当你使用Vue Router的History模式时浏览器访问/article/1服务器看到的请求路径是一个具体路径而服务器上其实并没有这个文件所以返回404。Nginx需要把这类路径全部映射到前端入口文件location / { try_files $uri $uri/ /index.html; }加了这条之后前端的路由才会重新接管URL。你这个坑如果不提前知道等部署之后再排查会很容易摸不着头脑。5.2 安全与健壮性评论区和文件上传的隐性坑安全话题在个人项目里特别容易被忽略但一旦出事就是大事。我见过不少人做博客系统开放评论功能之后完全没有任何过滤直接把用户提交的内容用v-html塞进页面这等于给攻击者开了一扇大门。我再次重申所有用户产生的内容在插入页面之前都要做白名单清洗。DOMPurify之类的库能处理绝大多数XSS攻击但前提是你真的用了它并且用到用户输入的入口上。文件上传也是重灾区。博客后台难免要传头像、传图片最常见的错误是前端只判断了文件后缀名然后私自把文件存到一个谁都读得到的目录。攻击者可能会上传一个伪装成图片的脚本文件一旦被服务器解析就会出大问题。我的做法是文件扩展名和服务端读取出的MIME Type双重校验只允许jpg、png、webp这几种文件内容不强信任文件名一律由服务端重新生成随机字符串加时间戳上传目录禁止执行脚本。这样即使有人强行上传了恶意文件服务器也不会把它当代码去执行。还有一个没有写在速查表里的问题就是数据库连接池耗尽。很多人做全栈项目时直接用配置文件的默认连接池参数但个人项目虽然流量小后台日志打印、定时任务可能会把连接池占满导致正常访问时连接超时。排查方法很简单如果后台日志频繁出现“Connection is not available”之类的字样就要检查连接池最大连接数设置同时可以通过临时调大的方式来验证是不是这个原因。尤其在开发模式下建议把接口层的重要操作日志打印出来比如谁登录了、谁删了文章、谁上传了文件。这些日志平时看着没用一旦服务器被攻击或者数据异常就是找问题的最快线索。6. 写在最后的一点个人建议做完全栈博客系统并上线之后我最大的感受是一个项目能不能真正体现你的水平更多取决于你如何处理边界情况而不是你写出了多少行代码。很多人在简历里写“独立开发全栈博客系统”但实际项目里没有做XSS过滤、没有处理Nginx刷新404、没有考虑JWT存储的安全位置这些细节面试官一问一个准。你如果在做这个项目时把这些隐藏问题都解决到位整个项目的含金量会完全不一样。从功能角度我觉得下一步可以继续补全文搜索、自动备份和访问统计。搜索如果需要好的中文分词可以试试Meilisearch或者OpenSearch数据量大一点的时候很有用自动备份用定时脚本或者云数据库的备份策略都很成熟访问统计如果用第三方平台可能会被广告拦截器过滤掉最好自己做一个埋点接口数据掌握在自己手里。这些扩展方向每一个都值得单独深入研究但前提是先让第一版完整、稳定地跑起来。最后分享一个很多教程不会说的心得给博客好好写README、画一张简单的架构图然后把它部署到公网放上自己的几篇原创文章这个过程本身才是全栈项目真正的体验。你不需要在V1.0里加入所有想象中应该在的东西做出一个能用、够安全、结构清晰的产品然后让它不断生长这就已经很好了。
返回列表