简介:一套面向JavaWeb初学者的个人博客系统全流程学习包,覆盖需求分析、架构设计、SSM框架实现、数据库设计与安全防护等关键环节,适合正在做课程设计或希望提升实战能力的开发者。压缩包共19个文件,大小44.52MB,核心内容以源代码zip、论文doc、答辩PPT、数据库sql脚本及项目截图为主,另有Java学习路线pdf与辅导视频入口,可按目录从入门到实现逐步对照学习。设计实现中重点演示了Servlet、JSP、JDBC以及Session/Cookie等技术的配合使用,并融入MVC分层思想与SQL注入防范措施,前沿模板引擎与前端框架的引入也有清晰讲解。已有761人学习下载,这套资料既适合作为毕业设计或课程项目的参考范本,也能帮助系统梳理JavaWeb开发全流程。
1. 个人博客系统的设计与实现,到底交付什么才能算“能用”
看到「个人博客系统的设计与实现.zip」这个标题,多数人第一反应是找份源码改个名就能交差。但个人博客系统这个方向,真正卡人的不是代码量,而是这套东西能不能跑通、能不能改、能不能扛得住线上访问。一个能交付的博客系统,前端要管文章渲染和后台编辑,后端要管登录鉴权和数据持久化,中间还夹着打包部署、跨浏览器兼容这些看不见的环节。做我们这行的不会只盯着 zip 里几个类文件看,而是把它拆成一份可复用的工程:技术栈怎么选、表怎么建、接口怎么定,这些设计决策才是 zip 里最值钱的部分。这篇文章我按自己做小型博客服务的一贯路子,把从技术选型到打包上线的完整路径讲清楚——拿到任何一份个人博客系统源码,你都能在半小时内判断出它的成色,并且知道该从哪个文件开始下手改。
2. 个人博客系统的技术选型:基于 Spring Boot + Vue 的前后端分离方案
2.1 从 JSP 单体到前后端分离:为什么我选择 Spring Boot + Vue
个人博客系统这个题目,网上能找到的旧教程大半用的是 JSP + Servlet,或者 Spring Boot 直接渲染 Thymeleaf 模板。这种单体渲染的方式不是不能跑,而是改起来非常难受:前端调整一个按钮位置,得重新编译整个 war 包;接口和页面混在一起,后端加需求很可能顺带手写一堆 JS。等到博客需要加移动端适配、加小程序端、加后台管理端的时候,同一个后端服务承担多端输出,模板渲染方案的维护成本会迅速失控。
所以我在“设计与实现”里更推荐前后端分离:后端只用 Spring Boot 写 REST API,返回 JSON;前端用 Vue 独立构建,打包成静态文件后交给 Nginx 托管。这样后端只要保证接口稳定,前端页面怎么换都不影响服务逻辑,跨浏览器适配时也只动前端的活儿。
选择 Spring Boot + Vue,还因为这是个人博客系统这种规模里性价比最高的组合。Spring Boot 自带内嵌 Tomcat、自动装配和健康检查,省去大量 XML 配置;Vue 的组件化开发让文章列表、标签云、评论框这些模块天然互相独立。真要说设计模式,这套组合本身就藏着不少经典模式的味道——Controller 层是模板方法,Service 层按业务拆策略,MyBatis-Plus 的 BaseMapper 算是对泛型模板模式的封装。类似的项目写多了你会发现,设计模式的 Java 实现很多时候不是单独写几个抽象类,而是框架替你把这层抽象做好了。
如果你想做的博客只是个人写作、不在乎后台交互,那完全没必要上这套组合,直接用 Hexo、Hugo 这类纯静态生成器更好,零部署成本。但如果你要的是一个能演示登录注册、文章发布、后台管理完整链路的系统,前后端分离的 Spring Boot + Vue 方案是最稳的落脚点。
2.2 数据库设计:文章、分类、标签、评论四张核心表的字段与索引
个人博客系统的数据模型并不复杂,但我见过很多项目把文章、分类、标签全塞进一张表里,最后查询和统计都做得非常别扭。我一般会按职责拆成四张核心表:用户表、文章表、分类表、标签表,再配两张关联表处理多对多关系。
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录名', `password` varchar(100) NOT NULL COMMENT 'BCrypt 加密后的密码', `nickname` varchar(50) DEFAULT '' COMMENT '展示昵称', `avatar` varchar(255) DEFAULT NULL, `role` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1=普通用户 2=管理员', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `article` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL COMMENT '文章标题', `summary` varchar(500) DEFAULT NULL COMMENT '列表页摘要', `content` longtext NOT NULL COMMENT 'Markdown 原文', `cover_image` varchar(255) DEFAULT NULL, `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0=草稿 1=已发布 2=回收站', `view_count` int(11) NOT NULL DEFAULT '0' COMMENT '阅读量', `category_id` bigint(20) DEFAULT NULL, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category_status` (`category_id`, `status`), KEY `idx_status_create_time` (`status`, `create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章表';这两个表有一个细节:password 字段存的是 BCrypt 密文,不是明文也不是简单的 MD5。登录认证时用 Spring Security 的 BCryptPasswordEncoder 去比对,这样数据库泄露后用户密码不会直接暴露。article 表里存的是 Markdown 原文,不是渲染后的 HTML,这个选择后面讲 Markdown 链路时再展开。
文章和标签之间是多对多关系,需要单独建一张关联表:
CREATE TABLE `article_tag` ( `article_id` bigint(20) NOT NULL, `tag_id` bigint(20) NOT NULL, PRIMARY KEY (`article_id`, `tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签关联表';为什么不能用 JSON 字段直接存标签列表?因为标签云和按标签筛选文章这两个功能都需要对标签做聚合查询,JSON 字段在 MySQL 里没法走索引。关联表配合 JOIN 或 EXISTS 查询,实现成本远低于维护冗余字段。
2.3 zip 包解压后的工程目录:后端、前端、SQL、部署四件套
当你拿到一份个人博客系统 zip 并解压后,第一步应该是看目录结构是不是前后端分离的。我自己整理这类工程时习惯用一个固定模板:
personal-blog/ ├── blog-backend/ // Spring Boot 后端工程 │ ├── src/ │ ├── pom.xml │ └── target/ ├── blog-frontend/ // Vue 前端工程 │ ├── src/ │ ├── package.json │ └── dist/ ├── sql/ │ ├── init.sql // 建库建表 │ └── data.sql // 测试数据 ├── deploy/ │ ├── nginx.conf // 线上 Nginx 配置 │ └── start.sh // 一键启动脚本 └── README.md这个结构的价值在于,拿到手的人能在三分钟内分清“改后端代码去哪、改页面样式去哪、怎么初始化数据库”。很多网上流传的源码包把前后端混在一个工程里,甚至把 node_modules 也打进了 zip,压缩包动辄几百兆,解压还慢——这不是体积问题,而是工程边界没划清楚。后端负责业务接口,前端负责页面交互,SQL 单独放一份,README 里写清启动步骤,这才算是一份能直接复现的交付物。
对于“设计与实现”型的课程设计或毕设项目,文档往往比代码更重要。我会在 README 里写清楚每个模块的职责、接口列表、数据库初始化顺序。后面部署时你会发现,文档里一句“先用 sql/init.sql 建库”,比代码里任何注释都省时间。
3. 后端核心模块:登录鉴权、文章发布、缓存与查询的实现链路
3.1 JWT 登录鉴权:拦截器 + Redis 校验,把“登录”做成可踢出的状态
个人博客系统的登录模块,看起来就是用户名密码比对,但这里有一个新手最容易翻车的问题:使用无状态 JWT 后,用户改密码或管理员封号,已经发出去的 token 依然有效,直到过期。所以我的习惯是 JWT 只承载用户身份和有效期,真正的登录状态在 Redis 里再存一份。
@Component public class JwtInterceptor implements HandlerInterceptor { private static final String SECRET_KEY = "change-this-secret"; private static final long EXPIRE_MINUTES = 120; @Autowired private StringRedisTemplate redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求,否则前端跨域请求第一次会直接失败 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } if (!(handler instanceof HandlerMethod)) { return true; } String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); } try { Claims claims = Jwts.parser() .setSigningKey(SECRET_KEY.getBytes(StandardCharsets.UTF_8)) .parseClaimsJws(token) .getBody(); String username = claims.get("username", String.class); // 核心:Redis 里没有这个 token 说明已被踢下线 String cachedToken = redisTemplate.opsForValue().get("login:token:" + username); if (cachedToken == null || !cachedToken.equals(token)) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } request.setAttribute("username", username); return true; } catch (Exception e) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } } }这段逻辑有几个关键点。第一,SECRET_KEY 不能写死在这种硬编码里,正式项目要放进配置中心或环境变量,这里只是演示。第二,每次请求都查 Redis 会多一次网络开销,但博客系统的 QPS 一般不高,换来的好处是可以在用户修改密码时直接删除 Redis 里的 token,实现单端登录或强制下线。第三,OPTIONS请求一定要放行,否则浏览器发起跨域请求时会被拦截器挡在门外,前端显示“请求失败”,后端却连日志都没打。
登录接口本身用 BCryptPasswordEncoder 比对密码,通过后生成 JWT 并在 Redis 中写入一个不过期的 key,value 就是 token。这个“JWT 无状态 + Redis 有状态”的组合,是我见过个人博客系统里兼顾设计感和可维护性的做法。
3.2 文章发布的 Markdown 链路:前端存原文、后端出 HTML
文章发布是整个系统的核心链路。前端编辑器写 Markdown,提交给后端时存原文;前端展示时,由后端把 Markdown 渲染成 HTML。为什么不能前端直接渲染后再提交?因为不同编辑器的渲染结果不一致,而且内容一旦入库,后续想换渲染引擎就要重刷全部数据。
后端渲染我一般用 commonmark-java,依赖少解析快:
public class MarkdownService { private final Parser parser = Parser.builder().build(); private final HtmlRenderer renderer = HtmlRenderer.builder().build(); public String render(String markdown) { Node document = parser.parse(markdown == null ? "" : markdown); return renderer.render(document); } }注意这里只做了渲染,没有做 XSS 过滤。用户一旦可以在文章里写<script>标签,博客就变成了存储型 XSS 的温床。所以在 render 之后,我会再走一层白名单过滤,把script、iframe、javascript:这类危险内容直接剔除。具体做法是用 jsoup 的Jsoup.clean(html, Safelist.basicWithImages()),把允许的标签和属性明明白白列出来,而不是黑名单式过滤。
文章列表页不需要完整 HTML,所以 article 表里的content字段保持了 Markdown 原文,summary字段在发布时由前端截取前 200 字,后端的渲染服务只在详情页被调用。这里有一个小技巧:把渲染结果按文章 ID 做缓存,文章更新时手动删除缓存,详情页的响应时间能从几十毫秒降到个位数。
3.3 用 Redis 缓存文章详情:穿透、一致性、刷新的落地策略
个人博客系统的读多写少,首页和详情页的浏览量占了绝大多数请求。数据库不经过任何缓存直接扛流量,MySQL 的 CPU 和连接数会先报警。我一般会在文章详情接口上做一级缓存,key 设计成article:detail:{id},value 是 JSON 字符串。
public ArticleDetailVO getArticleDetail(Long id) { String cacheKey = "article:detail:" + id; String cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return JSON.parseObject(cached, ArticleDetailVO.class); } Article article = articleMapper.selectById(id); if (article == null) { // 防空缓存 5 分钟,防止恶意请求穿透打到数据库 redisTemplate.opsForValue().set(cacheKey, "", 5, TimeUnit.MINUTES); return null; } ArticleDetailVO vo = convert(article); redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(vo), 30, TimeUnit.MINUTES); return vo; }这里有三个落地的细节。第一,缓存空值要设置短过期时间,原因是为了防穿透;如果直接用 null 返回,一个不存在的文章 ID 被反复请求,每次都会打到数据库。第二,更新文章时一定要主动删除缓存而不是等待过期,否则用户改了标题后旧内容最长 30 分钟不生效。第三,缓存预热可以放在启动后异步做,把已发布文章的前 200 条 ID 读进缓存,避免上线瞬间的缓存雪崩。这套策略对个人博客级别的流量完全够用,再往上才需要考虑多级缓存和消息队列异步更新。
4. 前端工程与跨浏览器支持:博客页面兼容性的设计与实现
4.1 Vue Router 路由设计:前台、后台、详情页的职责边界
前端工程如果一开始不规划路由,后面会越写越乱。我习惯把页面分成三层:游客可见的前台views/home、需要登录的后台views/admin、和文章详情页views/post。
// blog-frontend/src/router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', name: 'Home', component: () => import('@/views/home/HomePage.vue'), meta: { title: '首页' } }, { path: '/post/:id', name: 'PostDetail', component: () => import('@/views/post/PostDetail.vue'), meta: { title: '文章详情' } }, { path: '/admin', component: () => import('@/views/admin/AdminLayout.vue'), meta: { requiresAuth: true }, children: [ { path: '', redirect: '/admin/posts' }, { path: 'posts', component: () => import('@/views/admin/PostList.vue') }, { path: 'publish', component: () => import('@/views/admin/PostEditor.vue') } ] } ] export default createRouter({ history: createWebHistory(), routes })路由按“内容展示 → 应用管理”分成两层,后台管理里再做二级路由,这是 Vue 项目最常用的组织方式。代码里requiresAuth这个 meta 字段会在全局前置守卫里配合登录状态做跳转,未登录用户访问/admin时自动弹回登录页。异步组件() => import()是性能关键:首页只加载首页代码,详情页代码在用户点击后才拉取,首屏体积能瘦身一半以上。
跨浏览器表现在哪里?路由历史模式存在一个经典坑:createWebHistory依赖 History API,但在部分旧浏览器会退化成刷新就 404。所以部署时 Nginx 必须把未匹配到的路径全部try_files回index.html,否则用户直接输入/post/12这个地址,服务器返回 404。这一段在部署章节会再碰见。
4.2 Markdown 编辑器与代码高亮:编辑器选型不要追求大而全
前端选择 Markdown 编辑器,我踩过不少坑。很多人一上来就选富文本编辑器,最终编辑体验并不好。个人博客的编辑器只需要做两件事:写 Markdown 文本、预览 HTML。我常用的是 vditor,或者是 marked + highlight.js 的组合,前者开箱即用,后者轻量可控。
<template> <div class="editor-container"> <textarea v-model="rawContent" @input="handleInput" class="editor-area"></textarea> <div class="preview-area" v-html="previewHtml"></div> </div> </template> <script setup> import { ref, computed } from 'vue' import { marked } from 'marked' import hljs from 'highlight.js' const rawContent = ref('') const handleInput = (e) => { rawContent.value = e.target.value } const previewHtml = computed(() => { return marked(rawContent.value, { highlight: (code, lang) => { if (lang && hljs.getLanguage(lang)) { return hljs.highlight(code, { language: lang }).value } return hljs.highlightAuto(code).value } }) }) </script>这是最简洁的编辑器实现,核心思想是“数据驱动预览”:textarea 的值变化后,computed 实时调用 marked 生成 HTML。要注意的是 highlight 函数里的.value,返回的是高亮后的 HTML 字符串,不是整个代码块;如果直接返回整个<pre>,页面里会嵌套两次标签,样式全乱。代码语言识别失败时用highlightAuto兜底,保证用户写一段没有标注语言的代码块也能正常高亮。
4.3 跨浏览器支持的四个翻车细节:不是所有浏览器都按标准来
个人博客系统的用户可能来自各种浏览器和系统版本,跨浏览器支持这块我吃过不少亏,印象最深的有四个细节。
第一个是 iOS Safari 对日期字符串的解析。new Date("2024-01-01 12:00:00")在 Chrome 里正常,在 Safari 里会返回 Invalid Date,因为 Safari 不认这种带空格的时间格式。解决方法是把时间统一转成 ISO 8601 格式2024-01-01T12:00:00Z,或者在展示层用 day.js 这类库做格式化,不要直接传原始字符串给浏览器解析。
第二个是 Flex 布局在旧版 Safari 上的表现。flex: 1在 iOS 13 以下的 Safari 里兼容性不稳定,最常见的问题是子元素被拉伸或压缩。我一般会给 flex 子项补上min-width: 0和flex-shrink: 1,并避免使用gap属性依赖,老浏览器不认 flex gap,需要用 margin 做兜底。
第三个是 CSS 变量在部分旧浏览器的兼容问题。博客主题的配色方案用 CSS 变量管理很方便,但如果在页面直接用var(--primary-color)做背景,在旧浏览器会变成透明或默认色。我的习惯是在:root里写一份默认值,并给关键样式保留一个不带 var 的 fallback 声明,比如background: #4a90d9; background: var(--primary-color);。
第四个是中文输入法组合输入问题。用户在搜索框或评论区输入中文时,拼音未上屏阶段就会触发input事件,如果这时候去请求搜索接口,会连续发送很多垃圾请求。需要监听到compositionend之后再做过滤逻辑,这是中文博客系统特有的、却最少被人提起的跨浏览器问题。
5. 博客系统打包部署与避坑:从 zip 到线上可访问
5.1 打包发布:前端 dist 与后端 jar 怎么合成一个 zip
开发完代码,下一步是打包。Spring Boot 的 Maven 打包命令很常规,关键在于前端产物和后端产物怎么落到同一个目录里,给用户一个清晰可部署的 zip。
# 1. 前端构建 cd blog-frontend npm install npm run build # 产物输出到 dist/ 目录 # 2. 后端打包 cd ../blog-backend mvn clean package -DskipTests # 产物 blog-backend.jar # 3. 汇总成分发目录 cd .. mkdir -p distribution cp -r blog-frontend/dist distribution/ cp blog-backend/target/blog-backend.jar distribution/ cp -r sql distribution/ cp deploy/nginx.conf distribution/ cp README.md distribution/ # 4. 压缩成 zip cd distribution zip -r personal-blog-system.zip .这段脚本里最容易被忽略的是第 3 步的 README 拷贝。很多人交付 zip 后,对方拿到手只会对着 jar 发愣,不知道 MySQL 密码改哪里、Redis 要不要启、前端资源怎么挂载。把部署说明和配置模板一起打进去,这个 zip 才真正有交付价值。
打包细节上,前端npm install可能会因为网速慢而失败,建议在package-lock.json存在的前提下直接用npm ci,它按锁定文件精确安装,速度更快、行为更稳定。后端如果用了application.yml里的本地配置,交付前最好检查有没有写死绝对路径或本机 IP。
5.2 Nginx 反向代理与 HTTPS:博客上线的最小配置
博客上线后不能直接把端口 8080 暴露给用户。我一般用 Nginx 做反代,让 80/443 端口对外,静态资源由 Nginx 直接返回,动态请求转发给 Spring Boot。
server { listen 80; server_name example.com; # 前端静态资源 location / { root /var/www/blog/dist; try_files $uri $uri/ /index.html; } # 后端 API 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; proxy_set_header X-Forwarded-Proto $scheme; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ { root /var/www/blog/dist; expires 7d; add_header Cache-Control "public, immutable"; } }try_files $uri $uri/ /index.html;这一行对应前面说的 Vue 路由历史模式,没有它,直接访问/post/1就是 404,有了它,所有未知路径都会回退到前端首页再由前端路由接管。/api/这里的proxy_pass末尾没有带路径,意思是完整保留/api/...前缀发给后端;如果后端没有/api前缀,需要改成proxy_pass http://127.0.0.1:8080/;,这个差别很容易看错,配错后前端会报 404 或 500。
HTTPS 现在几乎是个人博客的标配。用 Certbot 申请 Let's Encrypt 证书,Nginx 里加一段 443 的 server 块,把证书路径填进去,再把 80 端口做 301 跳转。个人博客流量不大,免费证书足够了,不必为付费证书纠结。
5.3 常见问题排查记录:端口、跨域、乱码、路由回退
这里是运行个人博客系统时最常遇到的问题清单,都是我在实际部署和帮别人排查时见过的。
现象一:后端启动报端口被占用原因:上次的 Java 进程没有退干净,或者内嵌 Tomcat 默认 8080 被其他服务占了。 解决:先lsof -i:8080找进程,确认是残留的 java 进程就kill -9杀掉;如果是配置冲突,改application.yml里的server.port。不要一上来就改端口,先确认残留进程。
现象二:前端页面能打开,登录接口跨域报错原因:前后端分离部署后,前端页面在 originhttp://blog.example.com,接口在http://127.0.0.1:8080,浏览器因为 CORS 策略拦截了响应。 解决:最简单是后端写一个全局 CORS 配置,用allowedOriginPatterns("*")配合allowCredentials(true),但生产环境我更推荐同域部署,让 Nginx 把/api/转发给后端,前端只跟同域名交互,浏览器就不会有跨域问题。
现象三:数据库写入中文变成问号原因:MySQL 连接串或表字段字符集不是 utf8mb4,或者serverTimezone设置不当。 解决:数据库连接串加?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai,并且建表时确认DEFAULT CHARSET=utf8mb4。查表不是乱码、写入才乱码,多数是连接串问题。
现象四:部署后 Vue 路由刷新变 404原因:createWebHistory在服务器端找不到对应路径。 解决:把try_files $uri $uri/ /index.html;加进 Nginx 配置里。如果用的是 Tomcat 部署前端,则要在 web.xml 配一个对应的 404 转发 Filter,或用 Hash 路由模式绕开。
现象五:Redis 连接失败导致登录不可用原因:Redis 服务没启动,或密码配置不正确。Spring Boot 启动时有时不会因为 Redis 挂掉直接退出,但登录接口就一定报错。 解决:先检查 Redis 是否在监听redis-cli ping,再核对 yml 里的spring.data.redis.host和password。个人博客我习惯把 Redis 的缓存降级做一层 try-catch,缓存挂了至少让写操作和登录不直接崩溃。
6. 上线后的进阶动作:SEO 预渲染与性能验证清单
6.1 SEO 预渲染:让搜索引擎拿到完整 HTML
Vue 这类 SPA 的页面在浏览器里正常,但搜索引擎爬虫不一定执行 JavaScript。个人博客如果想要被搜索引擎好好收录,最简单可靠的方案是预渲染。在构建时用prerender-spa-plugin或者vite-plugin-prerender把首页和文章详情页都生成静态 HTML,搜索引擎直接抓到最终内容,用户访问时也少一次白屏等待。
预渲染要求构建前知道需要预渲染的路由列表,这对文章博客是可行的,因为文章列表是从接口拉取的,可以在构建脚本里先请求一次接口拿到全部文章 ID,再拼进预渲染路由参数。这个动作放在 CI 里做,每次发布自动更新。如果不希望引入这套复杂度,退一步也要在index.html里把meta description和各文章的关键词写对,对收录有帮助。
6.2 性能验证清单:上线前必查的三件事
最后,在上线前我会跑一套最基础的性能验证清单,每次迭代都不省这步:
| 检查项 | 目标值 | 方法 |
|---|---|---|
| 首屏耗时 | 3G 网络下 < 3s | 浏览器 DevTools 网络面板模拟 Slow 3G |
| 接口响应 | 后端 API P95 < 300ms | 用 curl 循环请求 + 时间统计 |
| 静态资源体积 | 单文件打包后 < 250KB | npm run build产物分析 |
首屏慢,优先看是不是图片没压缩、有没有把整个组件库都打进了首屏包。接口慢,优先看有没有走缓存,没走缓存的接口是不是 SQL 缺索引。静态资源体积大,优先检查是不是把element-plus这类组件库全量引入了,按需引入能砍掉一半以上体积。
我自己的习惯是给详情页的图片加懒加载,列表页只展示摘要和封面图,正文图片延迟到滚动到附近再请求。这看起来是老生常谈,但对个人博客来说,图片往往就是体积大头。有次我只加了懒加载,首屏传输量直接降了 60%,体验提升非常明显。
博客系统做完之后,不要急着堆功能,先把这套验证清单跑一遍,把可见的性能问题清掉,再去考虑评论、点赞、多用户这些锦上添花的东西。希望帮到你。
本文还有配套的精品资源,点击获取