编程 Deno 2.9 深度拆解:当 JavaScript 运行时长出「桌面原生」能力——从 deno desktop 到性能全面飞跃的工程全貌(2026)

2026-07-18 06:14:41 +0800 CST views 4

Deno 2.9 深度拆解:当 JavaScript 运行时长出「桌面原生」能力——从 deno desktop 到性能全面飞跃的工程全貌(2026)

2026年6月25日,Deno Land 正式发布了 Deno 2.9。这是 Deno 历史上最具里程碑意义的版本之一——不是因为某个单一功能的堆砌,而是因为它真正完成了一次定位的跃迁:Deno 不再只是一个服务器端 JavaScript/TypeScript 运行时,而是一个可以贯穿从服务器到桌面的全栈运行时平台

本次更新最核心的功能是 deno desktop:一套将 Web 技术栈(Deno + WebView)直接编译为原生桌面应用的完整工具链。没有 Electron 的复杂性,没有 Tauri 的 Rust 绑定门槛,用你早已熟悉的 API 和工作流,就能构建、打包、发布一个真正的原生桌面应用。

与此同时,性能层面也交出了一份令人惊喜的成绩单:冷启动时间减半、HTTP 吞吐量提升 1.27 倍、峰值内存占用降低 2.2 倍——三项关键指标同时刷新。而 CSS 模块导入、deno install 读取竞品锁文件、Node.js 26 兼容等众多细节改进,则让这个版本在「好用」这件事上又往前迈了一大步。

本文将对 Deno 2.9 进行全面深度的拆解,从 deno desktop 的架构设计、原生 API、WebView vs CEF 抉择、性能基准测试数据、到具体的代码实战和 Electron/Tauri 对比,带你真正理解这个版本背后的工程全貌。


一、背景:Deno 的定位演进与 2.9 的战略意义

要理解 Deno 2.9 的意义,需要先回顾 Deno 这几年的演进路径。

Deno 于 2018 年由 Ryan Dahl(Node.js 原创始人)在 JSConf EU 上宣布,定位是「重新思考 Node.js 的设计」。最初它瞄准的是服务器端 JavaScript 的安全性问题:默认不允许文件、网络、环境变量访问,必须显式授权。但坦率地说,早期 Deno 在服务器端的市场渗透率并不高——Node.js 的生态壁垒太厚,npm 生态的惯性太大。

Deno 真正找到自己的差异化道路,是从 1.x 到 2.x 的演进过程中逐步完成的:

  • Deno 2.0(2024年):完整兼容 Node.js 和 npm生态,吸引了大量现有 Node.js 开发者平滑迁移
  • Deno 2.5(2025年):内置 deno compile,支持将 TypeScript 代码编译为单一可执行文件,打通了服务器部署的「最后一公里」
  • Deno 2.9(2026年):推出 deno desktop,补全了从服务器到桌面的全栈能力

这条演进路径清晰地指向一个战略目标:Deno 希望成为那个「一处代码,走遍全栈」的运行时。从前端框架(Vite SSR、Next.js、Nuxt、SvelteKit)到后端服务(Deno.serve)再到桌面应用(Deno desktop),你只需要掌握一套 API 和一个工具链。


二、deno desktop 核心架构:从服务器端运行时到桌面应用平台

2.1 设计哲学:零迁移成本的全栈统一

Deno desktop 的设计哲学非常明确:让已有的 Web 开发经验零成本迁移到桌面应用开发

传统的桌面开发存在一个根本性的割裂:前端工程师擅长 JavaScript/TypeScript、HTML、CSS,习惯于 Vite/HMR/Fast Refresh 这套开发体验,但构建桌面应用需要学习 Electron 的 IPC 机制、Tauri 的 Rust 绑定、NW.js 的特殊 API——这些与 Web 开发体验相去甚远。

Deno desktop 的做法是:你现有的 Deno 服务器端代码,在桌面环境下同样工作。最直观的例子:

// 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. 在一个内置 WebView(Windows 上是 WebView2,macOS/Linux 上是 WebKit)中打开一个窗口
  2. 在 Deno 运行时中启动 Deno.serve(),自动绑定到 WebView 打开的端口
  3. WebView 加载并渲染 Deno.serve 提供的 HTML 内容

整个过程不需要任何额外的配置、IPC 管道或构建步骤。

2.2 框架自动检测:把整个前端框架带入桌面

deno desktop 的另一个杀手锏是框架自动检测,与 deno compile 共享同一套检测逻辑。如果你在一个已有 Next.js、Astro、Fresh、Remix、Nuxt、SvelteKit、SolidStart、TanStack Start 或 Vite SSR 项目的目录中运行 deno desktop,它会自动:

  1. 检测到项目使用的框架
  2. 调用框架对应的构建命令(next buildastro build 等)
  3. 将构建产物打包进桌面应用
  4. 启动 Deno 服务器并打开 WebView 加载页面
