从 0.1 到 26:一个运行时的十七年进化史

2009 年 5 月 27 日,一个叫 Ryan Dahl 的年轻人往 GitHub 上推了第一个 commit。那是一个用 C++ 写的 JavaScript 运行时,把 Google 开源的 V8 引擎包了一层,加了一个事件循环和一个底层 I/O 接口。初始版本号 0.1.0,只跑在 Linux 和 macOS 上,连 Windows 都不支持。

半年后的 11 月 8 日,Dahl 在柏林的 JSConf EU 上做了第一次公开演示。那场演讲结束时,台下起了 standing ovation。一个在浏览器里被关了十几年的语言,突然有了在服务端运行的可能。

十七年后的 2026 年 5 月 5 日,Node.js 发布了 v26.0.0,代号 Lithium。V8 引擎升到了 14.6,内置了 Temporal API,Undici 升到了 8.0。从一个人写的实验项目,变成了全球服务端 JavaScript 的事实标准基础设施。

这篇文章要做的事情,是把这十七年间的关键变化拆开来看------不是罗列 changelog,而是搞清楚每个版本到底改变了什么,以及为什么要这样改。

架构地基:V8、libuv 和事件循环

要看懂 Node.js 的版本变化,先得理解它的底层架构。这不是一个从零开始的运行时,而是三个东西的组合体。

V8 是 Google 写的 JavaScript 引擎,负责把 JS 代码编译成机器码执行。Node.js 从第一天起就用 V8,这是整个项目能存在的技术前提------2008 年 V8 以 BSD 协议开源,Dahl 才有了搭建 Node.js 的基础。但 V8 本身没有事件循环,没有文件系统访问,没有网络能力,它只是一个 JS 执行器。

libuv 是 Node.js 的异步 I/O 层。最早 Node.js 用的是 libev 和 libeio,但这两个库只支持 Unix 系统。2011 年 Microsoft 和 Joyent 合作做 Windows 原生支持时,需要一个跨平台的异步 I/O 抽象层,于是 libuv 被创造出来。libuv 的事件循环是 Node.js 异步模型的核心,它还维护了一个线程池(默认 4 个线程,可通过 UV_THREADPOOL_SIZE 调整),用来处理那些系统层面没法非阻塞的操作,比如文件 I/O。

第三个组件是一组核心模块------HTTP、TCP、UDP、DNS、TLS/SSL、文件系统、Buffer、Stream、Crypto 等。这些模块构成了 Node.js 的标准库,让你不用装任何第三方包就能写出一个 HTTP 服务器。

理解了这个架构,就能理解 Node.js 十七年的变化本质上是在做什么:不断升级 V8 获取新 JS 特性和性能提升,不断完善 libuv 的异步能力,不断扩展和重构核心模块,以及不断调整模块系统和开发者工具来适应越来越大的生态。

发布周期:从随意到半年一次,再到一年一次

Node.js 的发布策略本身经历了三个阶段。

2009 到 2014 年是"随意发布"阶段。版本号从 0.1 一路蹦到 0.12,没有固定节奏,什么时候准备好什么时候发。v0.6 在 2011 年发布,第一次把 npm 打包进了 Node.js 发行版。v0.8 在 2012 年发布,带来了完整的 Windows 原生支持。v0.10 在 2013 年 3 月发布,成为了第一个被广泛生产使用的版本。v0.12 在 2015 年 2 月发布,是 io.js 分叉前的最后一个版本。

2015 年 v4.0 开始,Node.js 采用了正式的发布周期:每年两个大版本,4 月发偶数版本,10 月发奇数版本。偶数版本在 6 个月的 Current 阶段后升级为 LTS(长期支持版本),获得 12 个月活跃支持加 18 个月维护支持,总共 30 个月的 LTS 生命周期。奇数版本只有 6 个月寿命,用完即弃。这套规则的设计初衷是让企业有可预测的升级节奏,同时给新特性一个实验场。

2026 年 3 月,Node.js 官方宣布从 v27 开始改为年度发布。版本号与年份对齐:27.0.0 在 2027 年发布,28.0.0 在 2028 年发布。取消奇偶区分,每个版本都成为 LTS。原因是十年数据显示奇数版本采纳率极低,大多数用户只升级到 LTS,奇偶区分反而困扰新手。同时维护四五个活跃发布线的负担已经让志愿者团队不堪重负。

