工作流工具观察2026/06/228 分钟

不是干掉 Node:Nub 想做的是 Node.js Plus

这几年,JavaScript 运行时的叙事一直很热。

Nub Node.js Plus 公众号头图

这几年,JavaScript 运行时的叙事一直很热。

Bun 把“快”和“一体化工具链”讲得很漂亮:运行 TypeScript、装包、跑脚本、打包、测试,一个 bun 命令就能接住。Deno 则从另一条路重做运行时:安全默认值、TypeScript-first、内置工具链、现代模块系统。

看起来,Node.js 像是那个应该被替换的旧世界。

但真实项目里,很多团队并没有那么自由。服务跑在 Node 上,CI/CD 是 Node,历史脚本是 Node,npm 生态是 Node,线上排障经验也是 Node。你当然可以在新项目里尝试 Bun 或 Deno,但一个几年沉淀下来的业务系统,往往不能只靠“新工具更快”就迁过去。

所以真正的问题不是:

要不要干掉 Node?

更现实的问题是:

能不能继续用 Node,但把现代开发体验补上来?

最近开源的 Nub,切的正是这个口子。

Nub 作为 Node.js 增强层

Nub 是什么:一个站在 Node 上面的工具包

Nub 的作者是 Colin McDonnell,也就是很多人熟悉的 colinhacks,他还是 Zod、tRPC 等项目背后的核心作者之一。

从 GitHub README 的定位看,Nub 不是一个新的 JavaScript 运行时,而是一个 “all-in-one Node.js toolkit”。截至 2026-06-19,我通过 GitHub API 查到 nubjs/nub 仓库创建于 2026-06-03,主语言是 Rust,最新 release 已到 v0.1.4,仓库描述就是:The all-in-one Node.js toolkit

这几个信息放在一起,其实很能说明它的野心:

它想做的不是“另一个 Node 替代品”,而是“Node.js Plus”。

以前一个普通 Node 项目想要舒服地开发 TypeScript,往往会叠一堆工具:

你想要的能力常见工具
直接跑 TypeScript / TSXtsxts-node
自动加载环境变量dotenv
文件变更自动重启nodemonnode --watch
支持 tsconfig pathstsconfig-paths
临时执行 CLInpxpnpm dlx
管理 Node 版本nvmfnmvolta
管理包管理器版本corepack

这些工具都能用,但它们散落在不同层:有的挂在运行时前面,有的挂在脚本里,有的靠环境变量,有的靠编辑器和打包器理解。项目越久,这些小工具越容易变成一层层“隐形胶水”。

Nub 的思路很直接:把这些胶水收进一个入口。

nub index.ts
nub run dev
nubx prisma generate
nub install
nub watch src/server.ts
nub node install 22

这就是 Nub 最容易被误解的地方。

它看起来像 Bun,因为它也是一个命令接住很多事;但它底下跑的仍然是你项目里的 Node。

它不是魔法,而是把 Node 的扩展面用满

很多新工具一说“更快、更现代”,开发者第一反应会是:是不是又造了一个运行时?是不是又要迁移一堆兼容问题?

Nub 比较聪明的地方在于,它没有换掉 Node。

你执行:

nub app.ts

Nub 这个 Rust CLI 会先做几件事:找到当前项目该用哪个 Node,读取附近的 package.jsontsconfig.json.env 等配置,然后启动真实的 Node 进程。

关键在启动方式上。

Nub 会通过 --import 注入自己的 preload 脚本,再利用 Node 的模块自定义能力,把 resolveload 这类环节接进来。Node 官方文档里,module.registerHooks() 就是用来注册模块解析和加载钩子的 API;在 Node 22.15+ 之后,它走的是同步、同线程的 fast path。

换句话说,Nub 不是篡改 Node,而是在 Node 开放出来的入口上做增强。

当你的代码里 import 一个 TypeScript、JSX、YAML、TOML、JSONC 文件时,请求会先经过 Nub 的 load hook。Nub 负责把它转成 Node 能执行的东西,再交还给 Node。

TypeScript 这块尤其关键。