# 在 Next.js 项目目录中
$ deno desktop .        # 自动检测并打包 Next.js 应用
$ deno desktop --hmr     # 开发模式,开启 HMR 热更新

这意味着:如果你正在用 Next.js 或 Nuxt 开发 Web 应用,只需要一条命令就能把它变成一个原生桌面应用,安装到用户电脑上,图标、窗口管理、系统托盘——全部原生。

2.3 原生桌面 API:没有 Electron 的复杂度,也有 Electron 的能力

deno desktop 提供了一套精心设计的原生桌面 API,全部挂载在 Deno.* 命名空间下,不需要安装任何第三方包:

窗口控制——Deno.BrowserWindow

// 创建窗口并控制其行为
const win = new Deno.BrowserWindow({
  width: 1200,
  height: 800,
  title: "我的 Deno 桌面应用",
  resizable: true,
  center: true,
});

// 控制窗口显示/隐藏
await win.show();
await win.hide();
await win.minimize();
await win.maximize();
await win.close();

// 监听窗口事件
win.on("close", () => {
  console.log("用户点击了关闭按钮");
});

// 打开开发者工具
await win.openDevTools();

WebView 与 Deno 的双向绑定——window.bind()

这是 deno desktop 最具创新的设计之一。Electron 的 IPC 机制要求开发者在主进程和渲染进程之间手动定义通道、序列化消息、注册处理器。deno desktop 的做法更接近现代 Web 的思维方式:在 Deno 端绑定一个函数,在 WebView 的 JavaScript 中直接调用

// deno 端(main.ts)
Deno.serve(() => {
  const html = `<!DOCTYPE html>
<html>
<head><title>Deno Desktop Demo</title></head>
<body>
  <h1>Deno Desktop 演示</h1>
  <button id="btn">调用 Deno 函数</button>
  <pre id="output"></pre>
  <script type="module">
    const output = document.getElementById("output");
    document.getElementById("btn").onclick = async () => {
      // 直接调用 Deno 端绑定的函数
      const result = await bindings.doHeavyComputation(42);
      output.textContent = "计算结果: " + result;
    };
  </script>
</body>
</html>`;
  
  return new Response(html, {
    headers: { "content-type": "text/html" }
  });
});

// 绑定计算函数 —— 这个函数可以在 WebView 的 JS 中调用
window.bind("doHeavyComputation", async (input: number) => {
  // 在 Deno 运行时中执行,可以访问文件系统、网络等
  const data = await Deno.readTextFile("./data.json");
  const parsed = JSON.parse(data);
  return parsed.items.filter(item => item.value > input);
});
// WebView 端(嵌入在 HTML 中)
// bindings.doHeavyComputation 是从 Deno 端绑定过来的
const result = await bindings.doHeavyComputation(42);

系统托盘——Deno.Tray

// 创建系统托盘图标
const tray = new Deno.Tray();

// 设置图标(可以是本地文件或网络 URL)
const iconBytes = await Deno.readFile("./app-icon.png");
await tray.setIcon(iconBytes);

// 附加一个悬浮面板
const panel = tray.attachPanel({ url: "https://localhost:8000/panel" });

// 从面板中调用 Deno 函数
panel.window.bind("doThing", async () => {
  // 打开一个新窗口处理任务
  new Deno.BrowserWindow({ width: 800, height: 600 });
});

macOS Dock 集成——Deno.Dock

// 在 macOS Dock 中设置应用徽章
Deno.Dock.setBadge("3"); // 显示红点数字
Deno.Dock.setMenu([
  { label: "打开主窗口", click: () => mainWin.show() },
  { label: "偏好设置", click: () => openPreferences() },
  { type: "separator" },
  { label: "退出", click: () => Deno.exit(0) }
]);

原生对话框

// alert / confirm / prompt 自动渲染为原生系统对话框
const confirmed = confirm("确定要删除这个文件吗?");
const name = prompt("请输入你的名字:", "匿名");

自动更新——Deno.autoUpdate

// 轮询式自动更新,在后台下载并应用二进制补丁
Deno.autoUpdate({
  checkInterval: 60 * 60 * 1000, // 每小时检查一次
  onUpdateAvailable: (info) => {
    console.log(`发现新版本 ${info.version},正在后台下载...`);
  },
  onUpdateDownloaded: () => {
    // 下次应用启动时自动生效,或者立即重启
    Deno.exit(0); // 退出后下次启动即使用新版本
  }
});

2.4 WebView vs CEF:两种渲染后端的技术抉择

deno desktop 提供了两种渲染引擎供选择,通过 --backend 参数指定:

--backend webview(默认)

平台渲染引擎
WindowsWebView2(Edge Chromium)
macOSWebKit
LinuxWebKitGTK
$ deno desktop main.ts              # 默认使用原生 WebView
$ deno desktop --backend webview main.ts  # 显式指定