v26 是旧发布周期的最后一个版本,也是最后一个"奇数月发布"的版本。从 v27 开始,Node.js 进入一年一个 LTS 的新纪元。

io.js 分叉与合并:Node.js 最危险的一年

2014 年 12 月,一个叫 Fedor Indutny 的核心开发者 fork 了 Node.js,创建了 io.js。原因不是技术分歧,而是治理不满。当时 Node.js 的版权持有方 Joyent 对项目的管理被社区认为过于保守------V8 已经发到 4.1 了,Node.js 还在用老版本,社区贡献的 PR 大量积压没人合。

io.js 采取了开放治理模式,有独立的技术委员会,快速跟进 V8 最新版本,吸引了一批核心贡献者过去。短短三个月内 io.js 就发布了 1.0、2.0、3.0 三个大版本。

2015 年 2 月,在 Linux Foundation 的斡旋下,Node.js Foundation 成立。9 月,io.js v3.3 和 Node.js v0.12 合并成了 Node.js v4.0,代号 Argon。这次合并的意义远超版本号:它不仅统一了社区,还把 io.js 带来的 V8 4.5(支持 ES6 特性)和开放治理模式正式纳入了 Node.js 主线。LTS 发布周期也正是从 v4 开始的。

2019 年,JS Foundation 和 Node.js Foundation 合并,成立了 OpenJS Foundation,成为 Node.js 的新家。

模块系统:CommonJS 与 ESM 的十二年拉锯

Node.js 的模块系统演进是整个版本历史中最纠结的一条线。

CommonJS 是 Node.js 的原始模块系统。你写 require('http'),它同步加载模块,返回 exports 对象。这套系统简单、直观、同步执行,在 2009 年是 JavaScript 在浏览器外运行的事实标准(那时候 ES6 连影子都没有)。

但 CommonJS 有设计上的硬伤:它是同步的,不利于静态分析,不支持 tree-shaking,模块导出的是值的拷贝而非引用。ES6 规范定义了官方的 ES Modules(ESM)标准,用 importexport 语法,支持静态分析和 live bindings。

把 ESM 引入 Node.js 花了六年。

v13(2019 年 10 月)首次支持 ESM。你可以用 .mjs 扩展名,或者在 package.json 里写 "type": "module",就能用 importexport 了。但每次加载都会打印实验性警告。

v14(2020 年 4 月)去掉了 ESM 的实验性警告,ESM 正式成为可用状态。同时支持了 top-level await------在 ESM 模块顶层可以直接写 await,不用包 async IIFE。

但问题来了:CommonJS 和 ESM 之间有互操作性难题。CommonJS 用 require(),ESM 用 import,两者的模块解析算法完全不同。一个 CommonJS 模块想 require() 一个 ESM 模块?不行,因为 require 是同步的,而 ESM 模块加载是异步的。

v22(2024 年 4 月)终于开始解决这个问题:实验性支持 require() 同步加载 ESM 模块。只要 ESM 模块的依赖图是同步可解析的,就可以用 require() 引入它。这个功能由 Joyee Cheung 实现,她在博客里说这是"a feature that has been long overdue"。

v23(2024 年 10 月)把这个功能推广到了更多场景,稳定性进一步提升。

v25(2025 年 10 月)默认开启了 TypeScript type stripping,意味着 Node.js 原生支持 .ts 文件了------不需要 ts-node,不需要 tsx,直接 node app.ts 就能跑。类型注解会被剥离但不做类型检查,你想做类型检查还得靠 tsc。

到 v26,require(esm) 已经从实验走向稳定,CommonJS 和 ESM 之间的墙终于被打通了。十二年的拉锯,算是有了一个务实的结论:两套模块系统共存,互操作尽可能无缝。

V8 引擎升级:每个版本背后的性能引擎

Node.js 的 JavaScript 能力直接取决于 V8 的版本。每次大版本升级都伴随着 V8 更新,带来新的 JS 语法支持和性能改进。以下是主要版本与 V8 版本的对应关系:

| Node.js 版本 | V8 版本 | 带来的关键 JS 特性 |
|------------|---------|---------------------------------------------|---|--------|
| v4 (2015) | V8 4.5 | ES6 基础特性:let/const、箭头函数、class、Promise、模板字符串 |
| v6 (2016) | V8 5.1 | ES6 完整支持,性能优化 |
| v8 (2017) | V8 5.8 | 启动速度大幅提升,Async/Await 支持 |
| v10 (2018) | V8 6.6 | 解析器优化,内存占用降低 |
| v12 (2019) | V8 7.4 | 更快的 async/await,更好的性能 |
| v14 (2020) | V8 8.1 | 可选链(?.)、空值合并(??)、Intl 改进 |
| v15 (2020) | V8 8.6 | 逻辑赋值运算符(&&=、 | | =、??=) |
| v16 (2021) | V8 9.0 | 正则表达式 /d 标志,RegExp 匹配索引 |
| v17 (2021) | V8 9.5 | 性能优化 |
| v18 (2022) | V8 10.1 | Array.findLast()、Array.findLastIndex() |
| v19 (2022) | V8 10.7 | 性能优化 |
| v20 (2023) | V8 11.3 | 数组变更方法:toSorted()、toReversed() 等 |
| v21 (2023) | V8 11.8 | Array.groupBy() |
| v22 (2024) | V8 12.4 | Array.fromAsync(),Set 新方法 |
| v24 (2025) | V8 13.6 | Float16Array,RegExp.escape() |
| v25 (2025) | V8 14.1 | Uint8Array base64/hex 辅助方法 |
| v26 (2026) | V8 14.6 | Map.getOrInsert(),Iterator.concat() |

这张表解释了一件事:你在 Node.js 里能用的新 JavaScript 语法,不是 Node.js 自己发明的,而是 V8 升级带来的。Node.js 的角色是决定何时将新版 V8 引入稳定版。通常 V8 跟随 Chrome 浏览器发布,Node.js 团队会把它适配到自己的构建系统中。

一个值得注意的细节:v14 给 JavaScript 带来了可选链(?.)和空值合并(??)运算符,这两个特性对日常编码影响巨大------你不用再写 obj && obj.prop && obj.prop.value 了,直接 obj?.prop?.value。如果你还在用 v12 或更低版本,你享受不到这些语法糖。

Streams:从手写到对齐 Web 标准

Stream 是 Node.js 最核心的概念之一。任何数据流------HTTP 请求体、文件读写、TCP 连接------底层都是 Stream。

早期的 Stream API(v0.x 时期)设计得比较粗糙。v0.10 引入了 Stream 2,重写了流的背压(backpressure)机制。v0.12 引入了 Stream 3,进一步改进了 push/pushback 逻辑。但 Stream API 一直有一个问题:错误处理不完善,管道泄漏时不会自动清理上游资源。

v10 引入了 stream.pipeline(),解决了 pipe() 的错误处理和资源清理问题。你可以把多个流串起来,任何一个流出错,pipeline 会自动销毁所有流并调用回调。

javascript 复制代码
pipeline(readable, transform, writable, (err) => {
  if (err) console.error('Pipeline failed', err)
  else console.log('Pipeline succeeded')
})

v18(2022 年)是一个分水岭:Node.js 引入了 Web Streams API,与浏览器的 ReadableStreamWritableStreamTransformStream 完全对齐。这意味着你写的流处理代码可以在 Node.js 和浏览器之间无缝迁移。

到 v26,老的 _stream_wrap_stream_readable_stream_writable 等内部模块被完全移除,Stream API 完成了从"Node.js 私有设计"到"Web 标准对齐"的转型。

Worker Threads:打破单线程的限制

Node.js 的事件循环是单线程的,这对 I/O 密集型场景非常高效,但对 CPU 密集型任务是个灾难------一个长时间运行的同步计算会阻塞整个事件循环,所有请求都卡住。

早期的解决方案是 cluster 模块,它允许多个 Node.js 进程共享同一个端口。但 cluster 本质上是多进程,进程间通信靠 IPC,数据不能直接共享内存。

v10(2018 年)引入了实验性的 Worker Threads 模块,在旗标后面开启。它允许在 Node.js 进程内创建真正的工作线程,共享 SharedArrayBuffer 内存。

v11(2018 年 10 月)去掉了旗标要求,Worker Threads 成为不需要特殊标志就能使用的实验性功能。

v12(2019 年 4 月)将 Worker Threads 标记为 Stable。感谢 Anna Henningsen 的工作,这个版本终于让 Node.js 有了处理 CPU 密集型任务的标准方式。

