Chancy game dev notes · 全栈工程师

我的工具链里为什么没有 node_modules

一个 400 行的 Markdown 解析器换掉了四件依赖。这笔账什么时候划算、什么时候是自虐,以及我怎么用断言让它可信。

· 4 分钟读完

#tools #production

搭这个博客的时候我做了一个选择:不装任何 npm 包。整个站是一个 build.mjs(Markdown 解析 + 模板)、一个 serve.mjs(静态服务器)和一个 CSS 文件,加起来不到 700 行。

这不是"讨厌依赖"的姿态问题,是一笔算过的账。

我付了什么

一个自己写的 Markdown 解析器。它支持的语法就这么多:标题、段落、行内 code、粗斜体、删除线、链接、图片、有序/无序列表(含缩进嵌套)、表格(带对齐)、引用块、围栏代码块、分割线。不支持的东西我全都不写进文章。

这个代价是可接受的,因为需求上限是我自己控制的。个人博客的 Markdown 需求是"够用",不是"完备"。

我拿到了什么

  1. 没有依赖更新这件事。 个人博客最大的死因不是功能不够,是三年后 npm install 装不上了 —— 一个 transitive 依赖需要新 Node、一个构建插件改了 breaking change,然后你的博客就停在 2026 年。零依赖的静态站在五年后依然只需要一个 node build.mjs。
  2. 产物完全可读。 dist/ 里就是 HTML。出问题我直接看生成结果,而不是看 source map。
  3. 行为可控,包括那些很细节的。 中文段落换行不能插空格,所以我自己写了拼接规则(前后都是 CJK 字符就不加空格);英文换行按 Markdown 语义合并成一个空格。这种事交给通用 parser 就是"它怎么这样啊",自己写就是"两行代码"。
// 中文换行不插空格,英文换行插空格
const smartJoin = (a, b) => (a && CJK.test(a.slice(-1)) && b && CJK.test(b[0]) ? a + b : `${a} ${b}`);
  1. 安全边界是自己审过的。 javascript:、data: 这类协议的链接一律被改写成 #,代码块和正文全部走 HTML 转义。这两条都有断言盯着,不是我"觉得没问题"。

什么时候这算自虐

我给自己划了线,越线就用现成的包:

情况选择
需求有明确上限,且上限由我控制(Markdown 子集、静态站)自己写
需求会随外部规范扩张(CommonMark 全语法、GFM 扩展)用库
涉及正确性难的通用问题(日期、时区、加密、解析 cron)必须用库或用平台原生
需要别人来维护(同事接手、开源项目)用库,因为"我的实现"不能被别人读懂

第三行那条我踩过一次:时区。手写日期处理看着不难,直到有用户报告边界日期不对 —— 那类 bug 的成本远高于依赖成本。"我能写出来"不是理由,"我能不能测全"才是。

顺带,同样的判断我用在了语言运行时上:宁可升级本机 Node,用它原生的能力(node:fs/promises、node:http、fetch、顶层 await、--experimental-strip-types 那一类),也不为了跑 TS 先装四个包。这不是省磁盘,是让新人接手时环境只有一条要求:装个 Node。

让它可信的那部分才是重点

零依赖最大的质疑是"你怎么知道你的解析器没写错"。我的答案是把它变成可断言的东西。这个仓库里 node build.mjs --selftest 会跑一组用例:

ok   嵌套列表                ok   表格 + 对齐
ok   宽松列表保留段落        ok   代码块内的 HTML 被转义
ok   有序列表不被无序吃掉    ok   危险协议链接被中和
ok   行内代码里的星号不参与强调
ok   中文换行不插空格,英文换行插空格
ok   标题进目录且 id 唯一    ok   引用块可嵌列表
ok   front matter 与非法输入

写这几条断言的时候立刻抓出两个真 bug:嵌套列表的缩进在 dedent 时被吃掉(结果 - a / - b / - c 全变成同层,而且尾部重复输出),以及我用一个函数上的全局槽位传递解析游标 —— 递归解析嵌套列表时游标被子调用覆盖,外层循环就从错误的位置继续。第二个 bug 是"手写解析器"最典型的样子,而它只花了我一次断言就现形。

这就是我平时做事的习惯:能变成数据表和断言的东西,就不要留在"我看着没问题"这一层。 占位美术的点数和配色、牌面几何的高度带、Markdown 解析器的语法边界,全是同一类东西 —— 一旦没有断言,它们就只能在评审时靠人眼逐张核对,而那是最贵的核对方式。

值不值

对这一个博客:值。700 行代码、零依赖、五年后还能跑、行为可断言。

对我正在做的游戏项目:不一定。引擎里我最不愿意自己写的东西是渲染后端和压缩算法 —— 因为它们的"外部规范"会变(新硬件、新格式),而且测试矩阵我永远补不全。零依赖不是美德,"我能测到边界"才是边界。

参考

  • 本站源码:blog/build.mjs、blog/serve.mjs,跑 npm run check 会依次做语法检查、解析器用例、dry-run 构建
  • CommonMark 规范(我参照它决定"哪些语法必须支持、哪些干脆不支持")