Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3
5608 字
28 分钟
Node.js 十大设计缺陷(Ryan Dahl 2018 JSConf 演讲)
2026-04-04
无标签

回顾Node.js 创始人 Ryan Dahl 在 2018 年 JSConf EU 上的经典演讲《10 Things I Regret About Node.js》中,提出的十个关于Nodejs的设计缺陷#

缺陷 1:没有坚持使用 Promise(Not sticking with Promises)#

  1. 历史背景: Ryan 在 2009 年首次给 Node 加入了 Promise 实现,但 2010 年错误地将其移除,选择了 回调函数(Callback) 作为异步 API 的唯一标准(err, result 风格)。

  2. 核心缺陷:

  • Promise 是 async/await 语法的必要抽象基础,过早移除导致 Node 异步生态长期被回调嵌套(回调地狱)主导,严重拖慢了 async/await 标准化和落地的进程。

  • 大量核心 API(fs、http 等)长期基于回调实现,后续改造难度极大,API 生态严重老化,出现 “回调 + Promise 混用” 的混乱局面。

  • 破坏了异步编程的统一性,开发者需要同时适配两种异步模型,学习成本和维护成本极高。

后续问题解决与优化#

1. 标准化 Promise 支持(2015-2018 铺垫)

  • ES6 标准化:2015 年 ES6 正式将 Promise 纳入语言标准,Node.js v4+ 原生支持全局 Promise,为后续改造奠定基础。

  • 工具链过渡:社区出现 bluebird 等 Promise 库,提供 promisifyAll 等工具,将回调 API 批量转换为 Promise 版本,缓解了混用问题。

  • util.promisify 内置:Node.js v8.0.0(2017)内置 util.promisify,官方支持将回调风格 API 一键转为 Promise,降低了改造门槛。

2. 核心 API 全面 Promise 化(2018 至今)

  • fs/promises 模块:Node.js v10.0.0(2018)正式推出 fs/promises 模块,提供完全基于 Promise 的文件系统 API,替代原回调版 fs,成为官方推荐用法。

  • 核心模块批量改造:http、https、child_process、stream 等核心模块逐步推出 Promise 版本,同时保留回调 API 以兼容旧代码。

  • async/await 原生深度支持:Node.js v7.6.0(2017)原生支持 async/await,v8+ 完全优化了异步执行性能,Promise 成为 Node 异步编程的事实标准,回调仅用于兼容旧场景。

3. 生态统一与现代化

  • npm 生态迁移:绝大多数 npm 包从回调风格全面转向 Promise + async/await,回调包逐步被淘汰。

  • 顶层 await 支持:Node.js v14.8.0(2020)支持 ES Module 中的顶层 await,进一步简化异步代码,彻底摆脱回调依赖。

  • fetch API 原生支持:Node.js v18.0.0(2022)原生实现 Web 标准 fetch API(基于 Promise),统一了浏览器与 Node 的异步网络编程模型。

4. 现状(2026)

  • Promise + async/await 已成为 Node.js 异步编程的唯一主流标准,回调 API 仅作为兼容保留,新代码 99% 基于 Promise 编写。

  • 核心模块 100% 提供 Promise 版本,fs/promises 成为默认文件系统 API,彻底解决了 Ryan 提到的 “API 老化” 问题。

缺陷 2:安全设计缺失(Security)#

  1. 核心缺陷:V8 本身是一个极强的安全沙箱(浏览器中隔离 JS 与系统),但 Node.js 设计时完全放弃了沙箱隔离,默认给所有 JS 代码完全的系统权限(绷不住了,这真TM逆天):

后果:

任意 Node 脚本可直接读写文件、访问网络、执行系统命令,没有任何权限限制。

  • 比如代码检查工具(linter)、构建工具这类仅需处理文件的工具,却拥有完整的系统访问权,一旦被注入恶意代码,会直接导致系统被攻陷。
  • 没有实现 “最小权限原则”,安全完全依赖开发者手动管控,成为 Node 生态的重大安全隐患(供应链攻击、恶意包泛滥的核心原因之一)。