优势:

  • 二进制体积极小(不需要打包任何浏览器引擎)
  • 启动速度快(直接使用系统已有的 WebView,无额外进程开销)
  • 系统资源占用低

劣势:

  • 渲染效果依赖宿主系统安装的 WebView 版本
  • 部分现代 Web API 可能不可用(取决于系统 WebView 版本)
  • Windows 7 等老系统上没有 WebView2

--backend cef(Chromium Embedded Framework)

$ deno desktop --backend cef main.ts  # 打包 Chromium

优势:

  • 跨平台渲染完全一致(每个用户都运行相同版本的 Chromium)
  • 支持所有现代 Web API(Service Worker、WebGPU、WebRTC 等)
  • 在所有目标平台上行为一致

劣势:

  • 构建时需要下载 Chromium(约 100-200MB),CI 构建时间显著增长
  • 最终二进制体积大幅增加
  • 首次运行时有 Chromium 初始化开销

选择建议:绝大多数应用选择默认的 WebView 后端即可,这是 Deno 团队的首选推荐。只有在以下场景才考虑 CEF:

  • 应用依赖最新的 Web API(如 WebGPU),且需要保证跨平台一致性
  • 你的用户群体中有大量 Windows 7 用户(没有 WebView2)
  • 你需要 Service Worker 或 WebRTC 等 WebView 可能不支持的功能

三、性能深度分析:数据背后的工程决策

Deno 2.9 在性能上的提升是全方位的——不是某一项优化,而是从冷启动到内存占用再到 HTTP 吞吐的三线并行突破。

3.1 冷启动:从 34ms 到 17ms

Deno 2.8 的 hello-world 程序冷启动时间是 34ms,2.9 缩短到 17ms,提升了 1.98 倍。这一提升来自四项具体优化:

① 延迟加载 node: 全局变量

在 Deno 2.8 中,node: 模块(如 node:fsnode:path)对应的全局变量在 V8 快照加载时就被初始化了,即便应用根本不用这些模块。这是一个纯空间的浪费:快照体积增大,加载时间增加。

2.9 将这些全局变量的初始化推迟到真正被访问的时刻。如果你的应用不导入 node: 模块,这部分开销完全为 0。

② Node 引导程序限制到 Node Worker

Node.js 的引导程序(bootstrap)用于兼容 Node.js 生态中的全局变量(如 processBuffer__dirname)。在 2.8 中,这套引导程序在主线程启动时就执行了。但大多数 Deno 应用根本不需要 Node.js 兼容模式,这笔开销同样是浪费。

2.9 将 Node 引导程序推迟到仅在创建 Node Worker(new NodeWorker)时才加载,主线程不再承担这笔开销。

③ V8 代码缓存

V8 有一个 JIT 代码缓存机制:将已编译的 JavaScript/TypeScript 代码编译产物缓存到磁盘,下次加载时直接使用缓存而无需重新编译。对于反复启动的服务器进程,这个优化效果显著。

2.9 为延迟加载的 ESM 模块启用了这一缓存机制。

④ 运行时快照压缩精简

Deno 的 V8 快照是运行时启动的关键组件。2.9 对快照进行了压缩精简,移除了不必要的元数据,进一步减小了加载体积。

3.2 内存占用:从随负载线性增长到基本恒定

这是 2.9 性能层面最令人惊喜的改进:

场景Deno 2.8 RSSDeno 2.9 RSS降幅
纯文本响应94MB~62MB基准更低
真实 JSON 工作负载142MB64MB2.2x
1MiB 大文件流197MB63MB3.1x

关键观察:2.8 中 RSS(Resident Set Size,常驻内存集)随工作负载增长——流式传输大文件时内存占用几乎翻倍。但在 2.9 中,无论什么工作负载,RSS 基本稳定在约 62-64MB。

这个改进的工程意义非常实际:同一台服务器在内存耗尽前,可以运行更多并发的 Deno.serve 实例。对于高密度部署场景(VPS、Serverless 函数等),这是直接影响成本的关键指标。

3.3 HTTP 吞吐量:新的 Deno 自有 HTTP/1.1 路径

Deno.serve 的吞吐量在三项测试中均有提升:

工作负载Deno 2.8Deno 2.9提升
真实 JSON(POST + Bearer 认证 + JSON 响应)56.8k req/s72.4k req/s1.27x
纯文本 Hello World77.0k req/s85.6k req/s1.11x
1MiB 大文件流1,617 req/s1,907 req/s1.18x

提升来源于新引入的 Deno 自有 HTTP/1.1 服务路径(之前依赖的是 V8 内置的 HTTP 实现或第三方库)。用 Rust 重写 HTTP 处理逻辑后,Deno 团队能够直接控制连接管理、缓冲区分配和请求解析的每一个细节,而不需要受制于其他组件的实现约束。

3.4 Rust 重写热路径

