Electron 太胖了?试试 Electrobun,12MB 打包一个 AI 桌面助手

前两天刷掘金热榜看到 Electrobun 这个名字,第一反应是------又一个 Electron 替代品?Tauri 不是已经卷过一轮了吗?

但是当我看到打包体积 12MB 的时候,还是没忍住试了一下。结果一个下午就撸出了一个能用的 AI 聊天桌面助手,打包完一看体积,确实有点离谱。

先说结论

对比项 Electron Tauri Electrobun
运行时 Node.js + Chromium Rust + 系统 WebView Bun + 系统 WebView
开发语言 JS/TS Rust + JS/TS 纯 TypeScript
Hello World 包体积 ~270MB ~8MB ~12MB
冷启动速度 很快
学习成本 高(要会 Rust)
生态成熟度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐(刚 v1)

Tauri 体积更小,但你得写 Rust。Electrobun 的卖点就是:纯 TypeScript 全栈,不用学新语言,体积还能压到 12MB 级别。

为什么不用 Electron?

不是黑 Electron,我之前的几个小工具都是 Electron 写的。但问题真的很实际:

  1. 一个 Hello World 就 270MB,用户下载要等半天
  2. 每个 Electron 应用都自带一个 Chromium,开 3 个 Electron 应用等于开了 3 个 Chrome
  3. 内存占用,随便一个小工具就吃 200MB+ 内存

Tauri 解决了体积和性能问题,但代价是你得写 Rust。对于纯前端来说,这个门槛确实不低。

Electrobun 的思路是:用 Bun 替代 Node.js 做主进程(Bun 本身就比 Node 快),渲染层用操作系统自带的 WebView(macOS 用 WebKit,Windows 用 Edge WebView2),不捆绑浏览器引擎。

上手:5 分钟跑起来

前置条件:装好 Bun(没装的话 curl -fsSL https://bun.sh/install | bash)。

bash 复制代码
# 创建项目
bunx electrobun init my-ai-chat
cd my-ai-chat

# 装依赖
bun install

# 跑起来
bun run dev

跑完你会看到一个原生窗口弹出来,里面是一个简单的欢迎页面。整个过程不到 1 分钟。

项目结构长这样:

perl 复制代码
my-ai-chat/
├── src/
│   ├── main.ts              # 主进程(Bun 环境)
│   └── renderer/
│       ├── index.html        # 页面
│       ├── style.css         # 样式
│       └── script.ts         # 前端逻辑
├── electrobun.config.ts      # 构建配置
└── package.json

和 Electron 的结构很像,main.ts 对应 Electron 的 main.jsrenderer/ 对应渲染进程。

改造成 AI 聊天助手

我的目标:做一个桌面版的 AI 聊天工具,支持多模型切换(GPT-4o、Claude、DeepSeek 等),流式输出。

主进程:创建窗口 + IPC

typescript 复制代码
// src/main.ts
import { BrowserWindow } from "electrobun/bun";

const win = new BrowserWindow({
  title: "AI Chat Desktop",
  width: 800,
  height: 600,
  url: "electrobun://renderer/index.html",
});

// 监听渲染进程发来的消息
win.webview.onMessage("chat-request", async (data) => {
  const { model, messages } = data;

  try {
    // 调用 AI API,流式返回
    const response = await fetch("https://api.ofox.ai/v1/chat/completions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.OFOX_API_KEY}`,
      },
      body: JSON.stringify({
        model: model,
        messages: messages,
        stream: true,
      }),
    });

    const reader = response.body?.getReader();
    const decoder = new TextDecoder();

    while (reader) {
      const { done, value } = await reader.read();
      if (done) break;

      const chunk = decoder.decode(value);
      const lines = chunk.split("\n").filter((line) => line.startsWith("data:"));

      for (const line of lines) {
        const json = line.slice(5).trim();
        if (json === "[DONE]") continue;

        try {
          const parsed = JSON.parse(json);
          const content = parsed.choices?.[0]?.delta?.content;
          if (content) {
            // 实时推送到渲染进程
            win.webview.sendMessage("chat-stream", { content });
          }
        } catch {}
      }
    }

    win.webview.sendMessage("chat-done", {});
  } catch (err) {
    win.webview.sendMessage("chat-error", { error: String(err) });
  }
});