javascript 复制代码
const { Worker } = require('worker_threads')
const worker = new Worker('./heavy-calc.js')
worker.on('message', (result) => console.log(result))
worker.postMessage({ data: 'start' })

Worker Threads 并不意味着 Node.js 变成了多线程运行时。主线程仍然是单线程事件循环,Worker Threads 是补充手段,适用于图像处理、加密计算、数据序列化等场景。

N-API:原生模块的 ABI 稳定性

Node.js 的原生模块用 C/C++ 编写,直接调用 Node.js 的内部 API。在 N-API 出现之前,每次 Node.js 大版本升级,原生模块都需要重新编译,因为 Node.js 内部的 ABI(应用二进制接口)不稳定。这导致 node-gyp rebuild 成为升级 Node.js 版本时最痛苦的环节。

v8(2017 年)引入了实验性的 N-API,提供了一套稳定的 C API,让原生模块不再依赖 V8 的内部 API。这意味着用 N-API 写的模块在 V8 版本升级时不需要重新编译。

v10(2018 年)把 N-API 标记为 Stable,版本号独立于 Node.js 版本号维护。你可以用 N-API 在 C、C++ 甚至 Rust 中编写原生模块,一次编译到处运行。

N-API 的意义在于:它把 Node.js 从"绑定 V8 内部 API"升级为"提供稳定 ABI 层",让原生模块生态不再被 V8 升级牵着走。

内置 Web API:从 polyfill 到原生支持

很长一段时间里,Node.js 缺少浏览器里理所当然的 API。你要发 HTTP 请求?装 node-fetch 或 axios。你要处理 URL?用 url 模块而非 URL 构造器。你要 WebSocket?装 ws 包。

这造成了一个尴尬现实:同一段 JavaScript 代码在浏览器和 Node.js 里跑不通,因为 API 不一样。

v17.5(2022 年初)在旗标后面引入了实验性 fetch API,由 undici 项目提供底层实现。undici 是 Node.js 团队自己写的 HTTP/1.1 客户端,专为 Node.js 的事件循环设计,比基于 node-fetch 的 polyfill 快得多。

v18(2022 年 4 月)把 fetch 设为默认可用的全局 API,不需要任何标志。同时引入了 HeadersFormDataBlob 等关联 API,以及 BroadcastChannel 用于跨进程消息广播。

v21(2023 年 10 月)在旗标后面引入了实验性原生 WebSocket 客户端,以及全局 navigator 对象(提供 navigator.userAgent 等浏览器惯用接口)。

v22(2024 年 4 月)将 WebSocket 设为稳定可用的内置 API,不再需要第三方包就能建立 WebSocket 连接。同时 node:fs 模块内置了 globglobSync 函数,不再需要装 glob 包。

v25(2025 年 10 月)引入了实验性 Web Storage API(localStorage/sessionStorage),进一步缩小与浏览器 API 的差距。

这些变化的战略意图很明确:让 Node.js 尽可能兼容 Web 平台标准,让同构代码(isomorphic code)成为现实而非奢望。

测试运行器:告别第三方测试框架

直到 v18 之前,Node.js 没有内置测试框架。你必须装 jest、mocha、tap 或 vitest。不是不行,但一个运行时连测试都要靠第三方包,总觉得差点意思。

v18(2022 年 4 月)引入了实验性 node:test 模块。不用装任何东西,直接:

javascript 复制代码
const test = require('node:test')
test('basic test', (t) => {
  t.assert.strictEqual(1 + 1, 2)
})

v20(2023 年 4 月)将测试运行器标记为 Stable,并添加了 mock 功能、代码覆盖率支持、以及更丰富的断言 API。

v22(2024 年)进一步增强了测试运行器,支持 glob 模式匹配测试文件、--watch 模式自动重跑。

v24(2025 年)让测试运行器自动等待子测试完成,修复了一个长期困扰开发者的竞态条件。

到 v26,Node.js 内置测试运行器已经可以替代大多数中小型项目的测试框架需求。大型项目可能还需要 jest 或 vitest 的快照测试和 mock 强大能力,但对单元测试和集成测试,node:test 足够了。

权限模型:运行时安全的第一步

Node.js 一直有一个安全问题:任何脚本都可以访问文件系统、网络、环境变量。你装了一个 npm 依赖,它可能在 postinstall 脚本里干任何事------读你的 SSH key,上传到远程服务器,你都不会察觉。