除了上述主要改进,2.9 还将一些关键热路径从 JavaScript 迁移到了 Rust:

// 以下 API 的实现从 JS 迁移到了 Rust,性能显著提升
// crypto.subtle —— 全部 Web Crypto API
const key = await crypto.subtle.generateKey(
  "AES-GCM",
  { length: 256 },
  true,
  ["encrypt", "decrypt"]
);

// Deno.inspect —— 控制台输出的底层实现
console.log(Deno.inspect(someComplexObject, { colors: true, depth: 4 }));

Rust 重写的好处在于:避免了 JS ⇄ Rust 的跨边界(syscall)开销,且 Rust 代码的内存分配行为更可预测,对这类高频调用的性能提升尤为明显。


四、CSS 模块导入:前端开发体验的补全

Deno 2.9 支持通过 import attributes 语法导入 CSS 文件,并将其作为构造样式表(Constructable Stylesheet)使用:

// main.ts
import sheet from "./styles.css" with { type: "css" };

// 将 CSS 模块应用到当前文档
document.adoptedStyleSheets = [sheet];

// 样式完全隔离,不会污染全局

这对应的是 CSS 模块脚本(CSS Module Scripts)Web 标准,支持在 JavaScript 中直接 import CSS 文件并获得一个 CSSStyleSheet 对象,然后通过 adoptedStyleSheets(Shadow DOM)或 document.adoptedStyleSheets(主文档)应用样式。

这一能力与 Vite、webpack 的 CSS Modules 功能完全对应,意味着 Deno 在前端开发体验上又补全了一块短板。对于 deno desktop 场景下的 UI 开发,这个功能直接解决了「如何用模块化方式管理桌面应用的样式」的问题。


五、Node.js 生态迁移:从「可以运行」到「无缝迁移」

5.1 deno install 读取竞品锁文件

Deno 2.9 最大的易用性改进之一:deno install 现在可以直接读取 npm、pnpm、yarn 和 Bun 的锁文件。

这意味着:如果你想把现有项目的包管理器从 npm/pnpm/yarn/Bun 切换到 Deno,只需要:

# 1. 项目已有 package.json 和 package-lock.json / pnpm-lock.yaml / yarn.lock / bun.lockb
$ cd my-existing-node-project

# 2. 运行 deno install,Deno 会自动检测并读取已有的锁文件
$ deno install

# 3. Deno 根据锁文件的内容安装依赖,而不是从头解析 package.json

这个设计非常聪明:不是让你「迁移」,而是让你「直接使用 Deno 替代」——已有的锁文件就是 Deno 需要的所有信息,不需要修改任何一行代码,也不需要经历依赖解析的不确定性。

5.2 Node.js 26 兼容性目标

Deno 2.9 将 Node.js 兼容性目标从 Node.js 24 升级到 Node.js 26,node-compat 测试套件同步更新至 26.3.0 版本。这确保了大量依赖 Node.js 最新 API 的 npm 包能够在 Deno 中正常运行。


六、完整的桌面应用发布工作流

6.1 构建与打包

deno desktop 基于 deno compile 的底层机制,所有打包能力完全继承:

# 在 macOS 上构建 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

# 输出格式自动根据扩展名推断:
# .app/.dmg  → macOS 应用包
# .exe       → Windows 可执行文件
# .msi       → Windows MSI 安装包
# .AppImage  → Linux AppImage
# .deb/.rpm  → Debian/RPM 包

6.2 交叉编译:Linux CI 一条命令出全平台包

deno desktop 的交叉编译能力是其区别于其他桌面方案的重要优势。传统上,打包跨平台桌面应用需要准备多台虚拟机或使用 GitHub Actions 的 matrix 策略,配置极为繁琐。

deno desktop 的打包逻辑完全用 Rust 实现,不依赖平台特定的工具链(fpm、electron-builder 等),所以在任意平台上都能输出任意目标平台的二进制:

# .github/workflows/release.yml
name: Release
on:
  push:
    tags:
      - "v*"

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: denoland/setup-deno@v1
        with:
          deno-version: 2.9.0
      - run: deno task build
      
      # 在 Ubuntu Linux 上构建全平台桌面应用
      - name: Build desktop apps
        run: |
          deno desktop --all-targets --output dist/MyApp main.ts
          
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

6.3 压缩发布

--compress 参数可以将运行时和 UI 后端打包为自解压压缩包,首次启动时自动解压,比传统的独立安装程序更轻量:

$ deno desktop --compress --output MyApp.AppImage main.ts

七、与 Electron/Tauri 的深度对比

作为一个严肃的桌面应用方案,deno desktop 无法回避与 Electron 和 Tauri 的对比。以下是三者的全方位对比:

维度Deno desktopElectronTauri
渲染引擎WebView(系统内置)或 CEFChromium(捆绑)WebView(系统内置)或 CEF
主语言TypeScript/JavaScriptJavaScript/TypeScriptRust
API 风格原生 Deno API,Deno.* 命名空间Electron IPC,app/BrowserWindowRust 命令,app:: 模块
二进制体积~15MB(WebView)+ 你的代码~150MB(Chromium)~3-5MB(Rust)+ WebView
冷启动速度极快(WebView 直接使用系统进程)慢(Chromium 进程启动)极快(Rust 编译的二进制)
Node.js 兼容优秀(Node.js 26 目标)100%无(需额外集成)
npm 生态完整支持(通过 npm: 规范)100%需要 wasm 或插件
全栈一致性服务器+桌面同一运行时/同一套API服务器用 Express/NestJS,桌面用 IPC服务器需用其他方案
生产成熟度2.9 experimental高度成熟成熟
企业采用早期大量(VS Code、Slack 等)较多(Figma、1Password 等)

场景分析:何时选 deno desktop?

选 Deno desktop

  • 你已经用 Deno 做后端服务,想把同一套代码或同一种技术栈扩展到桌面
  • 团队是前端/全栈背景,不愿意学 Rust 或 Electron IPC 的复杂性
  • 桌面应用主要展示 Web 内容,不需要深度系统集成
  • 需要同时服务服务器端和桌面端,希望用同一套依赖管理

选 Electron

  • 需要最高程度的渲染一致性(所有用户运行相同 Chromium 版本)
  • 需要深度 Electron 生态集成(如 electron-builder、electron-updater 等成熟工具链)
  • 应用需要 Node.js 环境访问和 npm 生态完全兼容

选 Tauri

  • 需要极小的二进制体积和接近原生的性能
  • 需要深度系统集成(Rust 可以直接调用任何系统 API)
  • 团队有 Rust 经验

八、生产实战:从零构建一个笔记桌面应用

下面我们用 deno desktop 构建一个完整的笔记应用示例,展示从项目初始化到打包发布的完整流程。

8.1 项目结构

deno-notes/
├── main.ts              # Deno 桌面应用入口
├── src/
│   ├── server.ts         # Deno.serve 服务器逻辑
│   ├── store.ts          # 数据存储层(Deno KV)
│   ├── desktop-api.ts    # 桌面原生 API 封装
│   └── renderer/
│       ├── index.html    # HTML 模板
│       └── app.ts        # 前端逻辑
├── styles/
│   └── main.css
└── data/
    └── notes.db          # 数据文件(SQLite)

8.2 数据存储层

// src/store.ts —— 使用 Deno KV 作为数据存储
import { KV } from "@db/kit";

const kv = await Deno.openKv();

export interface Note {
  id: string;
  title: string;
  content: string;
  createdAt: number;
  updatedAt: number;
  pinned: boolean;
}

export const notesStore = {
  // 创建笔记
  async create(title: string, content: string): Promise<Note> {
    const note: Note = {
      id: crypto.randomUUID(),
      title,
      content,
      createdAt: Date.now(),
      updatedAt: Date.now(),
      pinned: false,
    };
    await kv.set(["notes", note.id], note);
    return note;
  },

  // 获取所有笔记
  async list(): Promise<Note[]> {
    const notes: Note[] = [];
    for await (const [, note] of kv.list<Note>({ prefix: ["notes"] })) {
      notes.push(note);
    }
    return notes.sort((a, b) => b.updatedAt - a.updatedAt);
  },

  // 更新笔记
  async update(id: string, patch: Partial<Pick<Note, "title" | "content" | "pinned">>): Promise<void> {
    const note = await kv.get<Note>(["notes", id]);
    if (!note.value) throw new Error(`Note ${id} not found`);
    await kv.set(["notes", id], {
      ...note.value,
      ...patch,
      updatedAt: Date.now(),
    });
  },

  // 删除笔记
  async delete(id: string): Promise<void> {
    await kv.delete(["notes", id]);
  },
};

8.3 桌面 API 封装

// src/desktop-api.ts —— 封装 deno desktop API,提供一致的接口
export async function minimizeWindow(win: Deno.BrowserWindow): Promise<void> {
  await win.minimize();
}

export async function maximizeWindow(win: Deno.BrowserWindow): Promise<void> {
  const isMaximized = await win.isMaximized();
  if (isMaximized) {
    await win.unmaximize();
  } else {
    await win.maximize();
  }
}

export function bindNoteOperations() {
  // 绑定笔记操作函数到 WebView
  window.bind("saveNote", async (id: string, title: string, content: string) => {
    if (id) {
      await notesStore.update(id, { title, content });
    } else {
      await notesStore.create(title, content);
    }
    return { ok: true };
  });

  window.bind("deleteNote", async (id: string) => {
    await notesStore.delete(id);
    return { ok: true };
  });

  window.bind("togglePin", async (id: string) => {
    const note = await kv.get(["notes", id]);
    if (note.value) {
      await notesStore.update(id, { pinned: !note.value.pinned });
    }
    return { ok: true };
  });
}

