上周五晚上10点,我正躺沙发上刷手机,突然收到服务器报警——博客挂了。赶紧爬起来一看,Nginx日志里全是502,PHP-FPM进程卡死,数据库连接池满了。
排查了半小时,发现是评论区有人刷垃圾广告,一晚上发了2000多条,直接把MySQL连接数打满了。我一边清数据一边想,这破博客从上线到现在快两年了,功能还是最原始那一套,连个验证码都没有,不被打才怪。
干脆趁这个机会,一口气把几个一直想加的功能全干了。花了3天,加了3个东西:图片懒加载、文章目录锚点、评论区验证码。今天上线,流量直接翻了倍——当然也可能是因为我11天没发文,读者以为我跑路了。
先说第一个:图片懒加载。
我之前写文章喜欢堆截图,一个技术教程动不动贴十几张图。问题是首页加载的时候,所有图片一起请求,首屏渲染慢得要命。Chrome DevTools里看了眼,首屏加载时间4.7秒,LightHouse评分直接给了个56。
方案用的是IntersectionObserver API,这个IE11不支持,但我的博客用户里Chrome占了87%,Firefox 9%,Safari 3%,剩下的1%我决定放弃。代码很简单,就几十行:
“`javascript
const images = document.querySelectorAll(‘img[data-src]’);
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute(‘data-src’);
observer.unobserve(img);
}
});
}, { rootMargin: ‘100px’ });
images.forEach(img => observer.observe(img));
“`
注意一个坑:rootMargin设了100px,意思是图片距离视口还有100px就开始加载,防止用户快速滚动时看到空白。我之前设了0,结果滚快了全是灰块,体验极差。
改完之后,首屏加载时间降到1.2秒,LightHouse评分直接蹦到89。效果立竿见影。
第二个功能:文章目录锚点。
我的技术文章动不动就5000字,之前有读者在后台留言说“看你的文章像看论文,没有目录根本找不到重点”。我心想你倒是说对了,我写的时候也没目录。
这次干脆用JavaScript自动解析文章里的h2、h3标签,动态生成一个侧边目录。实现思路是:页面加载完后,遍历所有标题元素,提取id和文本,构建一个树形结构,然后渲染到侧边栏。
核心逻辑:
“`javascript
function generateTOC() {
const headings = document.querySelectorAll(‘.post-content h2, .post-content h3’);
const toc = document.getElementById(‘toc’);
const list = document.createElement(‘ul’);
headings.forEach((h, index) => {
if (!h.id) {
h.id = `heading-${index}`;
}
const li = document.createElement(‘li’);
li.className = h.tagName === ‘H2’ ? ‘toc-h2’ : ‘toc-h3’;
const a = document.createElement(‘a’);
a.href = `#${h.id}`;
a.textContent = h.textContent;
li.appendChild(a);
list.appendChild(li);
});
toc.appendChild(list);
}
“`
这里踩了个坑:smooth scroll。默认的锚点跳转是瞬间跳过去的,用户会闪一下。我加了`scroll-behavior: smooth`,结果在Safari上无效。查了下,Safari直到15.4才支持这个CSS属性,老版本还是得用JS polyfill。我懒得搞,直接用了`element.scrollIntoView({ behavior: ‘smooth’ })`,兼容性好一些。
还有个细节:目录要跟着滚动高亮当前章节。我用IntersectionObserver监听每个标题,当某个标题进入视口时,给对应的目录项加一个active类。这个实现比监听scroll事件性能好太多,不会触发频繁的重排。
第三个功能:评论区验证码。
这个是最让我头疼的。之前被刷垃圾评论搞到服务器宕机,这次必须上验证码。但我不想用Google reCAPTCHA,原因有二:一是国内用户访问Google服务不稳定,经常验证码刷不出来;二是Google那套东西太臃肿,为了一个验证码加载好几MB的JS。
自己写一个简单的数学验证码,生成两个随机数,让用户算和。PHP端实现:
“`php
session_start();
$num1 = rand(1, 10);
$num2 = rand(1, 10);
$_SESSION[‘captcha_result’] = $num1 + $num2;
// 返回给前端用于显示
echo json_encode([‘num1’ => $num1, ‘num2’ => $num2]);
“`
前端提交评论时,附带用户输入的结果,后端对比:
“`php
if ((int)$_POST[‘captcha’] !== $_SESSION[‘captcha_result’]) {
die(json_encode([‘error’ => ‘验证码错误’]));
}
“`
看起来简单,但有个坑:session并发问题。如果用户在同一个浏览器开两个标签页,两个页面的验证码请求会覆盖同一个session变量,导致验证失败。解决方案是在生成验证码时,用一个唯一标识关联验证结果,而不是直接存session。我用的是当前时间戳+用户IP的md5作为key,存到redis里,5分钟过期。
“`php
$key = md5(time() . $_SERVER[‘REMOTE_ADDR’]);
$redis->setex(‘captcha:’ . $key, 300, $num1 + $num2);
// 返回key给前端,提交时一起带回来
echo json_encode([‘num1’ => $num1, ‘num2’ => $num2, ‘key’ => $key]);
“`
提交时拿这个key去redis里取结果对比。这样多个标签页互不干扰。
上线后跑了24小时,垃圾评论从每天几百条降到了0。完美。
最后说点踩坑经验。
图片懒加载的坑:别忘了给img标签加width和height属性,不然图片加载时页面布局会抖动。我一开始没加,懒加载生效后图片突然撑开,整个页面内容往下跳,体验极差。
目录锚点的坑:如果文章内容是通过AJAX动态加载的,要在内容渲染完成后才执行目录生成函数。我一开始在DOMContentLoaded事件里调用了,结果内容还没渲染完,h2元素数量是0,目录生成了个寂寞。
验证码的坑:要防止暴力破解。我虽然用的是数学题,但如果攻击者写脚本不断尝试,10以内的加法最多19种组合,分分钟爆破。所以我在后端加了频率限制:同一个IP 10秒内最多提交3次评论,超过就返回429。用redis计数器实现:
“`php
$ip = $_SERVER[‘REMOTE_ADDR’];
$count = $redis->incr(‘rate_limit:’ . $ip);
if ($count === 1) {
$redis->expire(‘rate_limit:’ . $ip, 10);
}
if ($count > 3) {
http_response_code(429);
die(json_encode([‘error’ => ‘提交太频繁,请稍后再试’]));
}
“`
这次改完,博客总算像个正经产品了。之前一直觉得“功能够用就行”,结果被人刷到宕机才被迫升级。下次再有新想法,我可能不会再拖两年了。
📎 延伸阅读
看完这篇,如果你想:
- 直接拿工具 → 回复”13“,我把跨境获客工具包发给你
- 系统学习 → 点击菜单”AI训练营”,从0开始跑通AI变现