后续解决与优化(2018-2026 完整过程)#

1. 安全沙箱与权限控制(核心解决方案)

1. Node.js Permission Model(权限模型):Node.js v20.0.0(2023)正式推出原生权限模型,实现了 “默认受限、按需授权” 的安全机制:

  • 启动 Node 时默认无任何系统权限,需通过 —allow-fs-read、—allow-fs-write、—allow-net 等参数显式授权。
  • 支持细粒度权限控制(如仅允许读写指定目录、仅允许访问指定网络端口),完美解决了 “linter 拥有完整系统权限” 的问题。
  • 兼容旧代码,默认不开启,不影响现有项目运行。
  • 沙箱运行时:Node.js v19+ 支持 —experimental-sandbox,提供更严格的 V8 沙箱隔离,限制脚本对系统的访问,用于运行不可信代码。

2. 供应链安全强化 npm 安全机制升级:npm 推出 npm audit、npm ci(锁定依赖版本)、package-lock.json 完整性校验、恶意包自动拦截等机制,降低供应链攻击风险。 SBOM(软件物料清单)支持:Node.js v18+ 内置 SBOM 生成工具,可生成依赖清单,用于漏洞扫描和合规审计。 签名验证:npm 支持包签名验证,确保依赖包未被篡改。

3. 安全工具与生态 Node.js Security Working Group:成立专门的安全工作组,定期发布安全补丁、漏洞预警,建立快速响应机制。 第三方沙箱方案:社区出现 vm2、isolated-vm 等沙箱库,用于运行不可信代码,补充原生沙箱能力。 Deno 反向推动:Ryan 后续推出的 Deno 以 “默认安全” 为核心设计,反向推动 Node.js 强化安全机制。

4. 现状(2026)

原生权限模型已成为 Node.js 核心安全特性,生产环境广泛使用,实现了 “最小权限” 的安全设计。 供应链安全体系完善,npm 生态的恶意包拦截、漏洞扫描能力大幅提升,安全风险显著降低。 沙箱隔离能力持续优化,可满足不可信代码运行的安全需求,弥补了最初的设计缺陷。

缺陷 3:构建系统 GYP(The Build System (GYP))#

核心问题#

  1. 历史背景:Ryan 跟随 Chrome V8 选择了 GYP(Generate Your Projects)作为 Node 的构建系统,但 Chrome 后续切换为 GN,Node 成为 GYP 的唯一主流用户。

  2. 核心缺陷:

  • GYP 是基于 Python 的非标准 JSON 格式(类似 JSON 但有语法扩展),学习成本高、维护难度大,用户体验极差。
  • 作为 Node 核心构建系统,同时暴露给所有 C++ 插件(native addon)开发者,导致插件构建流程复杂、跨平台兼容性差。 缺乏社区维护,Chrome 放弃后,Node 独自维护 GYP,技术债务持续累积,成为 Node 最大的架构失败之一。

后续解决与优化#

1. 核心构建系统迁移:GYP → GN → Ninja

Node.js 核心迁移:

Node.js v12.0.0(2019)启动核心构建系统从 GYP 迁移到 GN(Chrome 新一代构建系统),v14.0.0(2020)完成核心迁移,彻底抛弃 GYP 作为核心构建工具。

GN 优势:GN 是高性能、跨平台的构建系统,语法清晰、性能远优于 GYP,与 V8/Chrome 生态完全对齐,解决了维护问题。

Ninja 编译加速:配合 Ninja 构建工具,大幅提升编译速度,降低构建耗时。

2. C++ 插件构建体系现代化

node-gyp 升级与替代方案:

  • node-gyp 作为插件构建工具,逐步优化 GYP 兼容性,同时推出 node-gyp@9+ 支持 GN 构建。

  • node-addon-api (N-API):Node.js v8.0.0(2017)正式推出 N-API,提供稳定的 C 语言 API,彻底摆脱 V8 版本依赖,插件无需重新编译即可适配不同 Node 版本,大幅降低插件维护成本。

  • prebuildify:社区工具 prebuildify 实现插件预编译,用户无需本地构建,直接安装二进制包,解决了 GYP 构建的痛点。

  • CMake.js 替代方案:社区推出 cmake-js,用 CMake 替代 GYP 作为插件构建系统,跨平台兼容性更好,成为主流替代方案。

