<!-- title: Node之父忏悔录：十个设计悔棋 -->
<!-- domain: programming -->
<!-- video: https://www.youtube.com/watch?v=M3BM9TB-8yA -->

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

工程师圈子里，成功故事从来不缺，缺的是「创始人公开数落自己」的故事。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 会议上最缺的东西。多数团队的复盘止步于「下次注意」，而他的复盘产出了一个新的运行时。

## 知识结构图

```mermaid
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](https://www.youtube.com/watch?v=M3BM9TB-8yA) — 本文分析的核心视频
2. Design Mistakes in Node – Ryan Dahl – JSConf EU 2018 — 前传演讲，Deno 首次公开（YouTube 检索标题可达）
3. [Deno 官方网站](https://deno.com) — 默认安全模型与 Deno 2.0 的 npm 兼容说明
4. [Node.js 官方文档](https://nodejs.org) — ES Modules 与锁文件机制的官方演进说明
5. [JSR](https://jsr.io) — 新一代包注册表，对包管理问题的最终修正

---
> 本文由「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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
