Deno 2.8 实测:6 个新命令 + Node 兼容性飙到 76%,它想干掉 npm
上周四 Deno 2.8 发布了。我花了两天时间把新功能全跑了一遍,说说实际感受。
先说结论
这个版本最值得关注的不是某个单一功能,是整体的工具链补全。6 个新子命令(deno audit fix、deno ci、deno pack、deno transpile、deno bump-version、deno why),加上 CLI 默认识别 npm 包名、Node.js 测试套件通过率从 42% 涨到 76.4%------加在一起看,Deno 在包管理器这个方向上已经补齐了 npm/yarn/pnpm 的大部分能力。
deno audit fix:自动修漏洞
2.6 版本加了 deno audit,能扫描 npm 依赖的已知漏洞。2.8 多了个 fix,直接帮你升级到补丁版本。
bash
$ deno audit fix
╭ body-parser vulnerable to denial of service when url encoding is enabled
│ Severity: high
│ Package: body-parser
│ Vulnerable: <1.20.3
╰ Info: https://github.com/advisories/GHSA-qwcr-r2fm-qrc7
Found 2 vulnerabilities
Severity: 0 low, 1 moderate, 1 high, 0 critical
Fixed 1 vulnerability:
body-parser 1.19.0 -> 1.20.3
1 vulnerability could not be fixed automatically:
express (major upgrade to 5.0.0)
我拿一个老项目试了。里面有 12 个 npm 依赖,deno audit 扫出来 3 个漏洞,deno audit fix 修掉了 2 个。剩下 1 个需要 express 从 4.x 升级到 5.0,属于 breaking change,工具没帮你做------这个设计是对的,major 升级不该自动搞。
踩坑点:deno audit fix 只处理 npm 依赖,jsr 包目前不走这条路。如果你的项目纯 Deno 生态,这个命令暂时用不上。
deno ci:CI 环境一行搞定
之前在 Docker 或 GitHub Actions 里装依赖,要写 deno install --frozen,还得确认 lockfile 存在。现在一个命令:
bash
$ deno ci
它会做三件事:
- 检查
deno.lock是否存在,没有直接报错 - 删除已有的
node_modules - 用
--frozen模式安装,lockfile 和 config 不一致就失败
实际用起来比 npm ci 体验好。我在 GitHub Actions 里把 npm ci 换成 deno ci,工作流跑通了。Dockerfile 里也一样,原来三行变一行:
dockerfile
# 之前
RUN npm ci --production
# 如果用 deno
RUN deno ci --prod
deno pack:TypeScript 直接打包成 npm 包
这个我觉得是最有意思的新功能。假设你有一个 Deno 项目:
json
// deno.json
{
"name": "@scope/my-lib",
"version": "1.0.0",
"exports": "./mod.ts"
}
跑一下 deno pack,直接生成一个 .tgz,里面有:
- 转译好的 JavaScript
.d.ts类型声明- 自动生成的
package.json - 模块说明符自动改写(
jsr:@std/path变成@jsr/std__path,npm:express@4变成express)
bash
$ deno pack
Creating scope-my-lib-1.0.0.tgz
$ deno pack --dry-run # 看看会生成什么,不实际打包
我试着把一个 Deno 工具库用 deno pack 打包后发到 npm,Node 项目 npm install 后能直接用。以前做这件事要 tsc + tsup 或者 unbuild,配一堆东西。现在零配置。
有个细节:如果你的代码里用了 Deno.* API(比如 Deno.readTextFile),打包时会自动加上 @deno/shim-deno 作为依赖,让包在 Node 下也能跑。不想要这个 shim 可以加 --no-deno-shim。
deno why:追踪依赖来源
装了一堆依赖后,有时候会发现 node_modules 里冒出一些不认识的包。deno why 帮你追踪某个包是被谁拉进来的:
bash
$ deno why qs
qs@6.14.2
npm:express@4 > qs@6.14.2
qs@6.15.1
npm:express@4 > body-parser@1.20.5 > qs@6.15.1
同时支持 JSR 依赖:
bash
$ deno why @std/path
@std/path@1.1.4
jsr:@david/dax@0.43 > @std/path@1.1.4
jsr:@david/dax@0.43 > @david/path@0.2.0 > @std/path@1.1.4
jsr:@david/dax@0.43 > @std/fs@1.0.23 > @std/path@1.1.4
功能上跟 npm explain / pnpm why 一样,但 Deno 的好处是 npm 和 jsr 两套生态都能追踪。
deno transpile:纯粹的 TS 转 JS
有时候你只想把 TypeScript 转成 JavaScript,不要打包、不要模块改写、不要任何多余的事。deno transpile 干的就是这个:
bash
$ deno transpile greeter.ts -o greeter.js
输入:
typescript
interface User {
name: string;
balance: number;
}
export function greet(user: User): string {
return `Hello ${user.name}, you have $${user.balance.toFixed(2)}`;
}
输出:
javascript
export function greet(user) {
return `Hello ${user.name}, you have $${user.balance.toFixed(2)}`;
}
类型信息干净地剥掉了,其他什么都没动。还能加 --declaration 同时输出 .d.ts,加 --source-map inline 输出 source map。
我在一个场景下用到了它:有个内部工具需要在不支持 TS 的运行时里跑(一个旧版嵌入式 V8),用 deno transpile 批量转换后直接扔进去,没出问题。以前用 esbuild --loader=ts 也行,但 deno transpile 不改模块结构,更干净。
Node 兼容性:42% → 76.4%
这个版本最"硬"的数据。Deno 跑 Node.js 自己的测试套件,通过率从 2.7 的 42% 涨到 76.4%(4457 个测试过了 3405 个)。500 多个 commit 改了几乎所有 node: 模块。
对比 Bun 1.3.14 的数据:40.6%(1810/4457)。
这意味着什么?你拿一个中等复杂度的 Node 项目,不改代码直接用 deno run 跑,成功的概率比以前高了不少。我试了几个场景:
- Express 4.x 的 hello world:能跑
- Fastify 基本路由:能跑
- 用了
node:crypto做哈希:能跑 node:child_process调用外部命令:能跑node:worker_threads做简单并行:能跑
失败的场景也有:
- 一些依赖 native addon 的包(
.node文件) - 涉及
node:vm沙箱隔离的高级用法 - 某些
node:net的边缘 case
性能数据
这组数据挺猛的,都是官方在 Linux 上对比 Deno 2.7 测的:
| 指标 | 2.7 | 2.8 | 提升 |
|---|---|---|---|
| 冷启动 npm install | 3319ms | 906ms | 3.66x |
| node:buffer base64 | 2594ms | 844ms | 3.07x |
| node:http 吞吐量 | 8339 req/s | 18431 req/s | 2.21x |
| node:crypto scrypt | 1533ms | 724ms | 2.12x |
| node:http p99 延迟 | 20.86ms | 11.89ms | 1.75x |
冷启动 npm install 3.66 倍提升是怎么做到的?四个优化叠加:
- 精简的包元数据请求 :npm registry 有个精简版 metadata 接口(
application/vnd.npm.install-v1+json),只返回 resolver 需要的字段。之前 Deno 没用这个,现在用了。 - 并行依赖解析:之前解析器一次走一个父节点,现在并发展开所有分支。
- 解压从事件循环搬到线程池:大包的 gzip 解压以前会阻塞事件循环,现在丢到后台线程。
- tarball 解压拆成 CPU 和 I/O 两阶段:解压和写磁盘分开跑,换了更快的 gzip 解码器(libdeflater 替代 flate2)。
base64 的 3.07 倍提升更简单------换了 simdutf 库做编解码,一个 PR 搞定。
import defer:延迟模块执行
TC39 的新提案,Deno 率先支持了。用法:
javascript
import defer * as heavy from "./heavy-module.js";
console.log("程序启动");
// heavy-module.js 的顶层代码还没执行
console.log(heavy.someValue);
// 这时候才真正执行 heavy-module.js
模块会被加载和解析,但顶层代码推迟到你第一次访问导出值时才执行。对启动时间敏感的场景有用------比如一个 CLI 工具有 20 个子命令,每个子命令依赖不同的重模块,用 import defer 可以只在命中对应子命令时才付执行成本。
实际用来替代 npm 可行吗
我花了半天把一个中等规模的 Node 项目(Express + Prisma + Bull 队列)的包管理器从 pnpm 换成 deno。
操作很简单:
bash
# 删掉 node_modules 和 pnpm-lock.yaml
rm -rf node_modules pnpm-lock.yaml
# 用 deno 安装
deno install
deno 读 package.json,生成 deno.lock,创建 node_modules。安装速度确实快,冷缓存下比 pnpm 快了 2-3 倍(pnpm 已经很快了)。
但是------代码还是用 node 跑的,不是用 deno run。Prisma 的 CLI 和 Bull 的 Redis 连接那块,直接用 Deno 运行时还跑不通。所以现在的状态是:Deno 当包管理器用,Node 当运行时用。听起来有点奇怪,但确实可行,而且安装速度实打实地快。
小结
Deno 2.8 的策略很清楚:不要求你一步到位把项目迁移到 Deno,而是一点一点蚕食 Node 工具链。先当你的包管理器,再当你的安全审计工具,再当你的打包工具......等你发现大部分工具都在用 Deno 的时候,切换运行时就是最后一步了。
从实测来看,deno ci、deno pack 和 deno audit fix 这三个命令的完成度很高,可以在生产项目里用。Node 兼容性 76% 说明大部分常见场景已经覆盖了,但复杂项目(native addon、底层网络操作)还得等。
值得试试的场景:
- CI/CD 里用
deno ci替代npm ci - 有 Deno/JSR 库想发布到 npm 的,用
deno pack - 纯前端项目只需要包管理器的,用
deno install替代 pnpm