export function setupAutoUpdater() {
  if (typeof Deno !== "undefined" && Deno.autoUpdate) {
    Deno.autoUpdate({
      checkInterval: 60 * 60 * 1000,
      onUpdateAvailable: (info) => {
        console.log(`新版本 ${info.version} 可用`);
      },
      onUpdateDownloaded: () => {
        // 在下次启动时自动应用更新
        console.log("更新已下载,将在重启后生效");
      }
    });
  }
}

8.4 服务器入口

// main.ts —— deno desktop 入口
import { notesStore, type Note } from "./src/store.ts";
import { bindNoteOperations, setupAutoUpdater } from "./src/desktop-api.ts";
import sheet from "./styles/main.css" with { type: "css" };

// 在 WebView 中应用 CSS
if (typeof document !== "undefined") {
  document.adoptedStyleSheets = [sheet];
}

// 绑定桌面操作函数
bindNoteOperations();
setupAutoUpdater();

// Deno.serve —— 同时服务桌面 UI 和 REST API
Deno.serve(async (req: Request) => {
  const url = new URL(req.url);

  // CORS 预检
  if (req.method === "OPTIONS") {
    return new Response(null, {
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
        "Access-Control-Allow-Headers": "Content-Type",
      }
    });
  }

  // API 路由
  if (url.pathname === "/api/notes") {
    if (req.method === "GET") {
      const notes = await notesStore.list();
      return Response.json(notes);
    }
    if (req.method === "POST") {
      const body = await req.json();
      const note = await notesStore.create(body.title, body.content);
      return Response.json(note, { status: 201 });
    }
  }

  if (url.pathname.startsWith("/api/notes/")) {
    const id = url.pathname.split("/").pop()!;
    if (req.method === "PUT") {
      const body = await req.json();
      await notesStore.update(id, body);
      return Response.json({ ok: true });
    }
    if (req.method === "DELETE") {
      await notesStore.delete(id);
      return Response.json({ ok: true });
    }
  }

  // 静态文件服务(生产模式)
  if (url.pathname === "/" || !url.pathname.includes(".")) {
    const html = await Deno.readFile("./src/renderer/index.html");
    return new Response(html, {
      headers: { "content-type": "text/html; charset=utf-8" }
    });
  }

  // 404
  return new Response("Not Found", { status: 404 });
});

8.5 前端 UI