3. 现状(2026)

Node.js 核心已完全基于 GN + Ninja 构建,GYP 仅用于兼容旧插件,核心代码无 GYP 依赖。

N-API 成为 C++ 插件的官方标准,绝大多数插件基于 N-API 开发,彻底摆脱 GYP 依赖,跨平台兼容性大幅提升。

node-gyp 逐步被 cmake-js、prebuildify 替代,插件构建流程简化,用户体验大幅优化。

缺陷 4:package.json 设计失误#

核心缺陷:#

  • 中心化依赖:将 npm 内置到 Node 发行版,使其成为事实上的包管理标准,导致 Node 生态完全依赖私有中心化仓库(npm 由公司运营),存在单点故障、商业控制、供应链攻击等风险。

  • 错误的模块抽象:package.json 创造了 “目录即模块” 的概念,这是 Web 环境中不存在的抽象,破坏了 JS 模块的原生语义(Web 中模块是单个文件,通过 URL 引用)。

  • 冗余信息泛滥:package.json 包含大量非必要信息(许可证、仓库、描述等),成为样板文件噪音,增加维护成本。

  • 依赖管理冗余:需要手动在 package.json 中声明依赖,而 Web 中通过 URL 路径即可定义版本,无需额外声明,增加了复杂度。

  • require 语义污染:让 require() 读取 package.json 的 main 字段,破坏了模块加载的简洁性,为后续 node_modules 问题埋下伏笔。

后续解决与优化#

1. 去中心化与多包管理器生态

npm 替代方案崛起:yarn、pnpm 等包管理器出现,打破 npm 垄断,提供更优的依赖管理、性能和安全性,同时支持去中心化仓库。

Node.js 内置包管理器中立化:Node.js 不再强制绑定 npm,允许用户自由选择 yarn、pnpm 等工具,官方对所有包管理器提供同等支持。

去中心化仓库支持:Node.js 支持私有 npm 仓库、开源去中心化仓库(如 Verdaccio、JSR),降低对 npm 官方仓库的依赖。

JSR 生态融合:Deno 推出的 JSR(JavaScript Registry)作为开源去中心化仓库,Node.js v20+ 原生支持 JSR 包,实现跨运行时的去中心化模块管理。

2. ES Module 标准化,回归 Web 语义

ES Module 原生支持:Node.js v12.0.0(2019)正式支持 ES Module(ESM),完全遵循 Web 标准:

模块通过文件 / URL 引用,无需 package.json 即可加载,回归 Web 原生语义。