v20(2023 年 4 月)引入了实验性权限模型。你可以用 --allow-fs--allow-envallow-net 等标志限制脚本的访问范围:

css 复制代码
node --allow-fs-read=./data --disallow-net app.js

这个功能还在实验阶段,API 可能会变。但它的方向是对的:Node.js 终于开始像 Deno 那样提供运行时级别的权限控制,而不是把安全责任全甩给操作系统。

v25 进一步增加了 --allow-net 的细粒度控制,可以指定允许访问的主机名白名单。

TypeScript 原生支持

Node.js 对 TypeScript 的态度经历了一个明显的转变。

v22(2024 年 4 月)引入了 --experimental-transform-types 标志,允许 Node.js 直接执行 .ts 文件。但这个方案是用 TypeScript 编译器做完整类型转换,启动速度慢,依赖 tsx 或 ts-node 作为后备。

v23(2024 年 10 月)引入了 --experimental-strip-types,采取了一个更轻量的方案:不做类型转换,只剥离类型注解。类型检查依然需要 tsc,但运行 .ts 文件不再需要任何编译步骤。

v25(2025 年 10 月)把 type stripping 设为默认行为。你直接 node app.ts 就能跑 TypeScript 文件,不需要任何标志。这个功能由 Node.js TSC 成员 Marco Ippolito 贡献,他同时也是 Babel 的维护者。

v26(2026 年 5 月)移除了 --experimental-transform-types,意味着"完整类型转换"路线被放弃,Node.js 明确选择了"只剥离类型,不做类型检查"的方案。你想做类型检查?用 tsc。你想运行 TypeScript?直接 node。

这个决策背后的理念是:运行时不应该承担类型检查的职责,那是编译器的事。Node.js 只需要能执行 TypeScript 代码,类型正确性由开发者在 CI 里用 tsc 验证。

v26 的核心变化

2026 年 5 月 5 日发布的 v26 代号 Lithium,是旧发布周期的收官之作。几个值得关注的变化:

Temporal API 默认启用。Temporal 是 JavaScript 的新一代日期时间 API,设计用来替代臭名昭著的 Date 对象。Date 有大量设计缺陷:月份从 0 开始、时区处理混乱、没有日期范围类型。Temporal 提供了 Temporal.NowTemporal.PlainDateTemporal.ZonedDateTime 等类型,彻底解决这些问题。由 Richard Lau 在 PR #61806 中贡献。

V8 升级到 14.6.202.33,对应 Chromium 146。新增了 Map.prototype.getOrInsert() 和 Iterator.concat() 等特性。getOrInsert 解决了一个常见模式:从 Map 取值不存在时插入默认值,以前要写三行代码,现在一行。

Undici 升级到 8.0,带来了 HTTP 客户端的性能改进和新特性。

一批旧 API 被移除:http.Server.prototype.writeHeader() 被完全删除(用 writeHead() 替代),旧版 Stream 内部模块(_stream_wrap 等)被移除,module.register() 被运行时弃用。这些清理为未来的稳定维护扫清了道路。

GCC 编译器要求提升到 13.2,Python 3.9 不再被支持,构建工具链现代化。

版本代号:从 Argon 到 Lithium

从 v4 开始,每个 LTS 版本都有一个元素代号。这个传统借鉴自 Ubuntu 的命名策略,目的是让版本号更容易记忆和指代。

v4 = Argon(氩) v6 = Boron(硼) v8 = Carbon(碳) v10 = Dubnium(𫟼) v12 = Erbium(铒) v14 = Fermium(镄) v16 = Gallium(镓) v18 = Hydrogen(氢) v20 = Iron(铁) v22 = Jod(𬬢) v24 = Krypton(氪) v26 = Lithium(锂)

按元素周期表顺序排列,但跳过了部分元素------没有 Boron 之后跳到 Carbon 是连续的,从 Dubnium 开始每个代号的首字母按字母表顺序排列(D-E-F-G-H-I-J-K-L),这是有意设计的双重命名规则。

Ryan Dahl 的反思与 Deno 的竞争

