配了三年桌面应用,今天才发现后端和浏览器之间根本不用 IPC——Deno Desktop 把这件事变成了 127.0.0.1 上的 HTTP 调用

配了三年桌面应用,今天才发现后端和浏览器之间根本不用 IPC——Deno Desktop 把这件事变成了 127.0.0.1 上的 HTTP 调用。

这不是在说「同进程更好」那种老话。Deno Desktop 的方案是:把整个 HTTP 服务器直接塞进打包后的二进制里,WebView 访问的其实是 127.0.0.1 上的一个本地端口。后端函数不需要走 IPC 序列化,直接通过 bindings 对象在进程内通信——这和 Electron 里的 ipcRenderer.invoke() 完全是两种思路。

这个架构在 Deno 2.9.6(2026-08-27)里有了新的具体能力:Clipboard API 正式进 stable,桌面菜单支持 checked/icon/tooltip 属性,Vite HMR 开发模式跑进了 desktop runtime 里。本文把这个设计从头说清楚。


先说清楚它是怎么跑起来的

Deno Desktop 打包出来的二进制,启动时做了三件事:

  1. 运行时在本地随机选一个空闲端口,启动 Deno.serve()
  2. DENO_SERVE_ADDRESS(格式是 tcp:127.0.0.1:xxxxx)注入进环境变量
  3. WebView 导航到 http://127.0.0.1:端口

换句话说:你的 UI 就是一个普通的 HTTP 服务器,WebView 是它的浏览器。Deno 2.9.6 文档里明确说了这一点,并且强调「bound address 永远是 127.0.0.1,编译后的二进制永远不会绑定公网地址」——这是安全边界,不是巧合。

整个过程不需要任何 desktop-specific serving API,Deno.serve() 写法和普通 Web 服务器完全一样。

// main.ts
Deno.serve((req) => {
  const url = new URL(req.url);
  if (url.pathname === "/api/hello") {
    return Response.json({ hello: "world" });
  }
  return new Response(HOMEPAGE, {
    headers: { "content-type": "text/html" },
  });
});

浏览器里写 fetch("/api/hello") 正常工作,WebSocket/cookies/所有 HTTP 语义都在——你不需要为桌面端改任何业务代码。这是 Deno Desktop 文档里最强调的一点:write once, run as web or as desktop


那 bindings 是什么,它解决了什么问题

如果后端和 WebView 之间走的是 HTTP,那每次调用后端函数都要发一个 HTTP 请求,然后后端处理、返回 JSON。这对大多数场景够用了。

但 Deno Desktop 引入了 bindings——一种不走 HTTP 的后端调用方式,在进程内直接传数据,零序列化开销。

// 后端:注册一个可从 UI 调用的函数
const win = new Deno.BrowserWindow({ title: "Bindings Test", width: 800, height: 600 });

win.bind("getSystemInfo", async (userName) => {
  console.log(`[Deno side] Called from frontend, arg: ${userName}`);
  const denoVersion = Deno.version.deno;
  const os = Deno.build.os;
  await new Promise(resolve => setTimeout(resolve, 500));
  return {
    message: `Hello, ${userName}!`,
    os: os,
    denoVersion: denoVersion,
  };
});

// 前端:HTML 里直接用 bindings 调用
Deno.serve(() => {
  const html = `
    <h1>Bindings Test</h1>
    <div id="result">Click the button</div>
    <button id="btn">Get System Info</button>
    <script>
      document.querySelector('#btn').addEventListener('click', async () => {
        const data = await bindings.getSystemInfo("前端工程师");
        document.getElementById('result').textContent = JSON.stringify(data, null, 2);
      });
    </script>
  `;
  return new Response(html, { headers: { "content-type": "text/html" } });
});

点击按钮,0.5 秒后前端收到 JSON,全程没有 HTTP 请求,没有 fetch,没有 ipcRenderer。这是 Deno.BrowserWindow 的 API,不是 Web 标准,但写法和 DX 跟普通 async/await 一模一样。

和 Electron 的 IPC 对比:

  • Electron:contextBridge + preload.js + ipcRenderer.invoke(),三次序列化(渲染进程→preload→主进程)
  • Tauri:invoke("plugin:xxx|command"),一次序列化,加 JSON RPC 开销
  • Deno Desktop:win.bind("funcName", fn),零序列化,进程内 channel 直接传

