Node 中的设计错误

Jul 4, 2018 20:00 · 2278 words · 5 minute read Node

Ryan Dahl

背景

  • 我的目标高度集中在编写事件驱动的 HTTP 服务器。
  • 在当时这个焦点对于服务端的JavaScript 来说至关重要。
  • 但是服务端的 JS 需要事件循环才能成功。

当 2012 年退出时,我感觉 Node 已经达到了我的期望,也就是一个对用户友好的非阻塞框架:

  • 原生支持多种协议:HTTP、SSL…
  • 全平台,Windows 版基于 IOCP,Linux 版基于 epoll 和 基于 kqueue 的 Mac 版。
  • 一个有着稳定的 API 的相对轻量的内核。
  • 依托于 NPM 快速增长的外部模块生态系统。

但是我错了,还有更多的工作需要完成。。。

  • npm 与 Node 核心库脱钩并且允许分发生态系统。
  • N-API 是一种设计精美的绑定 API。
  • Ben Noordhuis 与 Bert Belder 共同打造了 libuv。
  • Mikeal Rogers 组织与管理着社区。
  • Fedor Indutny 有着巨大的影响力,尤其是加密方面。
  • 众多大佬:TJ Fontaine、Rod Vagg、Myles Borins等人。

我在过去的半年里才再次使用 Node。这段时间里我重定向了我的目标。

动态语言是科学计算的利器,允许用户快速得到结果。JavaScript 是最棒的动态语言!

我来吐槽下 Node 所有的黑点。当你是负责人之一时,bug 从未如此显而易见。多少次它让我如坐针毡。

它本可以变得更好。

遗憾:没有坚持 Promise

  • 我在 2009 年中曾经为 Node 增加了对 Promise 的支持,但是愚蠢地在 2010 年二月将其移除。
  • 在 Node 中统一 Promise 的使用可能会加快 async/await 的最终标准化。
  • 如今 Node 中许多异步 API 因此严重过时。

遗憾:安全性

  • V8 本身就是一个非常出色的安全沙箱。
  • 如果我当时能更多地考虑如何使特定的应用程序可维护,Node 本可以实现其他语言无法比拟的安全性。

你的 linter 就不应该有访问计算机和网络的所有权限。

遗憾:编译系统(GYP)

  • 编译系统非常困难又是重中之重。
  • V8 开始使用 GYP 后我就把 Node 拖下了水。
  • 后来 Chrome 用 GN 替换掉了 GYP,Node 就成了 GYP 的唯一玩家。
  • GYP 也不算是个很糟的内部接口,它只暴露给任何想要绑定 V8 的东西。
  • 用户体验差劲,这不是原生 JSON,而是 Python 版的 JSON。
  • 吊死在 GYP 上也许是 Node 核心库的最大的失误。
  • 我应该提供一个核心的对外接口(FFI)而不是引导用户直接面对 V8 写 C++。
  • 不少人早就建议我转向 FFI,很遗憾我没听。
  • libuv 采用自动工具让我很不爽。

遗憾:package.json

  • Isaac 作为 NPM 的掌门人,发明了 package.json。
  • 但是我允许使用 Node require() 来为 main 检查 package.json。
  • 最终我将 NPM 包含在 Node 发行版当中,这让 NPM 成为了事实上的标准。
  • 很不幸这是一个中心化的模块库,甚至由私人控制。

package.json 产生了“模块就是一个目录下的文件”这种概念。
这不是一种绝对必要的抽象,web 中并不存在。

package.json 现在包含了各种不必要的信息。许可证?库?描述?这就是一堆干扰信息。

如果只在导入时使用相关的文件或者 URL,由路径来定义版本。没必要列出所有的依赖。

遗憾:node_modules

大大地复杂化了模块解析算法。

本地即默认出发点是好的,但实际上只使用 $NODE_PATH 没有排除的模块。

很大地偏离了浏览器的语义。

我很抱歉这是我的锅,不幸的是现在已经不可能逆转了。

遗憾:require(“module”) 没有加上扩展名“.js”

  • 没必要省这一步。
  • 你在 html 的脚本标签里没法省掉“.js”,这就和浏览器中的 JavaScript 不同了。
  • 模块加载器不得不从多个路径查询文件系统来揣测用户的意图。

遗憾:index.js

  • 我觉得这样萌萌哒,看看人家 index.html。。。
  • 但它不必要地复杂化了模块加载系统。
  • 在支持了 package.json 后就更毫无意义了。

我在使用 Node 时遇到的所有问题几乎都和它如何管理用户的代码有关。与早期专注于事件化 I/O 相反,模块系统在本质上是后来加上去的。


Design Mistakes in Node

