配了三年桌面应用,今天才发现后端和浏览器之间根本不用 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 打包出来的二进制,启动时做了三件事:
- 运行时在本地随机选一个空闲端口,启动
Deno.serve() - 把
DENO_SERVE_ADDRESS(格式是tcp:127.0.0.1:xxxxx)注入进环境变量 - 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 产品桌面化,这是一个值得一试的选择。
评论区
登录后可评论。