Node 自己已经内置了 TypeScript type stripping,但官方文档也写得很清楚:它的目标是轻量,不支持需要生成 JavaScript 的语法,比如 enum、带运行时代码的 namespace、参数属性、decorators,也不读取 tsconfig.json,不处理 paths

Nub 要补的正是这段空白。

它通过 N-API addon 调用 oxc 做转译,所以能覆盖更完整的 TypeScript 表面:enumnamespace、参数属性、legacy decorators、TSX 等都可以被处理。对真实业务项目来说,这比“能不能把类型擦掉”更重要,因为很多老项目恰恰用到了这些不那么轻量的 TypeScript 特性。

它增强的是整条开发链路

如果 Nub 只是一个新的 tsx,那它就没那么值得写。

它真正有意思的地方,是把 Node 项目开发时的多个断点串了起来。

第一个是路径解析。

很多项目会在 tsconfig.json 里写:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

编辑器认识,TypeScript 认识,打包器认识,但原生 Node 不一定认识。于是你又要靠额外 loader、运行时补丁,或者在构建阶段重写路径。

Nub 的做法是把 tsconfig paths 接到 Node resolver 上。这样它不只是“编译 TypeScript”,而是在运行时解析模块时就理解项目规则。

第二个是 .env

很多项目启动前都要先接 dotenv/config,或者在命令里写一堆环境变量加载逻辑。Nub 会按类似 Vite / Bun 的习惯,在启动前自动加载 .env*,并支持变量展开。对开发体验来说,这属于小事,但小事多了就是摩擦。

第三个是 watch。

很多 TypeScript runner 最大的问题不是能不能跑,而是 watch 到底准不准。文件实际是内存转译的,Node 看到的可能不是原始 .ts 文件,变更触发就容易不完整。

Nub 的设计是借用 Node 原生 --watch,同时通过 preload 把 load hook 实际碰过的依赖文件报告给 watcher。这样 watch 不是靠你维护一组 glob,也不是凭空猜目录,而是跟着真实依赖图走。

第四个是子进程。

Node 项目里的脚本经常会再启动 node,比如某个 CLI 内部又 spawn 一个 Node 进程。如果外层增强,内层回到原生 Node,就会出现“主命令能跑,子命令又坏了”的体验断层。

Nub 用 PATH shim 处理这件事,让脚本里再次启动的 node 也能继承同一套增强能力。这个点很工程化,听起来不炫,但它决定了一个工具能不能在复杂项目里站住。

快的是工具链,不是你的业务代码

这里一定要把预期说清楚。

Nub 不会让你的业务接口突然快 10 倍。

因为你的业务代码最后还是跑在 Node 上,V8 还是那个 V8,Node 的 I/O 模型也还是那个模型。Nub 不替换 JavaScript 引擎,也不把你的服务变成 Bun。

它快的地方主要在工具链外层。

官方 README 里给了几组很醒目的数据:

命令Nub 对标对象官方描述
nub runnpm run / pnpm run冷路径可比 pnpm run 快约 24 倍
nubxnpx / pnpm dlx可比 npx 轻约 19 倍
nub install包管理安装README 提到使用 pnpm-shaped install engine

为什么这里能快?

原因并不神秘。npmpnpmnpx 本身也是 Node 程序。你执行一个脚本,外面先起一层 JavaScript CLI,CLI 再解析参数、读配置、找 bin、改 PATH、再启动你的脚本。

Nub 把很多调度逻辑放进 Rust CLI:找脚本、找二进制、找 Node、找包管理器、准备环境。这样少掉了不少 wrapper 启动开销。对大型服务的请求性能来说,这不算什么;但对本地开发里高频执行的 runxwatchinstall 来说,体感会很明显。

这也是 Nub 和 Bun 的一个重要差别。

Bun 的快,包含运行时、包管理器、打包器、测试器等一整套新栈。Nub 的快,更像是把 Node 项目前面的“启动和调度层”换成更轻的实现。

它对安全和兼容也有自己的判断

Nub 还有一个容易被忽略的点:它默认收紧了安装阶段的安全策略。