文档里明确说了这个区别:「calls are made over channels within the process. This means you don’t have to manage socket-based IPC directly, making the code clearer compared to Electron’s ipcMain/ipcRenderer or Tauri’s invoke.」


Deno 2.9.6 带来了什么新东西

8 月 27 日的 Deno 2.9.6 是这个桌面方案的又一次完善,最值得关注的是三个更新:

Clipboard API 正式进 stable。之前需要自己用 Deno.writeTextFile 绕过系统剪贴板,现在有了原生的 Deno.clipboard

// 读取剪贴板
const text = await Deno.clipboard.readText();

// 写入剪贴板
await Deno.clipboard.writeText("hello from Deno Desktop");

这对于桌面应用(截图工具、文档编辑器、数据处理工具)来说是高频需求,补全了 Electron/Tauri 里早就有的能力。

桌面菜单支持 checked/icon/tooltip。之前的菜单项只能显示文本和点击行为,现在可以加勾选状态、图标、悬浮提示:

win.setApplicationMenu([
  {
    submenu: {
      label: "View",
      items: [
        { item: { label: "Always on Top", id: "always-on-top", type: "checkbox", checked: false } },
        { role: { role: "toggleDevTools" } },
        { role: { role: "quit" } },
      ],
    },
  },
]);

Vite HMR 开发模式跑进了 desktop runtime。这是 2.9.6 的一个重要体验改进——之前在 Deno Desktop 里开发,改完代码要重新构建再启动,现在 deno desktop --hmr --backend=cef main.ts 可以热更新。


和 Tauri 的选型对比

说 Deno Desktop,就绕不开 Tauri。两者都是「Web 技术写 UI + 轻量运行时」的方案,但设计思路完全不同:

Deno Desktop Tauri
UI 渲染 系统 WebView(WebKit/WebView2)或 CEF 系统 WebView 或 WRY(Rust WebView)
后端语言 TypeScript/JavaScript Rust
后端调用 bindings 进程内调用 invoke() JSON RPC
启动速度 17.3ms(hello world) 更快(Rust 二进制)
包体(WebView) ~40MB ~3-5MB(不含 WebView)
npm 兼容 完整兼容 有限(需要 WASM 编译层)
框架检测 Next.js/Astro/Nuxt 等开箱即用 手动配置
状态 实验性(API 不稳定) 生产可用

Tauri 的包体更小(Rust runtime 本身就很小),但你要写 Rust。Deno Desktop 用 TypeScript 全栈,对已有 Web 团队的迁移成本几乎为零。

一个实际的选型判断:

  • TS 团队,做企业内部工具或 Web 产品桌面化 → Deno Desktop 最优解
  • 包体敏感、愿意写 Rust、需要系统级能力(FFI、嵌入式) → Tauri
  • Electron 已有成熟方案,不着急迁移 → 继续用 Electron,等 Deno Desktop 稳定

怎么跑起来

最简路径,三行命令:

# 升级到 canary(Desktop API 目前在 canary 里)
deno upgrade canary

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

# 打包所有平台
deno desktop --all-targets main.ts

--all-targets 会在同一台机器上交叉编译 macOS (.dmg)/Windows (.exe/.msi)/Linux (.AppImage/.deb/.rpm),不需要目标平台的 toolchain——macOS 上直接打出 Windows exe。

输出包体方面,默认是 WebView 后端(复用了系统 WebView,所以不含 Chromium),约 40MB;加 --backend=cef 捆绑 Chromium,约 150MB,但跨平台渲染完全一致。


总结一下

Deno Desktop 不是一个「更好的 Electron」,它是一个完全不同的思路:把 HTTP 服务器和 WebView 打包进同一个二进制,用 127.0.0.1 本地端口通信,用 bindings 替代 IPC——对前端工程师来说,后端调用几乎就是在写 await bindings.xxx(),心智负担和写普通 async 函数没区别。

Deno 2.9.6 的 Clipboard API 和菜单属性让这个方案从「玩具」向「可用的桌面开发平台」又近了一步。如果你已经用 Deno 写过服务端,想快速把 Web 产品桌面化,这是一个值得一试的选择。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 13 阅读