编程 万字深度解析 Deno 2.9:当 TypeScript 运行时成为全能开发平台——从桌面应用到供应链安全的完整技术指南(2026)

2026-07-01 18:16:46 +0800 CST views 6

万字深度解析 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 历史上最重要的版本之一,因为它解决了最关键的生态问题:

  1. Node.js 兼容性从 42% 跃升至 76.4%(高于 Bun)
  2. deno desktop 正式登场:一个命令就能把 Web 项目打包成跨平台桌面应用,无需 Electron 或 Tauri
  3. 锁文件直读:package-lock.json、bun.lock、pnpm-lock.yaml、yarn.lock 全部无缝导入
  4. 供应链安全成为默认特性:最低发布年龄、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 会:

  1. 启动一个 Deno.serve 服务器,绑定到 webview 自动分配的端口
  2. 打开一个原生窗口,加载该页面
  3. 服务器和窗口自动配对,无需手动指定端口

这就是一个完整的桌面应用——没有 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.8Deno 2.9提升幅度
冷启动时间34.2 ms17.3 ms1.98x 更快
Deno.serve(真实请求)56.8k req/s72.4k req/s1.27x 更快
Deno.serve(纯文本)77.0k req/s85.6k req/s1.11x 更快
Deno.serve(1 MiB 响应体)1,617 req/s1,907 req/s1.18x 更快
RSS 内存(真实请求)142 MB64 MB2.2x 更省
RSS 内存(1 MiB 响应体)197 MB63 MB3.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 中,这一问题通过以下方式得到解决:

  1. Deno 自有的 HTTP/1.1 服务路径:放弃了部分依赖 Rust 网络库的路径,改为 Deno 直接管理连接和缓冲区
  2. V8 堆配置优化:针对 HTTP 服务器场景重新调优了 V8 的堆分配策略
  3. 响应体流式处理: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.subtleconsole(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

信任等级排序(从高到低):

  1. 分阶段发布(staged rollouts):维护者通过实时 2FA 挑战审核后发布,信号最强
  2. 可信发布(trusted publishing):由 CI/CD 流水线通过 OIDC 证明的发布
  3. 来源证明(provenance attestation):npm 注册表记录的包来源信息
  4. 普通发布(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 会:

  1. 读取 package-lock.json(或 pnpm-lock.yamlyarn.lockbun.lock
  2. 提取每个依赖的精确版本号 + integrity hash
  3. 在 deno.lock 中写入完全相同的版本和 hash
  4. 从 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.jsonworkspacescatalogs 字段,同时保留原有配置文件的注释和格式不变

5.3 Node 工具链自动桥接

很多构建工具(如 Next.js 的 Turbopack worker pool)会直接调用 node 二进制来启动子进程。如果系统没有安装 Node.js,这些工具就会报错。

Deno 2.9 在没有检测到真实 Node.js 时,会在 PATH 中注入一个 shim(桩程序),它会:

  1. 拦截所有 node <args> 调用
  2. 将参数转发给 Deno 自身
  3. 转换 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 adddeno installdeno 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 标志,属于实验性阶段。


八、依赖管理的新工具

对于 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.xDeno 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,是时候给自己一个升级的理由了。

推荐文章

pin.gl是基于WebRTC的屏幕共享工具
2024-11-19 06:38:05 +0800 CST
LangChain快速上手
2025-03-09 22:30:10 +0800 CST
H5抖音商城小黄车购物系统
2024-11-19 08:04:29 +0800 CST
CSS Grid 和 Flexbox 的主要区别
2024-11-18 23:09:50 +0800 CST
mendeley2 一个Python管理文献的库
2024-11-19 02:56:20 +0800 CST
JavaScript设计模式:单例模式
2024-11-18 10:57:41 +0800 CST
程序员茄子在线接单