README 里提到,依赖的 build scripts 是 deny-by-default。也就是说,包安装时不是所有 postinstall 都可以默认执行,只有显式允许、可信依赖配置或满足某些信任条件时才会运行。

这件事放在今天的 npm 生态里很有意义。

过去几年,供应链攻击已经不是什么理论风险。安装依赖时自动执行脚本,体验上很方便,但安全边界很松。Nub 把这件事默认收紧,说明它不是只在卷性能,也在重新设计 Node 项目的默认开发边界。

兼容上,Nub 也没有强迫你进入一套全新格式。

它仍然围绕 package.json、现有 lockfile、项目已有 Node 版本约定工作。包安装部分是 pnpm-shaped,README 也强调 npm、pnpm、Bun lockfile 可以 round-trip,Yarn 目前是 read-only。

这对老项目很重要。真正能落地的工具,往往不是让你立刻迁移所有东西,而是允许你一点点接入。

为什么现在会有 Nub 这样的工具?

把 Nub 放到更大的趋势里看,它其实回答了一个过去两年越来越明显的问题:

Node 生态已经足够大,但 Node 的默认开发体验仍然偏“基础设施裸露”。

你想写 TypeScript,要选 runner;想跑脚本,要靠 npm/pnpm/yarn;想管理 Node,要装版本管理器;想统一包管理器,要靠 Corepack;想 watch,要补工具;想加载 .env,又是一层配置。

这些东西单看都能解决,但组合起来就很碎。

Bun 和 Deno 的流行,某种程度上就是开发者在表达一个需求:

我不想再为每个项目重新拼一遍工具链。

但 Bun / Deno 的答案是“换一个更现代的运行时入口”。Nub 的答案是“运行时继续用 Node,我把入口现代化”。

这就是它值得关注的原因。

它不是在和 Node 对着干,而是在承认 Node 的现实统治力之后,做了一层增强。对很多团队来说,这比“彻底迁移”更现实。

什么时候适合试 Nub?

我会把适用场景分成三类。

场景是否适合
新 Node 项目,想少装一堆开发工具很适合试
老项目 TS 配置复杂,但不想切 Bun/Deno值得小范围试
CI 里大量跑 npm/pnpm scripts,冷启动开销明显值得压测
对 install scripts 有供应链安全顾虑值得关注
生产运行时要求极度稳定,不能接受新工具风险先别急着全量切
依赖 Yarn 复杂能力或私有包策略很重需要先验证兼容

最稳妥的试法,不是马上把项目全迁到 Nub,而是从低风险命令开始:

nub run test
nub run lint
nubx eslint .
nub watch src/server.ts

先看脚本行为是否一致,再看本地开发体验是否真的更顺。如果只是为了追热点,把核心 CI 和生产启动方式一次性切过去,反而不符合 Nub “增强层”的定位。

最后:Node 不一定要被替代,它也可以被增强

过去我们讨论 JavaScript 运行时,常常习惯用“谁替代谁”的视角。

Bun 会不会替代 Node?Deno 会不会替代 Node?Node 会不会落后?

但 Nub 给了一个更务实的答案:

有些系统确实适合换新运行时;有些系统更适合继续站在 Node 上,把开发链路一点点补齐。

对已经离不开 Node 的团队来说,这可能是一条很有吸引力的中间路线。你不用放弃 npm 生态,不用重写部署环境,不用把所有历史脚本迁到一个新世界;你只是把 node 前面的那层开发入口换掉,让 TypeScript、脚本、watch、env、paths、Node 版本和包管理变得更像一个整体。

所以我觉得 Nub 真正有意思的地方,不是“它比 Node 更快”,而是它重新定义了一个问题:

既然 Node 还会长期存在,我们能不能把 Node 用得更像 2026 年的工具?

如果这个方向跑通,JavaScript 工具链接下来的竞争就不只是“谁有新的 runtime”,还会是“谁能把已有 runtime 的开发体验补得更完整”。

这对 Node 来说,不是坏消息。

这反而说明:Node 生态太重了,重到没人能轻易绕开;也太重要了,重要到值得有人专门给它做一个 Plus 版本。

参考资料