万字深度解析 Deno 2.9:当 TypeScript 运行时成为「全能开发平台」——从桌面应用到供应链安全的完整技术指南(2026)
前言
2026年6月25日,Deno 正式发布 v2.9。这个版本不是简单的修修补补,而是一次质的跨越:它让 Deno 从一个「更安全的 Node.js 替代品」,进化成了一个真正意义的全能开发平台。
你可以在一个 Deno 项目里:
- 用 TypeScript 写后端 API(Deno.serve)
- 用同样的代码写桌面应用(deno desktop)
- 直接导入 CSS 模块
- 无缝复用 Node.js 生态的依赖(package-lock.json、bun.lock、pnpm-lock.yaml、yarn.lock)
- 在测试框架里用参数化测试和快照断言
- 自动防御供应链攻击(24小时最低发布年龄 + no-downgrade 信任策略)
冷启动时间从 34ms 砍到 17ms,内存占用从 197MB 降到 63MB(1 MiB 响应体场景下),HTTP 吞吐量提升 27%。
本文从架构设计、核心特性、代码实战、性能基准测试和工程实践五个维度,全面解析 Deno 2.9 的技术细节。
一、背景:Deno 的进化史与 v2.9 的定位
1.1 Deno 为何而生
2018年,Ryan Dahl 在 JSConf EU 上发表了著名的「我造 Node.js 的十个遗憾」演讲,随后启动了 Deno 项目。Deno 的核心设计哲学是:
- 默认安全:不经过显式授权,脚本无法访问文件系统、网络、环境变量
- TypeScript 原生:无需额外构建步骤,直接运行 .ts 文件
- 标准库优先:内置 HTTP、文件 I/O、加密等标准模块,减少外部依赖
- ES Modules 优先:废弃 CommonJS 的 require(),拥抱浏览器一致的 import/export
- 去 package.json:依赖通过 URL 直接导入,由 deno.lock 保证可复现性
这些设计选择让 Deno 在安全性、开发体验和可维护性上远超 Node.js,但在生态兼容性和包管理层面,Deno 一直面临「曲高和寡」的困境:大量 npm 包无法直接运行,开发者迁移成本极高。
1.2 v2.9 的转折意义
v2.9 是 Deno 历史上最重要的版本之一,因为它解决了最关键的生态问题:
- Node.js 兼容性从 42% 跃升至 76.4%(高于 Bun)
- deno desktop 正式登场:一个命令就能把 Web 项目打包成跨平台桌面应用,无需 Electron 或 Tauri
- 锁文件直读:package-lock.json、bun.lock、pnpm-lock.yaml、yarn.lock 全部无缝导入
- 供应链安全成为默认特性:最低发布年龄、no-downgrade 信任策略等 npm 生态梦寐以求的功能直接内置
换句话说,Deno 不再只是一个「更干净的 Node.js」,而是一个横跨后端、桌面、CLI 的统一开发平台。
二、deno desktop:桌面开发的范式转移
2.1 现有方案的痛点
Electron 和 Tauri 是当前桌面开发的主流选择,但各有各的烦恼:
Electron 的问题:
- 体积巨大:Chromium 打包进去,一个「Hello World」就是 150MB+
- 内存占用高:每个 Electron 实例就是一个完整的浏览器进程
- 开发体验割裂:主进程用 Node.js,渲染进程用浏览器 API,IPC 通信繁琐
Tauri 的问题:
- 前端框架绑定 WebView,系统 WebView 能力受限
- Rust 后端对前端工程师有较高门槛
- 插件系统复杂,调试困难
2.2 deno desktop 的设计思路
Deno 2.9 推出的 deno desktop 彻底重新思考了这个问题。它的核心理念是:
UI 运行在 webview,逻辑运行在 Deno,输出是单一可分发二进制
这意味着:
- 无需引入 Chromium(除非使用 --backend cef)
- 无需学习 Rust 或复杂的 IPC
- 前端工程师用自己熟悉的 Web 技术栈就能开发桌面应用
- 输出的二进制包含你的代码和静态资源,与框架无关
2.3 最简应用:零配置桌面窗口
// main.ts
Deno.serve(() =>
new Response(
"<!DOCTYPE html><h1>Hello from Deno desktop 👋</h1>",
{ headers: { "content-type": "text/html" } },
)
);
$ deno desktop main.ts
运行这条命令,Deno 会:
- 启动一个 Deno.serve 服务器,绑定到 webview 自动分配的端口
- 打开一个原生窗口,加载该页面
- 服务器和窗口自动配对,无需手动指定端口
这就是一个完整的桌面应用——没有 package.json,没有 Electron 进程,没有 webpack 配置。
2.4 框架自动检测
deno desktop 内置了与 deno compile 相同的框架检测逻辑。如果你有一个 Next.js、Astro、Fresh、Remix、Nuxt、SvelteKit、SolidStart、TanStack Start 或 Vite SSR 项目,只需:
$ deno desktop # 自动检测当前目录框架
$ deno desktop --hmr # 开发模式开启热模块替换
Deno 会自动构建你的框架项目并打包成桌面应用。
2.5 原生桌面 API
对于更复杂的应用,Deno 2.9 提供了一套完整的原生桌面 API,全部在 Deno.* 命名空间下,无需安装任何依赖:
Deno.BrowserWindow:窗口控制
// 创建自定义窗口
const win = new Deno.BrowserWindow({
title: "My Deno App",
width: 1200,
height: 800,
x: 100,
y: 100,
});
// 控制窗口行为
await win.setAlwaysOnTop(true);
await win.maximize();
await win.setMenuBar([
{ label: "File", submenu: [...] },
{ label: "Edit", submenu: [...] },
]);
// 绑定 Deno 函数到渲染进程
win.bind("doThing", async (args: string) => {
// 在 Deno 端处理逻辑
const result = await processData(args);
return result;
});
// 打开 DevTools 辅助调试
await win.openDevTools();
Deno 与渲染进程的通信
在 Deno 主进程中绑定函数:
// main.ts
Deno.serve(async () => {
const win = new Deno.BrowserWindow();
await win.openDevTools();
// 绑定一个 Deno 函数到窗口
win.bind("getFileContent", async (path: string) => {
return await Deno.readTextFile(path);
});
win.bind("runAnalysis", async (config: AnalysisConfig) => {
return await analyze(config);
});
return new Response("<html>...</html>", {
headers: { "content-type": "text/html" }
});
});
在页面的 JavaScript 中调用:
// page.js(渲染进程)
const content = await bindings.getFileContent("/tmp/data.csv");
const result = await bindings.runAnalysis({ mode: "fast", limit: 1000 });
console.log(result);
Deno.Tray:系统托盘图标
const tray = new Deno.Tray();
tray.setIcon(iconBytes);
// 创建托盘面板(轻量级悬浮窗口)
const panel = tray.attachPanel({ url: "https://localhost:8000/panel" });
panel.window.bind("doThing", async () => {
// 处理托盘面板的交互
});
对话框原生化
prompt()、alert()、confirm() 在 deno desktop 中自动渲染为原生对话框:
// 渲染进程
const name = await prompt("请输入你的名字:");
const confirmed = await confirm("确定要删除吗?");
const choice = await alert({
title: "操作确认",
message: "此操作不可撤销",
detail: "文件将在 30 天后永久删除",
});
自动更新:Deno.autoUpdate
// 主进程
const updater = new Deno.autoUpdate({
checkInterval: 3600_000, // 每小时检查一次
onAvailable: (info) => {
console.log(`发现新版本: ${info.version}`);
},
onDownloaded: () => {
console.log("更新已下载,重启后生效");
updater.restart();
},
});
await updater.start();
2.6 双渲染引擎:webview vs CEF
Deno desktop 支持两种渲染引擎:
| 特性 | webview(默认) | cef(Chromium Embedded) |
|---|---|---|
| 渲染引擎 | 系统自带 WebView | 捆绑完整 Chromium |
| 二进制体积 | 小(几十 MB) | 大(+数百 MB) |
| 启动速度 | 快 | 较慢(需解压 Chromium) |
| 渲染一致性 | 依赖系统 WebView 版本 | 全部用户一致 |
| 适用场景 | 大多数应用 | 需要最新 Web 特性的应用 |
$ deno desktop main.ts # 使用系统 webview(默认)
$ deno desktop --backend cef main.ts # 捆绑 Chromium
2.7 跨平台构建:从一台机器发布到三个平台
这是 deno desktop 最令人惊喜的能力:在一台 Linux CI 机器上,可以直接交叉编译出 Windows 和 macOS 的安装包:
# 构建当前平台的安装包
$ deno desktop --output MyApp.dmg main.ts
# 交叉编译到 Windows
$ deno desktop --target x86_64-pc-windows-msvc main.ts
# 一条命令构建所有平台(Linux x64/arm64, Windows x64, macOS x64/arm64)
$ deno desktop --all-targets main.ts
# 压缩自解压包(体积更小)
$ deno desktop --compress main.ts
支持的目标格式:
- macOS:
.app、.dmg - Windows:
.exe、.msi - Linux:
.AppImage、.deb、.rpm
2.8 真实案例:denidian 笔记应用
GitHub 上已有真实的开源案例:denidian,一个基于 deno desktop 构建的笔记应用。它的架构充分展示了 deno desktop 的能力:
// denidian 简化架构
import { Database } from "@db/sqlite";
Deno.serve(async (req) => {
const url = new URL(req.url);
if (url.pathname === "/api/notes") {
const db = new Database("notes.db");
const notes = db.query("SELECT * FROM notes");
return Response.json(notes);
}
// Serve the frontend
return new Response(await Deno.readFile("public/index.html"));
});
通过 Deno KV(内置键值存储)配合 SQLite,以及前端使用熟悉的 React/Vue,整个应用的代码量可能只有等效 Electron 应用的 1/3。
三、性能革命:从「能用」到「标杆」
3.1 性能数据总览
Deno 2.9 在性能上实现了全面突破,以下数据均来自 Deno 官方基准测试(专用 x86_64 Linux 机器,server 和负载生成器绑定到独立核心,oha 中位数取3次运行):
| 指标 | Deno 2.8 | Deno 2.9 | 提升幅度 |
|---|---|---|---|
| 冷启动时间 | 34.2 ms | 17.3 ms | 1.98x 更快 |
| Deno.serve(真实请求) | 56.8k req/s | 72.4k req/s | 1.27x 更快 |
| Deno.serve(纯文本) | 77.0k req/s | 85.6k req/s | 1.11x 更快 |
| Deno.serve(1 MiB 响应体) | 1,617 req/s | 1,907 req/s | 1.18x 更快 |
| RSS 内存(真实请求) | 142 MB | 64 MB | 2.2x 更省 |
| RSS 内存(1 MiB 响应体) | 197 MB | 63 MB | 3.1x 更省 |
3.2 冷启动优化:17ms 的秘密
Deno 2.8 的冷启动时间是 34ms,已经比 Node.js 快不少,但 Deno 团队认为还有大量优化空间。v2.9 通过四项技术实现了 1.98x 的冷启动提升:
1. Node 全局变量懒加载
在 Deno 2.8 中,node: 前缀的所有全局变量(process、Buffer、EventEmitter 等)都在快照加载时就初始化了。但大多数 Deno 脚本并不使用 Node.js 兼容 API,这部分初始化完全是浪费。
v2.9 将这些全局变量的初始化推迟到首次实际使用时。只有当代码中真正出现 node:* 的 import 或 process 引用时,才加载对应的 Node.js 兼容层。
// 只有当这段代码实际执行时,才会初始化 node:process
import process from "node:process";
console.log(process.version); // 首次使用时才触发加载
2. V8 代码缓存
Deno 2.9 为懒加载的 ESM 模块添加了 V8 代码缓存。当一个模块首次被 V8 编译后,编译后的字节码会被缓存到磁盘。下次启动时,直接加载缓存的字节码,无需重新解析和编译:
# 第一次启动(无缓存)
$ time deno run server.ts
real 0m0.234s
# 第二次启动(V8 代码缓存命中)
$ time deno run server.ts
real 0m0.067s
3. 最小化快照
Deno 的核心运行时会打包成一个快照(snapshot)以加速启动。v2.9 对快照进行了「极简化」处理,移除了不必要的预加载模块,使快照体积更小、加载更快:
# 对比快照大小(示意)
$ ls -lh .deno/snapshots/
deno_main-2.8.bin 24.5 MB
deno_main-2.9.bin 18.1 MB # 少了 26%
4. macOS 优化:chained fixups
在 macOS 上,可执行文件的符号链接修复(fixup)过程耗时较长。v2.9 重新设计了 macOS 构建流程,通过 chained fixups 技术显著减少了 pre-main 阶段的时间消耗。
3.3 内存优化:3.1x 的降幅从哪来
Deno 2.9 的内存优化堪称惊人:1 MiB 响应体场景下,内存从 197MB 降到 63MB,降幅达到 3.1 倍。这个提升来自于 V8 引擎层面的改进和 Deno HTTP 栈的重构。
关键洞察:内存不随工作负载增长
在 2.8 中,RSS(Resident Set Size)随响应体大小线性增长:服务纯文本时约 94MB,服务 1 MiB 响应体时达到 197MB。这说明 V8 的堆管理策略在处理大型响应时存在内存碎片化问题。
2.9 中,这一问题通过以下方式得到解决:
- Deno 自有的 HTTP/1.1 服务路径:放弃了部分依赖 Rust 网络库的路径,改为 Deno 直接管理连接和缓冲区
- V8 堆配置优化:针对 HTTP 服务器场景重新调优了 V8 的堆分配策略
- 响应体流式处理:1 MiB 的大响应体不再一次性全部加载到堆内存,而是通过流式处理分块传输
3.4 HTTP 吞吐量:27% 提升
Deno.serve 的真实请求吞吐量从 56.8k req/s 提升到 72.4k req/s,主要来自于一个关键变化:Deno 自有的 HTTP/1.1 服务路径(PR #34446)。
之前,Deno.serve 的 HTTP/1.1 处理部分借助了 Rust 的 hyper 库,虽然高效但并非最优路径。2.9 版本中,Deno 团队用 Rust 重写了 HTTP/1.1 的核心路径,配合 Deno 的异步任务调度器,实现了更低的上下文切换开销。
同时,crypto.subtle 和 console(Deno.inspect)这两个高频调用的 API 也从 JavaScript 迁移到了 Rust 层(PR #34966、#35087),进一步降低了 JavaScript ↔ Rust 边界穿越的性能损耗。
四、供应链安全:内置的防御机制
4.1 npm 生态的安全困境
npm 生态的供应链攻击在 2025-2026 年间愈演愈烈:
- 2025年8月:多个主流 npm 包因维护者令牌被盗,发布了大版本恶意代码
- npm 洪水攻击:恶意包通过大量小版本发布进行混淆
- 依赖混淆攻击:攻击者在公开仓库发布与私有包同名的不安全版本
传统的防御手段是依赖人工审核和 Snyk/Dependabot 等工具,但这些都存在滞后性问题。Deno 2.9 在运行时层面内置了两层防御。
4.2 最低发布年龄:24小时安全窗口
这是 Deno 最早在 2.6 版本引入的功能,在 2.9 中升级为默认启用,窗口期为 24 小时:
恶意包(年轻 <24h)→ Deno 拒绝安装
正常包(成熟 ≥24h)→ 正常安装
原理:绝大多数 npm 供应链攻击包的从发布到被发现、被 npm 官方下架的时间窗口通常不超过 24-48 小时。通过强制等待 24 小时,Deno 确保恶意包在被发现前就已经被 npm 撤回。
配置方式(在 .npmrc 中):
# 采用默认 24 小时窗口
# 无需任何配置,已为默认值
# 自定义为 72 小时(更安全但可能影响新版本采纳)
min-release-age=72h
# 完全禁用(不推荐)
min-release-age=0
这个配置与信任策略配合使用时效果最佳(见下节)。
4.3 no-downgrade 信任策略
Deno 2.9 引入了 npm 生态中首创的 no-downgrade 信任策略,专门防御「被盗维护者令牌」类型的攻击:
# .npmrc
trust-policy=no-downgrade
信任等级排序(从高到低):
- 分阶段发布(staged rollouts):维护者通过实时 2FA 挑战审核后发布,信号最强
- 可信发布(trusted publishing):由 CI/CD 流水线通过 OIDC 证明的发布
- 来源证明(provenance attestation):npm 注册表记录的包来源信息
- 普通发布(plain token publish):维护者令牌直接发布,信号最弱
工作原理:如果某个包的某个历史版本是通过「可信发布 + 2FA」发出的,而新版本突然变成了「普通令牌发布」,Deno 会拒绝安装这个新版本,因为它可能来自被盗的令牌。
# 示例:某包从可信发布突然变成普通发布
$ deno install npm:popular-package
error[deno/npm]: Refusing to install 'popular-package@2.0.0' because it was
published with lower trust evidence (plain token) than the latest higher-trusted
version (trusted publishing, see npm registry provenance).
To proceed, set 'trust-policy=off' in .npmrc (not recommended).
4.4 锁文件完整性:git 合并冲突自动解决
Deno 2.9 还解决了 monorepo 中的一个老大难问题:deno.lock 文件中的 git 合并冲突自动解析。
之前,两个分支同时引入新依赖后,在 git merge 时 deno.lock 会产生冲突标记,此时 Deno 会直接报错,迫使开发者手动解决。2.9 版本实现了智能合并策略:
- 加性变更(同一依赖的不同版本):合并两者(保留更高版本)
- 真正的冲突:保留两者的版本信息,由开发者确认
五、Node.js 生态迁移:零摩擦的跨越
5.1 锁文件直读:你的版本不会变
从 Node.js 生态迁移到 Deno 最怕什么?最怕的是「迁移后依赖版本全变了」,导致一些依赖在新版本中出现了不兼容的变化。
Deno 2.9 的锁文件直读功能彻底解决了这个问题:
$ cd my-existing-node-project
$ deno install
Seeded deno.lock from package-lock.json
Deno 会:
- 读取
package-lock.json(或pnpm-lock.yaml、yarn.lock、bun.lock) - 提取每个依赖的精确版本号 + integrity hash
- 在 deno.lock 中写入完全相同的版本和 hash
- 从 npm.jsr.io 解析这些精确版本
没有任何重新解析,没有版本升级,你的 Deno 项目跑的就是之前 Node.js 项目跑的同一个版本。
5.2 pnpm 工作区自动迁移
pnpm 的工作区配置不在 package.json 里,而是单独放在 pnpm-workspace.yaml。这导致 Deno 之前无法识别 pnpm monorepo,解析依赖时直接报错。
Deno 2.9 实现了自动迁移:
$ deno install
Detected pnpm-workspace.yaml, migrating to deno.json...
pnpm-workspace.yaml packages/catalog → deno.json catalogs
pnpm-workspace.yaml packages/* → deno.json workspaces
? Continue migration? [Y/n] Y
Migration complete. Run 'deno install' again.
Deno 会将 pnpm-workspace.yaml 中的 packages/ 和 catalog 配置迁移到 deno.json 的 workspaces 和 catalogs 字段,同时保留原有配置文件的注释和格式不变。
5.3 Node 工具链自动桥接
很多构建工具(如 Next.js 的 Turbopack worker pool)会直接调用 node 二进制来启动子进程。如果系统没有安装 Node.js,这些工具就会报错。
Deno 2.9 在没有检测到真实 Node.js 时,会在 PATH 中注入一个 shim(桩程序),它会:
- 拦截所有
node <args>调用 - 将参数转发给 Deno 自身
- 转换 Node.js CLI 参数为 Deno 可理解的格式
# 如果系统中没有 node:
$ which node
/path/to/deno-shim # 实际上指向 Deno
$ node --version
v20.0.0 # 报告兼容的版本号
$ node server.js # 自动转发给 Deno 执行
可以通过 DENO_DISABLE_NODE_SHIM=1 禁用此行为。
5.4 package.json 优先模式
对于一些团队来说,package.json 是团队约定的标准,不希望迁移到 deno.json。Deno 2.9 的 preferPackageJson 设置就是为这种情况设计的:
// deno.json
{
"preferPackageJson": true
}
开启后,deno add、deno install、deno remove 都会直接操作 package.json 而非 deno.json,与现有的 Node.js 工作流完全一致。
六、测试框架:从「能用」到「专业」
6.1 参数化测试:Deno.test.each
对于需要用多组数据验证同一逻辑的场景,Deno 2.9 引入了 Deno.test.each:
import { assertEquals } from "@std/assert";
interface FibonacciCase {
input: number;
expected: number;
}
const cases: FibonacciCase[] = [
{ input: 0, expected: 0 },
{ input: 1, expected: 1 },
{ input: 10, expected: 55 },
{ input: 20, expected: 6765 },
];
Deno.test.each(cases)(
"fibonacci($input) = $expected",
async ({ input, expected }) => {
const result = fibonacci(input);
assertEquals(result, expected);
}
);
function fibonacci(n: number): number {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return b;
}
6.2 内置快照断言
Deno 2.9 将快照测试作为内置功能提供,不再依赖第三方库:
import { assertSnapshot } from "@std/testing";
// 快照文件:__snapshots__/format_test.ts.snap
Deno.test("format output", async (t) => {
const formatter = new CodeFormatter();
await t.step("formats simple input", async () => {
const output = formatter.format("const x = 1;");
await assertSnapshot(t, output);
});
await t.step("formats complex input", async () => {
const output = formatter.format("function foo() { return 42; }");
await assertSnapshot(t, output);
});
});
$ deno test --update
# 更新快照文件
6.3 测试重试与重复执行
// 网络请求测试(不稳定网络场景)
Deno.test({
name: "upload file to cloud",
fn: async () => {
const result = await uploadFile("large-data.zip");
assertEquals(result.status, 200);
},
retry: 3, // 失败时最多重试 3 次
repeats: 10, // 重复执行 10 次(压力测试)
});
6.4 测试分片:--shard
在大规模 monorepo 中,测试分片可以让测试分布到多台机器上并行执行:
# 机器 1:运行第 1/4 部分
$ deno test --shard 1/4
# 机器 2:运行第 2/4 部分
$ deno test --shard 2/4
# 机器 3:运行第 3/4 部分
$ deno test --shard 3/4
# 机器 4:运行第 4/4 部分
$ deno test --shard 4/4
6.5 变更感知测试
Deno 2.9 的 --changed 和 --related 标志让 CI 更快:
# 仅运行 git diff HEAD~1 中变更的文件所涉及的测试
$ deno test --changed HEAD~1
# 运行与变更文件相关的测试(包括间接依赖)
$ deno test --related src/auth/permission.ts
七、Web Locks API 与其他运行时增强
7.1 Web Locks API:跨标签页同步
Deno 2.9 实现了 navigator.locks API(PR #31166),这是 W3C Web Locks API 的标准实现,允许 Web 应用在多个标签页或 Worker 之间协调对共享资源的访问:
// 在 Deno 和浏览器中均可使用
async function performExclusiveOperation(resourceId: string) {
// 请求排他锁
const lock = await navigator.locks.request(
`resource:${resourceId}`,
{ mode: "exclusive" },
async () => {
// 只有持有锁的代码才能进入这里
// 其他尝试获取同一把锁的请求会等待
await doExclusiveWork();
}
);
return lock;
}
async function readSharedData(resourceId: string) {
// 请求共享锁(可并发,多个读取可以同时进行)
const lock = await navigator.locks.request(
`resource:${resourceId}`,
{ mode: "shared" },
async () => {
return await readData();
}
);
return lock;
}
典型应用场景:
- 多标签页同时操作 localStorage 时的竞态条件解决
- Service Worker 与主线程共享资源时的协调
- IndexedDB 写入操作的序列化
7.2 CSS 模块导入
Deno 2.9 支持通过 import 属性直接导入 CSS 文件为构造样式表:
// main.ts
import sheet from "./styles.css" with { type: "css" };
document.adoptedStyleSheets = [sheet];
这使得可以在 Deno 中直接测试需要加载 CSS 的前端组件,而无需打包工具:
// component_test.ts
import { assertEquals } from "@std/assert";
import { renderButton } from "./button.ts";
import buttonStyles from "./button.css" with { type: "css" };
Deno.test("button renders with correct styles", async () => {
const shadow = document.createElement("div").attachShadow({ mode: "open" });
shadow.adoptedStyleSheets = [buttonStyles];
const btn = renderButton({ label: "Click me" });
shadow.appendChild(btn);
// CSS 模块加载正确,样式应用成功
const computed = getComputedStyle(btn);
assertEquals(computed.backgroundColor, "rgb(59, 130, 246)");
});
注:此功能在 Deno 2.9 中需要
--unstable-raw-imports标志,属于实验性阶段。
八、依赖管理的新工具
8.1 deno link:本地包链接
对于 monorepo 中的本地包开发,deno link 提供了与 npm link 类似的体验,但更简洁:
# 在 my-lib 目录下
$ deno link
Linked as: my-lib
# 在主项目目录下
$ deno link ../my-lib
Link ../my-lib (my-lib)
生成的 deno.json:
{
"imports": {},
"links": ["../my-lib"]
}
links 字段本身在 Deno 2.9 中已标记为稳定(stabilized)。
8.2 deno list:依赖一览无余
deno list 提供了项目依赖的清晰视图:
$ deno list
┌───────────────────────┬──────────┬──────────┐
│ Package │ Required │ Resolved │
├───────────────────────┼──────────┼──────────┤
│ jsr:@hono/hono │ ^4 │ 4.12.23 │
├───────────────────────┼──────────┼──────────┤
│ jsr:@std/assert │ ^1 │ 1.0.19 │
├───────────────────────┼──────────┼──────────┤
│ npm:express │ ^5 │ 5.2.1 │
└───────────────────────┴──────────┴──────────┘
$ deno list --prod # 仅生产依赖
$ deno list -r # 包含 workspace 成员
$ deno list "*eslint*" # 按名称过滤(支持通配符)
$ deno list --depth 2 # 显示两层依赖树
8.3 JSR 包进入 node_modules
jsrDepsInNodeModules 选项让 JSR 包可以安装到 node_modules 目录,从而被外部类型检查器和打包工具识别:
// deno.json
{
"jsrDepsInNodeModules": true
}
开启后,jsr:@david/dax 会以 npm:@jsr/david__dax 的形式安装到 node_modules,外部工具(如 ESLint、TypeScript 语言服务)无需特殊配置即可识别。
九、代码实战:用 Deno 2.9 构建一个完整应用
9.1 场景:带桌面前的 API 服务
我们用一个完整的实战案例串联 Deno 2.9 的核心能力:构建一个笔记服务,同时提供 HTTP API 和桌面客户端界面。
项目结构:
notekeeper/
├── deno.json
├── deno.lock
├── main.ts # HTTP API 服务
├── desktop.ts # 桌面入口
├── public/
│ └── index.html # 桌面客户端界面
├── db/
│ └── schema.ts # 数据库 Schema
└── __tests__/
└── api_test.ts # 测试
deno.json 配置:
{
"imports": {
"@std/assert": "jsr:@std/assert@^1",
"@std/testing": "jsr:@std/testing@^1",
"@db/sqlite": "jsr:@db/sqlite@^1"
},
"tasks": {
"start": "deno run -A --watch main.ts",
"desktop": "deno desktop desktop.ts",
"test": "deno test --allow-all"
},
"compilerOptions": {
"lib": ["deno.window", "deno.unstable"]
}
}
**数据库 Schema(使用 Deno KV):
// db/schema.ts
import { assertEquals } from "@std/assert";
// 使用 Deno KV(内置,无需额外依赖)
const kv = await Deno.openKv();
// 数据类型
interface Note {
id: string;
title: string;
content: string;
createdAt: number;
updatedAt: number;
tags: string[];
}
// CRUD 操作
export async function createNote(
title: string,
content: string,
tags: string[] = []
): Promise<Note> {
const id = crypto.randomUUID();
const now = Date.now();
const note: Note = { id, title, content, createdAt: now, updatedAt: now, tags };
const result = await kv.atomic()
.set(["notes", id], note)
.set(["notes_by_time", now, id], id)
.commit();
if (!result.ok) {
throw new Error("Failed to create note");
}
return note;
}
export async function getNote(id: string): Promise<Note | null> {
const result = await kv.get<Note>(["notes", id]);
return result.value;
}
export async function listNotes(limit = 100): Promise<Note[]> {
const notes: Note[] = [];
const iter = kv.list<Note>({ prefix: ["notes"] });
for await (const entry of iter) {
notes.push(entry.value);
}
return notes
.sort((a, b) => b.updatedAt - a.updatedAt)
.slice(0, limit);
}
export async function deleteNote(id: string): Promise<void> {
const note = await getNote(id);
if (!note) return;
await kv.atomic()
.delete(["notes", id])
.delete(["notes_by_time", note.createdAt, id])
.commit();
}
**HTTP API 服务(main.ts):
// main.ts
import { assertEquals } from "@std/assert";
import { createNote, getNote, listNotes, deleteNote, Note } from "./db/schema.ts";
const PORT = 8000;
function corsHeaders(): HeadersInit {
return {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
}
async function handleRequest(req: Request): Promise<Response> {
const url = new URL(req.url);
// CORS 预检
if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders() });
}
try {
// GET /notes - 列出所有笔记
if (req.method === "GET" && url.pathname === "/api/notes") {
const notes = await listNotes();
return Response.json({ notes }, { headers: corsHeaders() });
}
// GET /api/notes/:id - 获取单个笔记
if (req.method === "GET" && url.pathname.startsWith("/api/notes/")) {
const id = url.pathname.split("/").pop()!;
const note = await getNote(id);
if (!note) {
return Response.json({ error: "Note not found" }, {
status: 404,
headers: corsHeaders()
});
}
return Response.json({ note }, { headers: corsHeaders() });
}
// POST /api/notes - 创建笔记
if (req.method === "POST" && url.pathname === "/api/notes") {
const body = await req.json();
if (!body.title || !body.content) {
return Response.json({ error: "title and content required" }, {
status: 400,
headers: corsHeaders()
});
}
const note = await createNote(body.title, body.content, body.tags || []);
return Response.json({ note }, { status: 201, headers: corsHeaders() });
}
// DELETE /api/notes/:id - 删除笔记
if (req.method === "DELETE" && url.pathname.startsWith("/api/notes/")) {
const id = url.pathname.split("/").pop()!;
await deleteNote(id);
return new Response(null, { status: 204, headers: corsHeaders() });
}
// 健康检查
if (req.method === "GET" && url.pathname === "/health") {
return Response.json({ status: "ok", version: "2.9" });
}
// 静态文件(开发时可选)
if (req.method === "GET") {
const filePath = url.pathname === "/" ? "/index.html" : url.pathname;
try {
const content = await Deno.readFile(`public${filePath}`);
const contentType = getContentType(filePath);
return new Response(content, {
headers: { "Content-Type": contentType }
});
} catch {
return Response.json({ error: "Not found" }, { status: 404 });
}
}
return Response.json({ error: "Not found" }, { status: 404 });
} catch (err) {
console.error(err);
return Response.json({ error: "Internal server error" }, {
status: 500,
headers: corsHeaders()
});
}
}
function getContentType(path: string): string {
const ext = path.split(".").pop()!;
const types: Record<string, string> = {
html: "text/html",
css: "text/css",
js: "application/javascript",
json: "application/json",
png: "image/png",
svg: "image/svg+xml",
};
return types[ext] || "text/plain";
}
console.log(`Deno NoteKeeper API running on http://localhost:${PORT}`);
Deno.serve({ port: PORT }, handleRequest);
**桌面入口(desktop.ts):
// desktop.ts
import { serve } from "./main.ts";
// deno desktop 会自动将 Deno.serve 绑定到 webview 的端口
// 无需修改 main.ts 的任何代码
serve();
运行方式:
# 启动 API 服务
$ deno task start
# 或者直接构建桌面应用
$ deno task desktop
# → 生成跨平台安装包
9.2 测试用例
// __tests__/api_test.ts
import { assertEquals, assertExists } from "@std/assert";
import { assertSnapshot } from "@std/testing/snapshot";
// 创建笔记
Deno.test("createNote - generates unique id", async () => {
const note = await createNote("Test", "Hello world", ["test"]);
assertExists(note.id);
assertEquals(note.title, "Test");
assertEquals(note.content, "Hello world");
assertEquals(note.tags, ["test"]);
});
// API 集成测试
Deno.test("API - create and retrieve note", async (t) => {
const baseUrl = "http://localhost:8000";
const createRes = await fetch(`${baseUrl}/api/notes`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "API Test", content: "Content" }),
});
const { note } = await createRes.json();
assertEquals(createRes.status, 201);
const getRes = await fetch(`${baseUrl}/api/notes/${note.id}`);
const { note: retrieved } = await getRes.json();
await assertSnapshot(t, JSON.stringify(retrieved, null, 2));
});
// 参数化测试
Deno.test.each([
{ id: "nonexistent", expected: null },
])("getNote returns $expected for id=$id", async ({ id, expected }) => {
const result = await getNote(id);
assertEquals(result, expected);
});
十、总结与展望
10.1 Deno 2.9 的核心价值
Deno 2.9 不是一个增量更新,而是一个平台级别的版本。它证明了 Deno 团队对「Deno 应该是什么」的答案已经进化:
| 维度 | Deno 1.x | Deno 2.x (2.8 之前) | Deno 2.9 |
|---|---|---|---|
| 后端运行时 | ✅ 成熟 | ✅ 成熟 | ✅ 性能标杆 |
| 桌面应用 | ❌ 不支持 | ❌ 不支持 | ✅ 原生支持 |
| Node.js 生态 | ⚠️ 部分兼容 | ⚠️ 42% 兼容 | ✅ 76.4% 兼容 |
| 包管理器迁移 | ❌ 不支持 | ⚠️ 基础支持 | ✅ 全锁文件覆盖 |
| 供应链安全 | ❌ 无 | ⚠️ 可选配置 | ✅ 默认启用 |
| 开发工具链 | ⚠️ 基础 | ✅ 完善 | ✅ 测试框架专业级 |
10.2 值得关注的未来方向
1. deno desktop 的成熟度
目前 deno desktop 标记为 experimental(实验性),部分平台特性仍在完善中。根据 Deno 团队的发布节奏,预计在 v2.10-v2.11 中会稳定化。
2. WASM 生态集成
Deno 2.9 的发布说明中提到 deno publish 已支持在 WASM 模块中展开 import specifiers。这暗示 Deno 正在加大对 WebAssembly 的投入,未来 Deno 可能会成为运行 WASM 组件模型的首选平台。
3. Node-API 10 的支持
Node-API v10 引入了对 JavaScript Engine 的更深度集成,Deno 对其的支持意味着更多原生 Node.js addon 可以直接在 Deno 中运行,进一步扩大了兼容范围。
4. ML-KEM 后量子密码学
Deno 2.8.2 就已实现 ML-KEM(FIPS 203)后量子 KEM 算法。随着量子计算威胁的临近,这一能力将使 Deno 成为处理「需要长期安全」的敏感数据的首选运行时。
10.3 迁移建议
立即可用(零摩擦迁移):
- 新项目:直接使用 Deno 2.9,从第一天就享受其性能和安全优势
- Node.js 生态项目:运行
deno install,锁文件直读,版本完全不变 - pnpm monorepo:运行一次迁移命令,全自动完成
值得观望(需要评估期):
- 生产级桌面应用:deno desktop 目前仍为 experimental,可先在内部工具上试点
- 深度 Node.js 原生 addon 依赖:确认 addon 的 Node-API v10 兼容性
推荐迁移信号:
- 团队正在评估 Bun 作为 Node.js 替代 → 优先评估 Deno 2.9
- 需要构建桌面 CLI 混合工具 → deno desktop 值得尝试
- 安全合规要求高(供应链攻击防御)→ Deno 的默认安全策略是最简单方案
Deno 2.9 用 2026 年最硬核的技术数据证明了一件事:一个现代 JavaScript/TypeScript 运行时,不应该在功能、性能和安全性之间做任何取舍。如果你还在用 Node.js,是时候给自己一个升级的理由了。