2009 年柏林 JSConf 那场演讲九年后,Ryan Dahl 在 2018 年的同一场地做了另一场演讲,标题是 "10 Things I Regret About Node.js"。他列出了 Node.js 设计中的十个遗憾:

  • 没有从第一天就引入 Promise(Promise 在 v0.x 时代不存在,后来加得很别扭)
  • 安全模型缺失(任何包都能访问一切)
  • 构建系统依赖 GYP(太复杂)
  • package.json 的设计(让它变成了包信息的中心,而非简单的依赖列表)
  • node_modules 的递归目录结构(安装慢,磁盘占用大)
  • require 默认加 .js 扩展名
  • 自己写了一套模块系统而不是直接用 CommonJS 规范

基于这些反思,Dahl 创建了 Deno------一个用 Rust 写的、内置 TypeScript 支持的、默认安全的、不支持 npm 的 JavaScript 运行时。2020 年 Deno 1.0 发布,一度被视为 Node.js 的颠覆者。

2024 年,另一个竞争者 Bun 出现,用 Zig 写的 JavaScript 运行时,主打极致的启动速度和 npm 兼容性。

面对竞争,Node.js 的回应不是重建,而是渐进式吸收------Deno 的安全模型启发了 Node.js v20 的权限模型,Deno 和 Bun 的 TypeScript 原生支持推动了 Node.js v25 的 type stripping。Node.js 选择在自己的架构基础上吸收竞争者的优点,而非推翻重来。

2024 年 Dahl 本人在一次访谈中说"写代码的时代结束了"------他的关注点已经从运行时转向了 AI 编程。Node.js 则继续在自己的轨道上运转:2026 年 v26 发布,2027 年 v27 将开启年度发布的新周期。

写在版本号的尽头

从 v0.1 到 v26,Node.js 走了十七年。这十七年的变化可以总结为几条主线:

模块系统从 CommonJS 一家独大,到 ESM 加入后的十二年拉锯,最终以 require(esm) 的互操作方案告一段落。

异步模型从回调地狱,到 Promise 被 V8 带入,到 async/await 成为标配,到 AsyncLocalStorage 提供上下文追踪,到 top-level await 打破最后的限制。

Web API 从缺失到补齐------fetch、WebSocket、Web Streams、AbortController、BroadcastChannel、URL Pattern------Node.js 从浏览器 API 的旁观者变成了 Web 平台标准的实现者。

原生模块从绑死 V8 ABI,到 N-API 提供稳定接口,原生模块不再随 V8 升级而碎。

安全模型从完全没有,到 v20 引入实验性权限控制------这是 Node.js 二十年来最大的安全架构变化。

TypeScript 从必须用 ts-node 编译,到 v25 默认 type stripping 直接运行------Node.js 终于承认 TypeScript 已经是 JavaScript 生态的实际标准。

发布周期从随意发布,到半年一次的奇偶双轨,到 v27 开始的年度发布------版本号将与年份对齐,Node.js 用一种更从容的节奏面对未来。

十七年前 Ryan Dahl 在那个 commit 里写的第一行代码,到今天仍然是 Node.js 的地基。上面的建筑已经换了无数轮,但 V8 引擎加事件循环加异步 I/O 的核心架构,从未被推翻。一个好的架构,大概就是这样------经得起十七年的扩展和重构,而不需要推倒重来。

相关推荐
晓得迷路了2 小时前
栗子前端技术周刊第 140 期 - pnpm、Node 新 API 文档网站、Oxlint...
前端·javascript·node.js
孟陬15 小时前
Node.js 测试报告格式一览和自定义 json 格式 `--test-reporter=spec | tap | dot | junit | lcov`
node.js
陳陈陳2 天前
🚀 前端流式输出革命:SSE + BFF 架构从零到一实战指南
vue.js·架构·node.js
Revolution612 天前
第一次运行 Node.js:终端里的 JavaScript 怎样执行
后端·面试·node.js
半句唐诗2 天前
我是如何通过 Access Token 成功发布第一个 npm 包的
前端·npm·node.js
贩卖黄昏的熊2 天前
NestJS简明教程——异常处理和日志
javascript·node.js·nest.js
Revolution612 天前
Node.js 是什么:前端项目里哪些事情由它完成
前端·面试·node.js
Revolution612 天前
Nest.js 是什么:怎样用它写出第一个后端接口
后端·node.js·nestjs
柳林林2 天前
解决 nvm 切换 Node.js 版本时报“没有文件扩展 ‘.vbs’ 的脚本引擎”错误
node.js