LegspCpd Blog

给博客加功能时,我踩过的三个反模式

写脚本改代码、凭记忆猜 API、把"能编译"当"能用"——这三个错误我都犯过,而且都花了一轮才修好。

技术

· 约 1 分钟

最近给博客做了一轮功能迭代,加了命令面板、灯箱、滚动动效、长文折叠、联系页之类的东西。

技术上都不复杂,但过程中我犯的几个错误挺典型的,写出来给自己提个醒。

反模式一:用脚本批量改已有代码

出了什么事

我写了个 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 {
      &: "&amp;",
      <: "&lt;",
      >: "&gt;",
      '"': "&quot;",
      "'": "&#39;",
    }[c];
  });
}

这是我“记得”的常见写法。写完我甚至没用它——因为 pagefind 返回的 HTML 片段需要保留 <mark> 高亮,我后来决定直接 innerHTML。

但函数已经写进去了。

构建时报错:

[CompilerError] Unexpected token

对象字面量里的裸标识符作键(&、<、>)在 ES5 严格模式下是语法错误——需要写成 "&"。

教训

没用的代码也要能编译。

我当时的逻辑是“反正不调用,语法错误也无所谓”。但它是错的:

  1. 语法错误会让整个文件解析失败,连带周围的正常代码也编译不了
  2. 下一个人(比如三个月后的我)会疑惑“这函数为什么在这儿”
  3. 它证明了这段代码没被真正执行过,也就意味着没被真正思考过

现在的做法:写完立刻检查有没有未使用的函数,有就删。

顺带一提,这次经历让我明白一个更普适的道理——

你“记得”的 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 让开按钮位置 引入了一个隐含依赖

每一次,我的意图和动作之间都有一层偏差。

而编译器、类型检查、构建脚本,全都只能检查“动作”,检查不了“意图”。

所以最后还是要靠:

  1. 动作完成后立即验证(哪怕只是拖一下浏览器窗口)
  2. 验证不通过时先怀疑自己的意图,而不是工具
  3. 写完问一句「这段代码真的会被执行吗 / 在所有情况下都成立吗」

最后

这三个错误都不复杂,加起来浪费了我大概两小时。

但它们有个共同特征:都是在我“以为很简单”的时候发生的。

越是觉得“这有什么难的”,越要多花十秒钟确认一下我到底在做什么。

这个习惯在写博客功能时如此,在写那些看起来很厉害的教程时——更应该如此。