支持相对路径、绝对路径、URL 导入,版本通过 URL 路径定义(如 import from “https://cdn.example.com/module@1.0.0.js”),无需手动声明依赖。

package.json 角色弱化:ESM 环境中,package.json 仅作为包元数据存在,不再是模块加载的必要条件,main 字段被 exports 字段替代,语义更清晰。

冗余信息优化:社区推动 package.json 精简,仅保留必要字段,非必要字段(如描述、仓库)成为可选项,降低样板文件负担。

3. 依赖管理现代化

pnpm 硬链接优化:pnpm 通过硬链接实现依赖共享,大幅减少 node_modules 体积,解决了依赖冗余问题。

依赖锁定标准化:package-lock.json、yarn.lock、pnpm-lock.yaml 成为标准,确保依赖版本一致性,降低 “依赖地狱” 风险。

零安装依赖:Deno 式 “零安装” 理念反向推动 Node.js,支持直接从 URL 导入模块,无需 npm install,无需 package.json 声明依赖。

4. 现状(2026)

ESM 已成为 Node.js 模块标准,完全对齐 Web 语义,package.json 仅作为包元数据存在,不再是模块加载的核心。

包管理器生态多元化,npm、yarn、pnpm 三足鼎立,去中心化仓库广泛使用,解决了中心化依赖的问题。 支持直接 URL 导入,版本通过路径定义,无需手动声明依赖,回归简洁的 Web 模块语义。

缺陷 5:node_modules 设计灾难(node_modules 称为宇宙中最重的物体,比黑洞还重)#

核心缺陷#

  1. 模块解析算法极度复杂:node_modules 实现了 “嵌套依赖、就近查找” 的解析规则,导致模块解析逻辑极其复杂,出现 “幽灵依赖”“依赖 hoisting”“版本冲突” 等一系列问题。

  2. 完全偏离浏览器语义:浏览器中模块通过 URL 加载,无本地依赖目录,node_modules 是 Node 独有的设计,彻底割裂了前后端模块语义。

  3. 体积爆炸与性能问题:嵌套依赖导致 node_modules 目录体积极度膨胀(动辄数万文件、数 GB 体积),安装慢、拷贝慢、CI/CD 效率极低,成为前端工程化的最大痛点。

  4. 依赖地狱:不同依赖的子依赖版本冲突,导致 “依赖 hoisting” 失效,出现重复依赖、版本不一致等问题,调试难度极大。

  5. 无法撤销:生态完全基于 node_modules 构建,彻底无法回退,成为永久技术债务。

后续解决与优化#

1. 依赖管理优化:pnpm 革命性突破

  • pnpm 硬链接机制:pnpm 通过内容可寻址存储 + 硬链接,实现依赖全局共享、本地零冗余,node_modules 体积减少 90% 以上,安装速度提升 10x,彻底解决体积爆炸问题。

  • 确定性依赖解析:pnpm 实现严格的依赖解析,杜绝幽灵依赖,确保依赖版本一致性,解决依赖地狱问题。 Node.js 官方认可:Node.js 官方将 pnpm 推荐为首选包管理器之一,v16+ 原生优化 pnpm 兼容性。

  1. ESM 与零安装方案
  • ESM 直接导入:Node.js v12+ 支持 ESM 直接从 URL/CDN 导入模块,无需 npm install,无需 node_modules,彻底摆脱依赖目录。

  • Deno 式零安装理念:Deno 的 “零安装、URL 导入” 反向推动 Node.js,Node.js v18+ 原生支持 node_modules 可选,支持完全无 node_modules 开发 。

  • bundleless 开发:Vite、Snowpack 等 bundleless 工具基于 ESM 实现,开发环境无需打包,直接运行源码,减少 node_modules 依赖。

  1. 模块解析算法优化
  • ESM 模块解析简化:ESM 遵循 Web 标准解析规则,比 CommonJS 的 node_modules 解析简单 10 倍,无嵌套查找,解析速度大幅提升。

  • exports 字段优化:package.json 中 exports 字段替代 main,明确模块入口,简化解析逻辑,同时支持条件导出,适配不同环境。

  • import maps 支持:Node.js v18+ 支持 import maps,实现模块别名映射,简化模块引用,无需复杂的路径解析。

  1. 单二进制可执行文件(Single Executable Applications)

Node.js v19+ 支持单二进制打包:将 Node 运行时 + 代码 + 依赖打包为单个可执行文件,彻底摆脱 node_modules,部署时无需安装依赖,直接运行。

pkg、nexe 等工具成熟:第三方打包工具持续优化,支持生产环境单二进制部署,解决 node_modules 部署痛点。

  1. 现状(2026) node_modules 仍作为 CommonJS 兼容保留,但 ESM 环境可完全无 node_modules 开发,成为主流趋势。 pnpm 成为包管理器主流,node_modules 体积问题基本解决,安装速度、性能大幅提升。 单二进制部署、URL 导入、bundleless 开发等方案,让 node_modules 不再是开发的必要环节,彻底缓解了 Ryan 提到的设计灾难。

缺陷 6:require 省略 .js 扩展名#

核心问题:#

语义不明确:require(“module”) 省略 .js 扩展名,导致代码可读性差,无法直接判断加载的是文件还是目录 / 模块。

偏离浏览器语义:浏览器中

模块加载性能损耗:模块加载器需要在文件系统中多次尝试(.js、.json、.node、目录等),猜测用户意图,导致加载速度慢、逻辑复杂,增加出错概率。

扩展性差:后续新增 .ts、.mjs 等扩展名时,进一步加剧了猜测逻辑的复杂度

后续解决与优化(2018-2026 完整过程)#

  1. ESM 强制要求扩展名

ES Module 标准强制扩展名:Node.js 实现的 ESM 完全遵循 Web 标准,必须写全文件扩展名(import module from ”./module.js”),彻底解决省略扩展名的问题。

对齐浏览器语义:ESM 中扩展名规则与浏览器完全一致,实现前后端模块语义统一,消除割裂。 CommonJS 兼容保留:CommonJS 仍保留省略扩展名的特性,用于兼容旧代码,但新代码全部基于 ESM 编写,强制使用扩展名。

  1. 模块加载器优化

ESM 加载器简化:ESM 加载器无需猜测扩展名,直接按路径加载文件,加载速度提升 5-10 倍,逻辑清晰,无歧义。

自定义加载器支持:Node.js v16+ 支持自定义 ESM 加载器,可实现扩展名自动补全(仅用于兼容旧代码),但默认强制要求扩展名。

TypeScript 集成优化:TypeScript 4.7+ 原生支持 Node ESM,编译后自动保留 .js 扩展名,无需手动修改,适配 ESM 标准。

  1. 现状(2026)

ESM 已成为 Node.js 模块标准,强制要求扩展名,彻底解决了 Ryan 提到的问题。

CommonJS 仅用于兼容旧项目,新代码 100% 基于 ESM 编写,扩展名规则与浏览器完全对齐。 模块加载器性能大幅提升,无文件系统猜测逻辑,加载速度显著优化。

缺陷 7:index.js 默认入口设计#

核心问题:

模仿 index.html 的错误设计:Ryan 模仿 Web 中 index.html 作为目录默认页,设计了 index.js 作为目录默认入口,导致模块加载逻辑复杂化。

完全不必要的抽象:在 require() 支持 package.json 的 main 字段后,index.js 彻底失去存在意义,反而增加了模块加载的复杂度。

加载逻辑冗余:模块加载器需要先检查目录下的 index.js,再读取 package.json,导致加载流程冗余,性能损耗。

语义混乱:目录入口同时存在 index.js 和 package.json 两种方式,导致模块加载逻辑不统一,开发者困惑。

后续解决与优化#

  1. ESM 废弃 index.js 默认入口

ESM 标准不支持目录默认入口:Node.js ESM 完全遵循 Web 标准,不支持目录自动加载 index.js,必须显式指定入口文件(import from ”./dir/index.js”),彻底移除冗余逻辑。

package.json exports 字段替代:ESM 中通过 package.json 的 exports 字段明确模块入口,替代 index.js 和 main 字段,语义清晰,无歧义。

CommonJS 兼容保留:CommonJS 仍保留 index.js 默认入口,用于兼容旧代码,但新代码全部基于 ESM,不再使用 index.js 作为目录入口。

  1. 模块加载流程简化

ESM 加载流程极简:ESM 加载器直接按路径加载文件,无需检查 index.js,无需读取 package.json(除非使用 exports 字段),加载速度大幅提升。

exports 字段标准化:exports 字段成为模块入口的唯一标准,支持条件导出、子路径导出,彻底替代 index.js 和 main 字段,语义统一。

  1. 现状(2026)

ESM 中 index.js 默认入口已被彻底废弃,仅 CommonJS 兼容保留,新代码不再使用。

package.json 的 exports 字段成为模块入口的唯一标准,模块加载逻辑极简、清晰,无冗余。 模块加载性能大幅提升,彻底解决了 Ryan 提到的 “不必要的复杂度” 问题。

八、补充缺陷(演讲中提及的完整 10 项,补充 3 项)

缺陷 8:未提供 FFI(Foreign Function Interface),过度依赖 C++ 绑定 2018 年问题

Ryan 提到:Node 应该提供核心 FFI 接口,而非让开发者直接绑定 V8 C++ API,导致插件开发难度极高、版本兼容性差。

后续解决#

Node-API (N-API):提供稳定的 C 语言 FFI 接口,彻底摆脱 V8 版本依赖,插件跨版本兼容。

Node.js FFI 模块:ffi-napi 等模块提供纯 JS 调用系统动态库的能力,无需 C++ 插件。

Wasm 支持:Node.js v12+ 原生支持 WebAssembly,通过 Wasm 实现跨语言调用,替代 C++ 插件。

缺陷 8:未提供 FFI(Foreign Function Interface),过度依赖 C++ 绑定#

历史背景:

2018 年问题,Ryan 提到:Node 应该提供核心 FFI 接口,而非让开发者直接绑定 V8 C++ API,导致插件开发难度极高、版本兼容性差.

后续解决:

  • Node-API (N-API):提供稳定的 C 语言 FFI 接口,彻底摆脱 V8 版本依赖,插件跨版本兼容。

  • Node.js FFI 模块:ffi-napi 等模块提供纯 JS 调用系统动态库的能力,无需 C++ 插件。

  • Wasm 支持:Node.js v12+ 原生支持 WebAssembly,通过 Wasm 实现跨语言调用,替代 C++ 插件。

缺陷 9:process 全局对象设计#

历史背景:

2018 年问题,process 全局对象设计过于复杂,包含大量非必要 API,破坏了 JS 全局环境的简洁性,且与浏览器不兼容。

后续解决:

ESM 中 process 可选:ESM 中不再默认挂载 process,需手动 import process from “process”,简化全局环境。

全局对象标准化:Node.js 逐步对齐 Web 全局对象(如 globalThis),process 仅作为 Node 特有 API 保留。

缺陷 10:错误的路径解析规则(__dirname/__filename)#

2018 年问题

__dirname、__filename 是 Node 独有的全局变量,与浏览器语义完全割裂,且在 ESM 中无法使用,导致前后端代码不兼容。

后续解决

ESM 中使用 import.meta.url:ESM 中通过 import.meta.url 替代 __dirname/__filename,完全对齐 Web 标准,实现前后端代码统一。

CommonJS 兼容保留:CommonJS 仍保留 __dirname/__filename,用于兼容旧代码。

总结#

Ryan 在 2018 年的演讲中,所有缺陷的本质都是为了早期快速落地,牺牲了长期架构的一致性、安全性、可维护性#

为了快速实现异步,放弃 Promise 选择回调;

为了快速开发工具,放弃 V8 沙箱,给完全权限;

为了跟随 Chrome,选择 GYP 构建系统;

为了快速实现包管理,设计了 package.json 和 node_modules;

为了模仿 Web,设计了 index.js、省略扩展名等非标准特性。

Nodejs8年时间进化的核心方向#

从 2018 到 2026,Node.js 的所有优化都围绕回归 Web 标准、补全设计缺陷、强化安全、简化架构展开: 异步模型:从回调 → Promise → async/await,彻底统一异步编程模型;

模块系统:从 CommonJS → ESM,完全对齐 Web 标准,消除 node_modules、index.js 等非标准设计;

安全:从默认全权限 → 权限模型、沙箱隔离,实现最小权限;

构建系统:从 GYP → GN/N-API,简化插件开发,提升兼容性;

包管理:从中心化 npm → 多包管理器、去中心化仓库,降低依赖风险。

所有的这些问题为后续Nodejs生产维护API模块带来了巨大的挑战,我本以为ryen 和他的团队会在之后在如何选择底层系统和模型规范方面更深思熟虑一点. 结果这哥们没有选择去这样做,而是推出了一个’Deno’的全新JS&TS运行时🤣🤣🤣#

Node.js 十大设计缺陷(Ryan Dahl 2018 JSConf 演讲)
https://mizuki.mysqil.com/posts/nodejs/
作者
ChoriaKiinweill
发布于
2026-04-04
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时