首页 / 文章 / Deno 2.x如何悄然解决了Node兼容性及工具使用带来的困扰。

Deno 2.x如何悄然解决了Node兼容性及工具使用带来的困扰。

本文介绍了Deno的2.0至2.9版本,展示了npm兼容性、权限设置以及内置工具如何消除了曾经导致开发者放弃它的种种障碍。

2160 词

那次试用是在五年前进行的。

上个月,一个临时项目需要一个小型的HTTP服务器。创建一个新目录,执行deno init命令,十分钟后就有一个功能完备的服务器出现了,其中已经集成了TypeScript、测试工具、代码格式化以及代码检查功能。无需编写tsconfig文件,也不需要设置prettier、eslint配置或jest.config.ts,更看不到package.json文件。

查看版本号显示为2.9.3。

原来从2024年10月到2026年7月间,Deno发布了十次小版本更新,每次都默默地解决了当初导致该工具被放弃的那些问题。当时并没有注意到这些更新,因为人们早已在心里将这个工具归类为“好点子,但还不适合实际使用”,也就没有理由再去查看它。

下面将逐一介绍实际发生的变化,针对每一项旧有的抱怨进行说明。

投诉1:npm包被视为二等公民

这对包括我在内的许多开发者来说都是一个难题。在Deno 1.x版本中,任何未发布到deno.land/x或没有正确ES模块URL的包都无法使用。Deno世界与npm世界之间的隔阂似乎将长期存在。

2024年10月发布的Deno 2.0彻底弥合了这一差距。现在你可以直接使用npm:指定符,从npm拥有的一百多万个包的目录中获取任何包:

import express from "npm:express@5";
import { PrismaClient } from "npm:@prisma/client";

如果你更愿意使用package.json,这也是支持的。Deno会解析它,从npm注册表中获取依赖项,如果你开启相应设置,甚至还能生成本地的node_modules文件夹。私有注册表也可以通过.npmrc来使用,方式与在Node中完全相同。

真正能证明其优势的数据是:如今超过75%的Node自带测试套件在Deno下可以成功运行。这并非表面的兼容层——它代表着经过验证的真正兼容性。

决定性的时刻是让Deno处理一个现有的Express项目。无需修改任何导入语句,只需添加一个包含“nodeModulesDir: ‘auto’”字段的deno.json文件,运行deno install后,冷启动时间约为900毫秒,而相同条件下使用npm则需要3秒多(测试项目使用了React、Vite、Babel和ESLint,且缓存已清空)。服务器在首次尝试时就正常运行了。

第二个问题:权限系统在实际使用中很麻烦

从概念上讲,这种权限模型是合理的。但在实际操作中,这意味着每次运行都得重复输入类似这样的内容:

deno run --allow-read=./data --allow-write=./data --allow-net=api.example.com --allow-env main.ts

只要忽略一个标志,进程就会崩溃。再添加一个需要读取环境变量的新依赖,进程同样会再次崩溃。这感觉更像是配置防火墙规则,而非开发一个子项目。

2025年9月发布的Deno 2.5引入了可直接在配置文件中定义的权限集功能。你可以在deno.json"permissions"键下声明命名权限集,然后通过-P标志(即--permission-set的简写)来使用它们:

{
  "permissions": {
    "default": {
      "read": ["./src", "./data"],
      "write": ["./data"],
      "net": ["api.example.com", "0.0.0.0:3000"],
      "env": true
    },
    "test": {
      "read": true,
      "write": ["./tmp"],
      "net": false
    }
  }
}

运行 deno run -P main.ts 时会自动从工作目录中的 deno.json 中加载“默认”权限设置,或者你可以使用 deno test -P=test 为测试单独设置权限。无需修改任何命令行参数,测试代码与生产代码就能拥有不同的权限范围。底层的安全保障并未改变,只是相关的繁琐操作消失了。

此外还有一个 DENO_AUDIT_PERMISSIONS 环境变量,它会产生一个 JSONL 日志,记录程序运行期间进行的每一次权限检查。这样就不必深入研究依赖项的源代码,就能清楚地知道它们试图访问什么内容。

抱怨3:我还是需要一堆额外的工具

Node的优点与缺点在于,几乎所有的功能都需要分配到不同的模块中。需要格式化?用prettier即可。代码检查?有ESLint以及一些插件。测试?可以用Jest或Vitest,再加上用于处理TypeScript转译的配置文件。类型检查?用tsc,其配置与运行代码的程序是独立设置的。代码打包?有六种左右的打包工具可供选择,每种都有自己独特的配置方式。

Deno将所有这些功能都作为内置子命令整合进来。这些子命令早在2.0版本之前就已存在:

deno fmt          # formats TS, JS, JSON, HTML, CSS, YAML, SQL
deno lint         # built-in linter with quick fixes
deno test         # test runner with coverage, snapshots, sharding
deno check        # type checking
deno compile      # single binary, cross-platform, code signing
deno bench        # benchmarking
deno doc          # documentation generation

从2.8版本开始,又增加了六个子命令:

deno audit        # security audit of dependencies
deno audit fix    # auto-upgrade vulnerable packages to nearest patched version
deno why          # explain why a package is installed (traces dependency paths)
deno transpile    # strip types, emit .d.ts declarations
deno pack         # build an npm-publishable tarball from JSR/Deno code
deno ci           # reproducible install for CI (errors without lockfile)

2.9版本则带来了更多新子命令:

deno desktop      # build native desktop apps via webview (experimental)
deno list         # show dependency tree (like npm ls)
deno link/unlink  # local package linking for development

最常被使用的命令是 deno compile。通过构建一个小型 CLI 工具并运行 deno compile --target x86_64-unknown-linux-gnu main.ts,就可以生成一个独立的二进制文件。目标机器上无需安装其他任何组件——只需将其复制到服务器上并执行即可。

注意:生成的二进制文件包含了 V8 引擎以及整个 Deno 运行时,因此普通应用程序的体积大约在 60–100 MB 左右。如果对文件大小较为敏感,可以使用 deno compile --bundle 命令(在 2.8 版本中仍不稳定),该命令会通过高效的代码压缩技术显著减小简单脚本的输出体积。Deno 的官方博客曾展示,使用 --bundle --minify 后,一个基于 lodash 的“hello world”示例程序的体积仅为 1.5 MB。

抱怨 4:迁移真实项目似乎难以实现

改变运行时的最大障碍通常与运行时本身无关,而是锁文件、现有工具所依赖的node_modules结构,以及整个代码库中分散的require()调用。

Deno 2.9引入了锁文件初始化功能。假设某个项目已经通过常见的包管理器锁文件来记录其依赖关系——比如npm的package-lock.json、pnpm的pnpm-lock.yaml、yarn的yarn.lock或bun的bun.lock——但从未生成过deno.lock。在该项目中运行deno install后,它会直接从这些文件中找到对应的锁文件并生成缺失的deno.lock。解析后的版本信息一致,完整性哈希值也相同,因此无需担心版本解析出现偏差。

对于确实需要物理层面的 node_modules 目录的场景——比如原生插件或直接扫描文件系统的工具——将 "nodeModulesDir": "auto" 设置为该值即可让 Deno 自动创建该目录。对于那些基于 npm 的扁平目录结构而非 pnpm 风格的符号链接构建的旧工具,还有 deno.json 中的 "nodeModulesLinker": "hoisted" 这一选项(自 2.8 版本起可用),可用于实现相应的处理。

节点模拟层的设计十分巧妙:一旦安装了 Deno,它就会自动在用户的 PATH 环境变量中添加一个 node 的替代二进制文件,无需任何手动配置。只要没有其他程序提供 node 可执行文件,这个模拟层就会拦截对 Node CLI 的调用,转换参数后再将其传递给 Deno。这样一来,那些仍然使用 node dist/server.js 的 CI 脚本无需任何修改就能继续正常运行。如果不想让此功能自动启用,可以通过 DENO_DISABLE_NODE_SHIM=1 来禁用它。

纯指定符导入——即编写 import fs from "fs" 使其解析为 node:fs——在 2.0 版本中就已引入,并且在 2.9 版本时已完全稳定,无需任何标志或配置即可使用。因此无需回退并重新编写导入语句来进行迁移。

迁移尝试

考虑一个规模较小的 Hono API 项目(Hono 是一种类似 Express 的轻量级 HTTP 框架),它包含大约 15 条路由,通过 Drizzle ORM 连接 Postgres 数据库,并配有后台任务处理机制。该项目原本在 Node 22 环境下使用 pnpm 运行,而整个实验则采用 Deno 2.9.3 进行。

转换过程如下:

  1. 从项目根目录运行 deno install,它会在不到两秒的时间内直接根据现有的 pnpm-lock.yaml 文件生成 deno.lock 文件。
  2. 在新建的 deno.json 文件中添加 "nodeModulesDir": "auto" 这一配置。
  3. 使用 deno run -A src/server.ts 启动应用程序。

运行结果非常理想。15条路由全部正常响应,基于Drizzle的查询也按预期执行,队列工作进程持续在后台处理任务。

