给博客加功能时,我踩过的三个反模式
写脚本改代码、凭记忆猜 API、把"能编译"当"能用"——这三个错误我都犯过,而且都花了一轮才修好。
技术
最近给博客做了一轮功能迭代,加了命令面板、灯箱、滚动动效、长文折叠、联系页之类的东西。
技术上都不复杂,但过程中我犯的几个错误挺典型的,写出来给自己提个醒。
反模式一:用脚本批量改已有代码
出了什么事
我写了个 Node 脚本,想给四个列表页的行元素统一加一个 CSS 类:
const targets = [
["src/pages/archive.astro", 'class="ar-row"', "ar-row rv"],
// ...
];
src = src.split(from).join(to);
看起来很合理对吧。问题是我把 from 和 to 都写成了裸字符串:
class="ar-row" → ar-row rv
结果是生成了 <a ar-row rv href="..."> ——属性名和属性值混在一起,HTML 直接失效。
类型检查报了一堆我看不懂的错:
Type '{ children: any[]; "ar-row": true; rv: true; href: string; }'
is not assignable to type 'AnchorHTMLAttributes'
我盯着看了半天才反应过来。
为什么这类脚本很危险
因为它绕过了你的视觉检查。你脑子里想的是“给元素加个类”,但脚本实际做的是“字符串替换”——这两件事在脑子里是等价的,在文本上完全不是。
而且一旦出错,报错信息会指向一个你完全没想到的地方。
现在的做法
已有文件一律用编辑器精确修改,脚本只用于新建文件。
判断标准很简单:
- 新建文件 → 随便写,写错了整个文件重写
- 改已有文件 → 必须能精确指出「哪几行、变成什么样」
如果确实需要批量改,我会先在一个文件上跑,验证产物正确,再推广到其他文件。而且改完立刻 astro check,不要攒着。
反模式二:凭记忆猜 API
出了什么事
我给命令面板写了个 escapeHtml 函数,用的是这种写法:
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return {
&: "&",
<: "<",
>: ">",
'"': """,
"'": "'",
}[c];
});
}
这是我“记得”的常见写法。写完我甚至没用它——因为 pagefind 返回的 HTML 片段需要保留 <mark> 高亮,我后来决定直接 innerHTML。
但函数已经写进去了。
构建时报错:
[CompilerError] Unexpected token
对象字面量里的裸标识符作键(&、<、>)在 ES5 严格模式下是语法错误——需要写成 "&"。
教训
没用的代码也要能编译。
我当时的逻辑是“反正不调用,语法错误也无所谓”。但它是错的:
- 语法错误会让整个文件解析失败,连带周围的正常代码也编译不了
- 下一个人(比如三个月后的我)会疑惑“这函数为什么在这儿”
- 它证明了这段代码没被真正执行过,也就意味着没被真正思考过
现在的做法:写完立刻检查有没有未使用的函数,有就删。
顺带一提,这次经历让我明白一个更普适的道理——
你“记得”的 API,可能来自另一个语言、另一个版本,或者你根本没真正用过。
与其凭记忆写,不如先查一眼,或者干脆换个更简单的实现。
比如这次我就该直接不写 escapeHtml,而不是“先写上以防万一”。
反模式三:把“能编译”当“能用”
出了什么事
我给移动端加了个固定顶栏,里面放品牌名 + 导航链接 + 抽屉按钮。
样式我是这么写的:
.su-mobile-bar-brand {
padding-left: 44px; /* 给抽屉按钮让位 */
}
@media (max-width: 520px) {
.su-mobile-bar-brand { display: none; }
}
我以为没问题。构建通过,类型通过,astro check 通过。
但 520px 以下品牌名一隐藏,那个 44px 的占位也跟着没了,按钮下方会留出一段空白。
编译器和类型检查都不会发现这种问题——因为 CSS 规则本身完全合法。
怎么发现的
我拖了一下浏览器窗口。
但我不可能每次都靠肉眼看。所以后来我写了个检查脚本:
// 检查移动端顶栏的结构完整性
const hasBar = html.includes("su-mobile-bar");
const hasTrigger = html.includes("su-mobile-trigger");
const hasBrand = html.includes("su-mobile-bar-brand");
虽然这个脚本也发现不了“留白”这种问题(它只检查元素存在与否),但它至少能挡住“忘记加某个元素”这类失误。
更根本的做法
我现在会在写 CSS 时问一句:
这条规则在所有断点下都成立吗?
上面那个例子的正确写法应该是:
@media (max-width: 520px) {
.su-mobile-bar-brand { display: none; }
/* 品牌名消失了,让位由顶栏自己承担 */
.su-mobile-bar { padding-left: 60px; }
}
把隐含的依赖写出来,而不是指望它自动成立。
三件事的共同点
回看这三个错误,我发现它们有个共同点:
| 错误 | 我以为在做什么 | 实际在做什么 |
|---|---|---|
| 脚本改类名 | 给元素加个类 | 字符串替换 |
| 写 escapeHtml | 写个工具函数 | 写了一段不会执行的代码 |
| 顶栏 padding | 让开按钮位置 | 引入了一个隐含依赖 |
每一次,我的意图和动作之间都有一层偏差。
而编译器、类型检查、构建脚本,全都只能检查“动作”,检查不了“意图”。
所以最后还是要靠:
- 动作完成后立即验证(哪怕只是拖一下浏览器窗口)
- 验证不通过时先怀疑自己的意图,而不是工具
- 写完问一句「这段代码真的会被执行吗 / 在所有情况下都成立吗」
最后
这三个错误都不复杂,加起来浪费了我大概两小时。
但它们有个共同特征:都是在我“以为很简单”的时候发生的。
越是觉得“这有什么难的”,越要多花十秒钟确认一下我到底在做什么。
这个习惯在写博客功能时如此,在写那些看起来很厉害的教程时——更应该如此。