Background

  • I created and managed Node through its initial development.
  • My goal was heavily focused on programming event driven HTTP servers.
  • That focus turned out to be crucial for Server-Side JavaScript at the time. It wasn’t obvious then but server-side JS required an event loop to succeed.

When I left in 2012, I felt Node had (more or less) achieved my goals for a user friendly non-blocking framework:

  • Core supported many protocols: HTTP, SSL, …
  • Worked on Windows (using IOCP) Linux (epoll) and Mac (kqueue).
  • A relatively small core with a somewhat stable API.
  • A growing ecosystem of external modules via NPM.

But I was quite wrong - there was so much left to do…

Critical work has kept Node growing since.

  • npm (AKA “Isaac”) decoupled the core Node library and allowed the ecosystem to be distributed.
  • N-API is beautifully designed binding API.
  • Ben Noordhuis and Bert Belder built the libuv.
  • Mikeal Rogers organized the governance and community.
  • Fedor Indutny has had a massive influence across the code base, particularly in crypto.
  • And many others: TJ Fontaine, Rod Vagg, Myles Borins, Nathan Rajlich, Dave Pacheco, Robert Mustacchi, Bryan Cantrill, Igor Zinkovsky, Aria Stewart, Paul Querna, Felix Geisendörfer, Tim Caswell, Guillermo Rauch, Charlie Robbins, Matt Ranney, Rich Trott, Michael Dawson, James Snell

I have only started using Node again in the last 6 months.

These days my goals are different.

Dynamic languages are the right tool for scientific computing, where often you do quick one-off calculations.

Dynamic languages are the right tool for scientific computing, where often you do quick one-off calculations

Rather I will complain about all the warts in Node.

Bugs are never so obvious as when you’re the one responsible for them.

At times Node is like nails on chalkboard to me.

It could have been so much nicer.

Regret: Not sticking with Promises

  • I added promises to Node in June 2009 but foolishly removed them in February 2010.
  • Promises are the necessary abstraction for async/await.
  • It’s possible unified usage of promises in Node would have sped the delivery of the eventual standardization and async/await.
  • Today Node’s many async APIs are aging badly due to this.

Regret: Security

  • V8 by itself is a very good security sandbox.
  • Had I put more thought into how that could be maintained for certain applications, Node could have had some nice security guarantees not available in any other language.
  • Example: Your linter shouldn’t get complete access to your computer and network.

Regret: The Build System (GYP)

  • Build systems are very difficult and very important.
  • V8 (via Chrome) started using GYP and I switched Node over in tow.
  • Later Chrome dropped GYP for GN. Leaving Node the sole GYP user.
  • GYP is not an ugly internal interface either - it is exposed to anyone who’s trying to bind to V8.
  • It’s an awful experience for users. It’s this non-JSON, Python adaptation of JSON.
  • The continued usage of GYP is the probably largest failure of Node core.
  • Instead of guiding users to write C++ bindings to V8, I should have provided a core foreign function interface (FFI).
  • Many people, early on, suggested moving to an FFI (namely Cantrill) and regrettably I ignored them.
  • (And I am extremely displeased that libuv adopted autotools.)

Regret: package.json

  • Isaac, in NPM, invented package.json (for the most part).
  • But I sanctioned it by allowing Node’s require() to to inspect package.json files for “main”.
  • Ultimately I included NPM in the Node distribution, which much made it the defacto standard.
  • It’s unfortunate that there is a centralized (privately controlled even) repository for modules.

require(“somemodule”) is not specific. There are too many places where it is defined.

Allowing package.json gave rise to the concept of a “module” as a directory of files.

This is not a strictly necessary abstraction - and one that doesn’t exist on the web.

package.json now includes all sorts of unnecessary information.
License? Repository? Description?
It’s boilerplate noise. If only relative files and URLs were used when importing, the path defines the version. There is no need to list dependencies.

Regret: node_modules

It massively complicates the module resolution algorithm.

vendored-by-default has good intentions, but in practice just using $NODE_PATH wouldn’t have precluded that.

Deviates greatly from browser semantics.

It’s my fault and I’m very sorry.
Unfortunately it’s impossible to undo now.

Regret: require(“module”) without the extension “.js”

  • Needlessly less explicit.
  • Not how browser javascript works. You cannot omit the “.js” in a script tag src attribute.
  • The module loader has to query the file system at multiple locations trying to guess what the user intended.

Regret: index.js

I thought it was cute, because there was index.html …

It needlessly complicated the module loading system.

It became especially unnecessary after require supported package.json.

My problems with Node are almost entirely around how it manages user code.

In contrast to the focus on evented I/O early on, the module system was essentially an afterthought.

With this in mind, I have long thought about how it could be done better….