这里有个细节:Electrobun 的 IPC 通信用的是 onMessage / sendMessage,比 Electron 的 ipcMain / ipcRenderer 简洁不少。不需要单独写 preload 脚本。

渲染进程:聊天界面

html 复制代码
<!-- src/renderer/index.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>AI Chat</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div id="app">
    <div class="header">
      <select id="model-select">
        <option value="gpt-4o">GPT-4o</option>
        <option value="claude-sonnet-4-20250514">Claude Sonnet 4</option>
        <option value="deepseek-chat">DeepSeek V3</option>
        <option value="qwen-plus">Qwen Plus</option>
      </select>
    </div>
    <div id="messages" class="messages"></div>
    <div class="input-area">
      <textarea id="input" placeholder="输入消息..." rows="3"></textarea>
      <button id="send-btn">发送</button>
    </div>
  </div>
  <script src="./script.ts"></script>
</body>
</html>
typescript 复制代码
// src/renderer/script.ts
import { webview } from "electrobun/webview";

const messagesDiv = document.getElementById("messages")!;
const input = document.getElementById("input") as HTMLTextAreaElement;
const sendBtn = document.getElementById("send-btn")!;
const modelSelect = document.getElementById("model-select") as HTMLSelectElement;

let chatHistory: Array<{ role: string; content: string }> = [];
let currentAssistantMsg: HTMLDivElement | null = null;

function addMessage(role: string, content: string) {
  const div = document.createElement("div");
  div.className = `message ${role}`;
  div.textContent = content;
  messagesDiv.appendChild(div);
  messagesDiv.scrollTop = messagesDiv.scrollHeight;
  return div;
}

sendBtn.addEventListener("click", () => {
  const text = input.value.trim();
  if (!text) return;

  // 显示用户消息
  addMessage("user", text);
  chatHistory.push({ role: "user", content: text });

  // 创建助手消息占位
  currentAssistantMsg = addMessage("assistant", "") as HTMLDivElement;

  // 发送到主进程
  webview.sendMessage("chat-request", {
    model: modelSelect.value,
    messages: chatHistory,
  });

  input.value = "";
  sendBtn.disabled = true;
});

// 接收流式响应
webview.onMessage("chat-stream", (data) => {
  if (currentAssistantMsg) {
    currentAssistantMsg.textContent += data.content;
    messagesDiv.scrollTop = messagesDiv.scrollHeight;
  }
});

webview.onMessage("chat-done", () => {
  if (currentAssistantMsg) {
    chatHistory.push({
      role: "assistant",
      content: currentAssistantMsg.textContent || "",
    });
  }
  currentAssistantMsg = null;
  sendBtn.disabled = false;
});

// Ctrl+Enter 发送
input.addEventListener("keydown", (e) => {
  if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
    sendBtn.click();
  }
});

样式(简洁暗色主题)

css 复制代码
/* src/renderer/style.css */
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: #1a1a2e;
  color: #eee;
  height: 100vh;
}

#app {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.header {
  padding: 12px 16px;
  background: #16213e;
  border-bottom: 1px solid #333;
}

.header select {
  background: #0f3460;
  color: #eee;
  border: 1px solid #444;
  padding: 6px 12px;
  border-radius: 6px;
  font-size: 14px;
}

.messages {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
}

.message {
  margin-bottom: 12px;
  padding: 10px 14px;
  border-radius: 10px;
  max-width: 80%;
  line-height: 1.6;
  white-space: pre-wrap;
}

.message.user {
  background: #0f3460;
  margin-left: auto;
}

.message.assistant {
  background: #1a1a3e;
  border: 1px solid #333;
}

.input-area {
  display: flex;
  gap: 8px;
  padding: 12px 16px;
  background: #16213e;
  border-top: 1px solid #333;
}

.input-area textarea {
  flex: 1;
  background: #0f3460;
  color: #eee;
  border: 1px solid #444;
  border-radius: 8px;
  padding: 10px;
  font-size: 14px;
  resize: none;
}

.input-area button {
  background: #e94560;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 14px;
}

