首页 › programming › Node之父忏悔录:十个设计悔棋

一场迟到了十年的自我审判

工程师圈子里,成功故事从来不缺,缺的是「创始人公开数落自己」的故事。2019年的 JSConf EU 大会上,Ryan Dahl——就是2009年写下 Node.js 的那位——做了一件极罕见的事:他站上讲台,用二十多分钟,把十年前自己在 Node.js 里做错的设计决定,一条条列出来,足足十条。这场演讲叫 《10 Things I Regret About Node.js》,至今仍是所有做技术选型的人绕不开的一堂课。

它解决的问题是每个开发者都会遇到的:当年那个「合理」的决定,为什么后来变成了债?更狠的问题是——如果你连自己亲手写的代码都看走眼了,今天你该怎么为新项目做决定?Dahl 不是在忏悔,他是在演示一种稀缺能力:把旧决策放回当时的约束条件下重新审视,然后诚实地宣布「我错了」。这份清单后来直接催生了 Deno,也倒逼 Node.js 自己补课。看懂这十条悔棋,比看一百篇「最佳实践」都管用。

视频背景卡片

项目 内容
视频标题 10 Things I Regret About Node.js – Ryan Dahl – JSConf EU 2019
频道 JSConf EU(官方频道)
时长 约27分钟
播放量 百万级
核心主题 Node.js 十大设计失误复盘,以及它们如何催生了 Deno

十宗罪里最痛的几刀

1. 最大的悔棋:他亲手删掉了 Promise

很多人不知道,Node.js 最初的版本里其实是有 Promise 的。Dahl 在演讲里承认,项目启动约半年后,他把 Promise 从核心里删掉了——理由非常务实:当年的工具链很难对 Promise 做性能分析(profiling),回调加 EventEmitter 的写法反而「看得见摸得着」。这个决定让他后悔了十年。

后续的历史我们都知道:回调地狱(callback hell)折磨了整个社区好几年,一层套一层的箭头函数成为一代人的心理阴影;ES2015 把 Promise 写进语言规范,ES2017 的 async/await 一统江湖,语言最终回到了 Dahl 当年放弃的那条路上。这一段给我的触动最深:技术选型的错误,往往不是选错了技术,而是为了迁就当下的工具链,放弃了正确的抽象。 当时的 profiling 工具跟不上,不代表抽象本身错了——这是视频原话之外,我认为最值得展开的一层分析。

2. 安全感的缺席:npm install 本身就是个后门

Dahl 在演讲里做了一个对比:浏览器里的 JavaScript 跑在沙箱里,读个文件都要弹窗授权;而 Node.js 生下来就拥有整台机器的全部权限——读文件、发网络请求、执行命令,随意。更糟的是 npm 的安装机制:npm install 的时候,包里声明的构建脚本(preinstall/postinstall)会自动执行任意代码。你装一个依赖,等于把陌生人请进家门还交出了钥匙。

这不是危言耸听。演讲前一年刚发生过 event-stream 事件:一个下载量巨大的老包被恶意注入代码,专门盯上了某个比特币钱包应用的用户;再往前,2016年 11 行代码的 left-pad 被作者一怒下架,半个 npm 生态的构建当场瘫痪,连 Babel 都中招。Dahl 给出的修正案体现在 Deno 里:默认零权限,读文件要 --allow-read,联网要 --allow-net,每个权限都显式授予。

3. require('foo') 到底从哪来?模块解析的层层魔法

第三刀砍向模块系统。require('foo') 这行代码里藏着一大套隐式规则:先看是不是核心模块,再沿目录树一层层向上找 node_modules,还要翻 package.json 里的 main 字段,最后兜底到 index.js。Dahl 认为这套「聪明的」解析算法是个错误——它让模块的来源变得不可预测,解析速度慢,依赖树层层嵌套,行为难以推理。

他的理想方案朴素得多:像浏览器一样,导入就写明确的路径,带上文件扩展名,甚至可以直接用 URL 导入远程模块。显式,永远优于魔法——这条原则后来成了 Deno 的基本教义:导入必须写全路径带扩展名,远程模块直接用 URL,不搞目录级暗语。

4. package.json 一个文件,背了太多锅

Dahl 对 package.json 的吐槽相当不留情:这个文件既是依赖清单,又是项目元数据,还是构建配置,塞进了一堆字段——而其中不少(比如 engines)实际上并没有被运行时严格执行,形同虚设。再叠加语义化版本的范围符号(caret 之类),同一个 package.json 今天装和明天装,可能得到两棵不一样的依赖树。

社区其实已经在用行动回应这个问题:2017年 npm 5 引入 package-lock.json 把依赖树钉死;npm ci 命令强制按锁文件安装。但这些补丁恰恰说明:当一个文件被赋予太多职责时,每一项职责都会做不好。 这是软件设计里反复出现的母题,不只是 Node 的毛病。

5. 工程债的细节:GYP 与原生模块的痛

清单里还点名了构建系统 GYP——当年为了编译 C++ 原生扩展选的工具,让写 Node 原生模块的开发者常年痛苦。选型当时的理由很现实:V8 项目本身在用,顺手。但「顺手」和「好用」是两回事,原生 addon 的编译配置从此成为社区多年的高频吐槽点。这一条提醒我们:基础设施层的偷懒,会被生态里的每一个人反复偿还。