<!-- src/renderer/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>笔记应用 - Deno Desktop</title>
  <style>
    :root {
      --bg-primary: #1a1a2e;
      --bg-secondary: #16213e;
      --text-primary: #eaeaea;
      --text-secondary: #8b8b9a;
      --accent: #4a90e2;
      --danger: #e74c3c;
      --sidebar-width: 260px;
    }
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      background: var(--bg-primary);
      color: var(--text-primary);
      height: 100vh;
      display: flex;
      flex-direction: column;
    }
    .titlebar {
      height: 36px;
      background: var(--bg-secondary);
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 0 12px;
      -webkit-app-region: drag;
      user-select: none;
    }
    .titlebar-title { font-size: 13px; color: var(--text-secondary); }
    .titlebar-actions { display: flex; gap: 8px; -webkit-app-region: no-drag; }
    .titlebar-btn {
      width: 28px; height: 22px;
      border: none; border-radius: 4px;
      background: transparent; color: var(--text-secondary);
      cursor: pointer; font-size: 12px;
    }
    .titlebar-btn:hover { background: rgba(255,255,255,0.1); color: white; }
    .titlebar-btn.close:hover { background: var(--danger); color: white; }
    .app-container { flex: 1; display: flex; overflow: hidden; }
    .sidebar {
      width: var(--sidebar-width);
      background: var(--bg-secondary);
      border-right: 1px solid rgba(255,255,255,0.06);
      display: flex; flex-direction: column;
    }
    .sidebar-header {
      padding: 16px;
      border-bottom: 1px solid rgba(255,255,255,0.06);
    }
    .new-note-btn {
      width: 100%; padding: 10px;
      background: var(--accent); color: white;
      border: none; border-radius: 6px;
      font-size: 14px; cursor: pointer;
      font-weight: 500;
    }
    .new-note-btn:hover { opacity: 0.9; }
    .notes-list { flex: 1; overflow-y: auto; padding: 8px; }
    .note-item {
      padding: 10px 12px; border-radius: 6px;
      cursor: pointer; margin-bottom: 4px;
      transition: background 0.15s;
    }
    .note-item:hover { background: rgba(255,255,255,0.05); }
    .note-item.active { background: rgba(74,144,226,0.2); }
    .note-item-title { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
    .note-item-preview { font-size: 12px; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .editor { flex: 1; display: flex; flex-direction: column; padding: 24px 32px; }
    .editor-title {
      font-size: 24px; font-weight: 600; border: none;
      background: transparent; color: var(--text-primary);
      margin-bottom: 16px; outline: none;
    }
    .editor-content {
      flex: 1; font-size: 15px; line-height: 1.7;
      border: none; background: transparent; color: var(--text-primary);
      resize: none; outline: none;
    }
    .editor-footer {
      display: flex; justify-content: space-between; align-items: center;
      padding-top: 16px; border-top: 1px solid rgba(255,255,255,0.06);
      margin-top: 16px;
    }
    .save-status { font-size: 12px; color: var(--text-secondary); }
    .editor-actions { display: flex; gap: 8px; }
    .btn {
      padding: 8px 16px; border: none; border-radius: 6px;
      font-size: 13px; cursor: pointer; font-weight: 500;
    }
    .btn-primary { background: var(--accent); color: white; }
    .btn-danger { background: transparent; color: var(--danger); border: 1px solid var(--danger); }
    .btn:hover { opacity: 0.9; }
    .empty-state {
      flex: 1; display: flex; flex-direction: column;
      align-items: center; justify-content: center;
      color: var(--text-secondary);
    }
    .empty-state h2 { font-size: 20px; margin-bottom: 8px; }
    .empty-state p { font-size: 14px; }
  </style>
</head>
<body>
  <!-- 桌面标题栏 -->
  <div class="titlebar">
    <span class="titlebar-title">笔记应用</span>
    <div class="titlebar-actions">
      <button class="titlebar-btn" id="btn-minimize">−</button>
      <button class="titlebar-btn" id="btn-maximize">□</button>
      <button class="titlebar-btn close" id="btn-close">×</button>
    </div>
  </div>

  <div class="app-container">
    <!-- 侧边栏 -->
    <div class="sidebar">
      <div class="sidebar-header">
        <button class="new-note-btn" id="btn-new">+ 新建笔记</button>
      </div>
      <div class="notes-list" id="notes-list"></div>
    </div>

    <!-- 编辑器 -->
    <div class="editor" id="editor-area">
      <div class="empty-state" id="empty-state">
        <h2>选择或创建一篇笔记</h2>
        <p>点击左侧「新建笔记」开始</p>
      </div>
      <div id="editor-form" style="display:none; flex-direction: column; flex: 1;">
        <input class="editor-title" id="note-title" placeholder="笔记标题..." />
        <textarea class="editor-content" id="note-content" placeholder="在这里开始书写..."></textarea>
        <div class="editor-footer">
          <span class="save-status" id="save-status"></span>
          <div class="editor-actions">
            <button class="btn btn-danger" id="btn-delete">删除</button>
            <button class="btn btn-primary" id="btn-save">保存</button>
          </div>
        </div>
      </div>
    </div>
  </div>

  <script type="module">
    // 状态管理
    let notes = [];
    let currentNote = null;
    let saveTimeout = null;

    // API 封装
    const api = {
      async list() {
        const res = await fetch("/api/notes");
        return res.json();
      },
      async create(title, content) {
        const res = await fetch("/api/notes", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ title, content })
        });
        return res.json();
      },
      async update(id, data) {
        await fetch(`/api/notes/${id}`, {
          method: "PUT",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(data)
        });
      },
      async delete(id) {
        await fetch(`/api/notes/${id}`, { method: "DELETE" });
      }
    };

    // UI 更新
    function renderNotesList() {
      const list = document.getElementById("notes-list");
      list.innerHTML = notes.map(note => `
        <div class="note-item ${currentNote?.id === note.id ? "active" : ""}"
             data-id="${note.id}">
          <div class="note-item-title">${escapeHtml(note.title || "无标题")}</div>
          <div class="note-item-preview">${escapeHtml(note.content || "")}</div>
        </div>
      `).join("");

      // 事件委托:点击笔记
      list.querySelectorAll(".note-item").forEach(el => {
        el.addEventListener("click", () => {
          const note = notes.find(n => n.id === el.dataset.id);
          selectNote(note);
        });
      });
    }

    function selectNote(note) {
      currentNote = note;
      document.getElementById("empty-state").style.display = "none";
      document.getElementById("editor-form").style.display = "flex";
      document.getElementById("note-title").value = note.title;
      document.getElementById("note-content").value = note.content;
      document.getElementById("save-status").textContent = "";
      renderNotesList();
    }

    function escapeHtml(str) {
      return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
    }

    function showEmpty() {
      currentNote = null;
      document.getElementById("empty-state").style.display = "flex";
      document.getElementById("editor-form").style.display = "none";
      renderNotesList();
    }

    // 保存逻辑(防抖)
    async function saveNote() {
      if (!currentNote) return;
      const title = document.getElementById("note-title").value;
      const content = document.getElementById("note-content").value;
      await api.update(currentNote.id, { title, content });
      document.getElementById("save-status").textContent = "已保存 " + new Date().toLocaleTimeString();
    }

    function scheduleAutoSave() {
      if (saveTimeout) clearTimeout(saveTimeout);
      saveTimeout = setTimeout(saveNote, 1000);
    }

    // 事件绑定
    document.getElementById("btn-new").addEventListener("click", async () => {
      const note = await api.create("新笔记", "");
      notes.unshift(note);
      renderNotesList();
      selectNote(note);
    });

    document.getElementById("note-title").addEventListener("input", scheduleAutoSave);
    document.getElementById("note-content").addEventListener("input", scheduleAutoSave);

    document.getElementById("btn-save").addEventListener("click", async () => {
      if (saveTimeout) clearTimeout(saveTimeout);
      await saveNote();
    });

    document.getElementById("btn-delete").addEventListener("click", async () => {
      if (!currentNote) return;
      const confirmed = confirm("确定删除这篇笔记?");
      if (!confirmed) return;
      await api.delete(currentNote.id);
      notes = notes.filter(n => n.id !== currentNote.id);
      showEmpty();
    });

    // 窗口控制按钮(通过 bindings 调用 Deno 端)
    document.getElementById("btn-minimize").addEventListener("click", () => {
      // 标题栏按钮行为由 OS 控制
    });
    document.getElementById("btn-close").addEventListener("click", () => {
      // 窗口关闭由 OS 控制
    });

    // 初始化
    notes = await api.list();
    renderNotesList();
    if (notes.length > 0) {
      selectNote(notes[0]);
    }
  </script>