.input-area button:hover { background: #c81e45; }
.input-area button:disabled { opacity: 0.5; cursor: not-allowed; }

打包体验

bash 复制代码
bun run build

打包完成后:

bash 复制代码
dist/
└── AI Chat Desktop.app   # macOS
    └── (总大小: ~64MB)

等等,说好的 12MB 呢?

这里要解释一下:12MB 是 Electrobun 框架本身的开销,加上你的业务代码和依赖。我的项目因为没有额外的 npm 包(AI 调用用的是原生 fetch),实际打包大约 64MB,主要是 Bun runtime 占了大头。

作为对比,同样功能的 Electron 版本打包后 310MB。差了将近 5 倍。

而且 Electrobun 有个杀手锏:差分更新。它内置了增量更新机制,版本迭代时只推送差异补丁,补丁大小可以小到 14KB。Electron 每次更新基本要重新下载整个 Chromium。

踩坑记录

1. Bun 版本兼容

Electrobun v1 要求 Bun >= 1.2。我一开始用的 1.1.x,bunx electrobun init 直接报了一堆类型错误。升级 Bun 后就好了:

bash 复制代码
bun upgrade

2. 环境变量加载

Bun 主进程不会自动读 .env 文件。需要手动加载:

typescript 复制代码
// main.ts 顶部
import { $ } from "bun";
// 或者直接在 electrobun.config.ts 里配 env

我最后的方案是在 electrobun.config.tsenv 字段里写死(开发时),打包时从系统环境变量读取。

3. WebView 兼容性

macOS 上用的是 WebKit,不是 Chromium。这意味着一些 Chrome 特有的 API 不能用。我一开始用了 structuredClone 在 IPC 里传数据,结果在某些 macOS 版本上挂了。改成 JSON.parse(JSON.stringify(...)) 就没问题了。

4. 流式响应的坑

Bun 的 fetch 对 SSE 流式响应的支持和 Node.js 有点不一样。response.body 返回的是 ReadableStream,需要用 getReader() 来读,不能直接 for await...of。这个搞了我半小时。

值不值得用?

说实话,Electrobun 目前还是 v1 早期阶段,生态和 Electron 没法比。但如果你的场景是:

  • 轻量级工具类应用(不需要复杂原生功能)
  • 对包体积敏感(给客户分发不想让人等半天)
  • 团队全是前端,不想碰 Rust
  • 想尝鲜 Bun 生态

那完全值得试试。

我这个 AI 聊天桌面助手的完整流程:从 bunx electrobun init 到打包出可用的 .app,大概 3 小时(包括踩坑时间)。体验下来比第一次用 Electron 顺畅不少,至少不用折腾 webpack 配置和 preload 脚本。

API 层我用的是兼容 OpenAI 协议的聚合接口,改个 base_url 就能切不同模型,省得每个模型单独对接 SDK。如果你也想做类似的多模型桌面工具,这个思路可以参考。

完整代码我后续整理后会放 GitHub,有兴趣的可以先 mark 一下。

相关推荐
小鲤鱼ya2 小时前
vue3 + ts + uni-app 移动端封装图片上传添加水印
前端·typescript·uni-app·vue3
zhangjikuan892 小时前
在 ArkTS 中,Promise 的使用比 TypeScript 更严格(必须显式指定泛型类型)
前端·javascript·typescript
向上的车轮2 小时前
TypeORM——基于 TypeScript/JavaScript 的对象关系映射(ORM)框架
javascript·typescript·typeorm
Yan-英杰3 小时前
TypeScript+React 全栈生态实战:从架构选型到工程落地,告别开发踩坑
javascript·学习·typescript
floret. 小花3 小时前
Vue3 知识点总结 · 2026-03-20
前端·面试·electron·学习笔记·vue3
牧码岛4 小时前
服务端之NestJS请求解析体系、从HTTP报文到参数注入的工程化实践、控制器方法、装饰器、Headers、Query、Param、Body、Req
typescript·nestjs
We་ct16 小时前
LeetCode 148. 排序链表:归并排序详解
前端·数据结构·算法·leetcode·链表·typescript·排序算法
梦鱼20 小时前
🖥️ 告别 Electron 托盘图标模糊:一套精准的 PNG 生成方案
前端·electron
紫_龙21 小时前
最新版vue3+TypeScript开发入门到实战教程之DOM操作
javascript·vue.js·typescript