6. 悔棋的下半场:Deno 是修订版,Node 在补课

演讲的最后,Dahl 把 Node 定义成一场实验——一场影响力远超预期的实验。而他的「修订版」就是 Deno:基于 Rust 重写、默认安全沙箱、原生支持 TypeScript 和 ES Modules、显式导入、单文件编译分发。几乎每一条设计都对着十年前的悔棋清单开枪。

但故事有个耐人寻味的反转:2024年发布的 Deno 2.0,又回头兼容了 npm 生态和 package.json——因为再正确的设计,也拗不过百万个现存项目的生态惯性。与此同时,Node.js 自己也在补课:支持 ES Modules、锁文件成为标配、审计与来源签名机制逐步落地。设计可以推翻重来,生态只能慢慢修复——这是清单之外的另一个大教训。

7. 这场演讲最值钱的,不是清单,是复盘的方法论

十个决定,每一个在2009年都有当时的合理性:删 Promise 是因为 profiling 工具跟不上,选 GYP 是因为 V8 在用,给满权限是因为服务端脚本本来就要操作文件和网络。Dahl 厉害的地方在于,他没有用今天的后见之明去羞辱昨天的自己,而是把「当时的约束」和「事后的代价」掰开讲清楚。这种复盘能力——把旧决策拆解成「前提、选择、代价」三段——恰恰是大多数技术团队 retro 会议上最缺的东西。多数团队的复盘止步于「下次注意」,而他的复盘产出了一个新的运行时。

知识结构图

flowchart TD
    A["2009:Dahl 写下 Node"] --> B["为性能弃用 Promise"]
    A --> C["require 按名字找模块"]
    A --> D["npm 脚本随装随跑"]
    B --> E["回调地狱十年"]
    C --> F["node_modules 黑洞"]
    C --> G["index.js 魔法"]
    D --> H["供应链攻击入口"]
    E --> I["ES2017 async/await<br/>证明 Promise 是对的"]
    F --> J["依赖树不可复现"]
    H --> K["event-stream 事件"]
    I --> L["2019:十宗罪演讲"]
    J --> L
    G --> L
    K --> L
    L --> M["Deno 修订版<br/>默认沙箱+显式导入"]
    L --> N["Node 自我补课<br/>锁文件+ESM+审计"]
    M --> O["启示:显式优于魔法<br/>标准优于自造"]
    N --> O

实战启示

如果你是日常写 Node 的人,三个动作明天就能做:第一,把锁文件提交进仓库,CI 里用 npm ci 而不是 npm install,让每次构建可复现;第二,审视安装脚本——可以在本机或 CI 里禁用依赖的 install 脚本(ignore-scripts 配置),代价是部分依赖原生编译的包需要单独处理,但至少给了供应链攻击加一道闸;第三,新项目直接上 ES Modules,导入写全路径带扩展名——不是为了赶时髦,而是为了可预测。

如果你做 API 或平台设计,这场演讲是免费的设计课:跟语言标准走,别自造平行体系(Node 之于 Promise 的教训);默认最小权限,能力按需显式授予(浏览器沙箱与 Deno 的启示);别让一个文件/一个概念承载太多职责(package.json 的教训);隐式魔法省下的每一行代码,将来都会以十倍调试时间还回来(index.js 的教训)。

如果你带团队,最值得抄的是他的复盘格式:每个旧决策拆成「当时的约束—做过的选择—今天的代价」,约束变了,结论才变得理直气壮。季度回顾时,让每个人列一份「如果重来我不会再做的十个决定」——这份清单的价值,通常高于任何一份成功经验总结。

延伸学习路径

  1. 《Design Mistakes in Node》 — Ryan Dahl,JSConf EU 2018(YouTube 检索标题可达)。「十宗罪」的前传,Deno 原型首次公开亮相的场合,两场连着看才是完整故事。
  2. Deno 官方网站(deno.com) — 看「默认安全」的权限模型如何落地,以及 2.0 之后如何与 npm 生态和解。
  3. Node.js 官方文档的 ESM 指南(nodejs.org) — 主流运行时如何一步步补课模块系统,对照 Dahl 的批评看别有味道。
  4. JSR(jsr.io) — Dahl 团队对包管理悔棋的最终答卷:新一代注册表,原生 TypeScript、按源码分发。
  5. left-pad 与 event-stream 事件复盘(npm 官方博客及各家工程博客可检索)— 理解 npm 供应链风险的两个教科书案例。

一句话总结

顶级工程师和普通工程师的差距,不在于谁不犯错,而在于谁敢把错误清单公开讲上二十七分钟——这场演讲教你的复盘能力,比任何框架都保值。

参考来源

  1. 10 Things I Regret About Node.js – Ryan Dahl – JSConf EU 2019 — 本文分析的核心视频
  2. Design Mistakes in Node – Ryan Dahl – JSConf EU 2018 — 前传演讲,Deno 首次公开(YouTube 检索标题可达)
  3. Deno 官方网站 — 默认安全模型与 Deno 2.0 的 npm 兼容说明
  4. Node.js 官方文档 — ES Modules 与锁文件机制的官方演进说明
  5. JSR — 新一代包注册表,对包管理问题的最终修正

本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入