</body>
</html>

8.6 运行与打包

# 开发模式(带 HMR)
$ deno desktop --hmr main.ts

# 本地运行
$ deno desktop main.ts

# 构建 macOS 应用包
$ deno desktop --output DenoNotes.dmg main.ts

# 一键全平台构建
$ deno desktop --all-targets --output DenoNotes main.ts

九、Deno desktop 的局限性与生产注意事项

9.1 Experimental 状态

Deno 2.9 文档明确标注 deno desktop 为 experimental(实验性) 功能。这意味着:

  • API 签名和命令参数可能在后续版本中发生变化
  • 部分平台功能仍在完善中(如 Windows 上的完整菜单支持)
  • 生产使用前请关注 Deno 官方文档 的稳定性声明

9.2 WebView 版本的系统依赖

默认 WebView 后端依赖宿主系统安装的 WebView 组件。如果你的应用需要面向 Windows 7 用户群,需要显式使用 --backend cef,因为 Windows 7 没有 WebView2。

9.3 缺少的部分

相比成熟的 Electron 生态,deno desktop 在 2.9 阶段缺少一些功能:

  • 内置应用内购买(IAP)支持
  • 原生通知 API(Notification API 在 WebView 中可能不可用)
  • 深度系统集成(如 Dock badge 在非 macOS 平台)

十、总结:2.9 之于 Deno 的战略意义

Deno 2.9 不只是一个版本号更新。它代表了 Deno 作为平台的一次定位升级:

从「服务器端 JavaScript 运行时」到「全栈统一运行时」。deno desktop 的出现,让 Deno 真正具备了横跨前端框架、后端服务、桌面应用的完整能力——而你只需要学一套 API(Deno.*)、用一个工具链(deno CLI)、管理一份依赖(deno.json)。

性能全面跃升。冷启动减半、内存降低 2-3 倍、HTTP 吞吐量提升 1.27 倍,让 Deno 在服务端场景的竞争力上了一个台阶。

生态迁移零成本deno install 读取竞品锁文件的能力,是 Deno 在生态策略上最务实的一步:不再要求用户「迁移」,而是允许用户「直接使用」——这个转变对于推动现有 Node.js 开发者采用 Deno 至关重要。

展望:随着 deno desktop 的逐步成熟,Deno 的生态边界将继续扩展。如果它能持续改进、完善桌面 API、稳定 API 签名,那么在 2027-2028 年,deno desktop 有望成为中小企业桌面应用开发的第三极选项,与 Electron 和 Tauri 并列。对于已经在使用 Deno 做后端服务的团队,这是一条成本最低的桌面扩展路径。


参考资料

  • Deno 2.9 官方博客:https://deno.com/blog/v2.9
  • Deno Desktop 官方文档:https://docs.deno.com/runtime/desktop/
  • Deno 官方推特:https://twitter.com/den_land
  • deno compile 文档:https://docs.deno.com/runtime/manual/tools/compiler/
  • Deno KV 文档:https://docs.deno.com/runtime/manual/runtime/kv/
  • denidian 示例应用:https://github.com/bartlomieju/denidian

推荐文章

XSS攻击是什么?
2024-11-19 02:10:07 +0800 CST
PHP 代码功能与使用说明
2024-11-18 23:08:44 +0800 CST
如何优化网页的 SEO 架构
2024-11-18 14:32:08 +0800 CST
程序员茄子在线接单