不过也有三处出现了问题:

  • 有一个测试文件依赖于jest.mock(),而Deno的内置测试运行器没有对应的功能。更换为手动模拟的方案只需不到五分钟。
  • 有一个依赖在CommonJS文件中使用了__dirname,但Deno却将其作为ES模块加载。解决方法是在该特定包的本地配置中添加"type": "commonjs"
  • 有一个工作脚本在未显式导入process模块的情况下读取了process.env.NODE_ENV。由于Deno从2.0版本起就将process作为全局对象暴露,这部分实际上运行正常——但该脚本的权限标志中并未包含env: true,因此需要添加此项。
  • 从开始到结束,整个转换过程大约耗时25分钟。根据在50次测试中使用hyperfine工具进行的测量,冷启动时间从约620毫秒降至320毫秒;空闲内存使用量(RSS)则从142 MB下降到64 MB。此外,还有四个独立的配置文件可以删除:prettier、eslint、jest和tsconfig。

    仍存在的缺陷

    这篇文章的标题提到了“我所厌恶的一切”,而前面列出的那些抱怨确实是 Node 开发者普遍存在的真实问题。不过,还有几项新的注意事项值得注意:

    • Node API 的覆盖范围并不完整。 Node 测试套件的通过率为 75%,这意味着仍有四分之一的测试未通过。这种差距可能不会影响常规的工作负载,但那些依赖 node:vm 的特殊功能、程序化使用 node:inspector,或深入运用 node:cluster 功能的人,应事先在 node-test-viewer.deno.dev 上查看兼容性情况。
  • 编译后的原生模块仍然需要一个本地的 node_modules 文件夹。任何使用 C++ 绑定的包——如 sharp、bcrypt、sqlite3 以及类似软件——都需要同时设置 nodeModulesDir 参数和启用 --allow-ffi 标志。虽然这样可行,但多了一个容易被人忽略的设置步骤。
  • 除非特别说明,否则安装时的脚本会被阻止运行。那些会执行 postinstall 钩子的包——比如 node-gyp 的编译步骤或 prisma generate 命令——需要通过 --allow-scripts=npm:package-name 明确授予权限。这是出于安全考虑的刻意设计,但可能会在团队进行迁移时造成意外困扰。
  • 仍有许多工具认为自己在与 Node 交互。一些实用程序会检查process.versions.node并据此调整行为,或者直接硬编码文件路径,如/node_modules/.cache。Deno 的 pnpm 风格符号链接模块结构让部分工具出现故障。虽然存在提升模式选项可作为临时解决方案,但这只是权宜之计,并非根本解决方式。
  • 上述问题都还不至于严重到阻碍上述迁移过程。不过对于其他项目而言可能就会构成障碍,因此最好在迁移前就进行验证,而非在迁移过程中才发现问题。

    为何这些修复未被注意到

    在21个月内发布了10个次要版本,每个版本都默默解决了三到五个问题。没有一次大规模的重写,也没有“Deno 3.0”发布活动。团队在2.5版本中引入了细粒度的权限设置,在2.8版本中实现了与npm相当的安装速度,还在2.9版本中添加了锁文件功能,将这些都视为常规维护而非重大新闻。

    这与框架升级通常的宣传方式形成对比:一篇博客文章、一场会议演讲、一份迁移指南,再加上社交媒体上的大量评论、激进观点以及对这些观点的反驳。Deno则跳过了整个公开讨论的流程,直接发布了修复补丁。

    那些早期就忽视该运行时的开发者,往往是因为一旦形成观点后就不再随着工具的成熟而重新审视它。这是注意力不集中,而非项目本身的缺陷。

    对于那些在 Deno 2.0 版本之前就对其有印象的人而言,那时的测试版本实际上已经不复存在。如今的 Deno 更不像“一个无法使用 npm 的有趣 TypeScript 运行时”,而更像是“去除了诸多冗余功能的 Node”。当时出现的种种问题确实令人困扰,但这些都已经得到解决。值得再仔细看看。

    相关阅读

  • TypeScript 6在通往原生TS 7编译器的道路上的桥梁作用 — 了解TypeScript 6如何更新默认配置、模块解析机制及导入语法,为代码库适配更快、基于Go语言的TypeScript 7编译器做好准备。
  • JavaScript在2026年的变革:运行时环境、TypeScript 7与Rust工具链 — 梳理2026年JavaScript生态系统的变化——Bun、Deno与Node.js之间的竞争,基于Go语言重写的TypeScript,以及由Rust驱动的构建工具——并解释对开发者而言真正重要的内容。
  • NestJS vs Node.js:为何在大规模应用中结构优于自由度 — 本文阐述了NestJS如何将架构、依赖注入和规范构建在Node.js之上,从而解决纯Node或Express无法处理的可维护性难题。