---
title: Electron 入门：主进程、页面与有限的 IPC 能力
description: 沿 ChatCrystal 的模式选择、BrowserWindow、preload 和 Fastify 启动路径，理解桌面宿主与 Web 页面的权限和生命周期边界。
image: "https://zengliangyi.com/og/blog/electron-app-basics.png"
---

Electron 为 Web 页面提供桌面宿主，但主进程、渲染页面和 preload 具有不同职责。学习 ChatCrystal 时，先画出权限和进程边界，再跟踪启动与退出；不能仅把它概括成“给网页套一个窗口”。

## 先分清运行位置

按 [Electron 进程模型](https://www.electronjs.org/docs/latest/tutorial/process-model)，主进程管理应用与窗口，渲染进程运行页面，另有 GPU 等辅助进程。preload 是页面加载前的桥接脚本，不是独立的后端服务进程。页面是否能调用 Node 能力，还取决于配置及暴露的接口。

固定 [main.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/electron/main.ts) 定义 onboarding、local 与 cloud 模式。打包本地模式通过 importServerModule 加载编译后的后端，再在主进程调用 createServer；数据库与 Fastify 不因此获得一个独立 PID。开发时 VITE_DEV_URL 让页面指向 Vite，核心服务由外部启动。云端模式加载验证过的远端地址；本机历史上传另有子进程 worker。这些差异也影响 [内存测量](/blog/conversation-import-memory-budget)。

## BrowserWindow 的配置要照当前代码读

createWindow 恢复窗口状态，使用无边框窗口，最小宽高为 1280×800，show 为 false，并在 ready-to-show 后显示。它按模式选择 preload，webPreferences 明确设置 contextIsolation: true、nodeIntegration: false、sandbox: true。

这些设置限制页面的权限；它们不表示所有桥接方法都天然安全，也不表示页面只剩“最基本的 Web API”。[上下文隔离文档](https://www.electronjs.org/docs/latest/tutorial/context-isolation) 说明 preload 与页面使用不同 JavaScript 上下文。跨过这条边界的每个能力仍需要具体设计。

## 只暴露页面确实需要的能力

[本地 preload](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/electron/preload.ts) 除 isElectron 与版本信息，还提供 windowControls 与 updates。下面是其中的源码摘录，不是可独立启动的完整应用：



```typescript
windowControls  : {  
    minimize  : ()   =>   ipcRenderer.  invoke  (  'window:minimize'  ),  
    toggleMaximize  : ()   =>   ipcRenderer.  invoke  (  'window:toggle-maximize'  ),  
    close  : ()   =>   ipcRenderer.  invoke  (  'window:close'  ),  
    isMaximized  : ()   =>   ipcRenderer.  invoke  (  'window:is-maximized'  ),  
    onMaximizedChange  : (  callback  :   (  isMaximized  :   boolean  )   =>   void  )   =>   {  
      const   listener   =   (  _event  :   unknown  ,   isMaximized  :   boolean  )   =>   {  
        callback  (isMaximized);  
      };  
      ipcRenderer.  on  (  'window:maximized-change'  , listener);  
      return   ()   =>   ipcRenderer.  removeListener  (  'window:maximized-change'  , listener);  
    },  
  },
```



页面调用特定方法，而不是拿到原始 ipcRenderer。主进程窗口处理器从 event.sender 找所属窗口；事件订阅返回取消函数，调用方卸载时也需要执行清理。云端与 onboarding 的接口、允许来源检查应继续追踪 [IPC 注册](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/electron/ipc.ts)。不能把某一类处理器的校验概括成所有 IPC 都使用同一策略。

## 本地服务与模块格式各有边界

本地核心优先探测端口 3721，被占用时寻找空闲端口，然后调用 `createServer({ port, host: '127.0.0.1', startWatcher: false })`。窗口加载对应本地 URL；开发模式则使用配置的前端与核心地址。端口探测后释放再监听，并不消除其他进程抢占的可能，所以启动失败仍应处理。

加载器用 Function 构造动态 import 以保留当前编译路径的调用方式，同时先检查编译文件是否存在。这不是“Electron 主进程不支持 ESM”的证据；[官方 ESM 文档](https://www.electronjs.org/docs/latest/tutorial/esm) 区分主进程与 preload 的加载规则。应核对模块后缀、package.json 与 TypeScript 产物，避免照抄片段后随意关闭 sandbox。

本地 CSP 在没有 VITE_DEV_URL 时安装，并只对匹配的本地 HTTP 响应改写头；远端响应不会套用这段本地策略。开发代理、CSP、导航限制和 API 认证分别负责不同边界，不能互相替代。前端主题的构建与变量注入则见 [Tailwind 与 Vite](/blog/tailwind-v4-vite-setup)。

## 关闭窗口与退出应用是两件事

窗口 close 先尝试保存位置；未进入退出状态时阻止关闭并隐藏窗口。单实例锁与 second-instance 处理用于恢复现有窗口，但不能为其他 CLI 服务共享数据文件提供写锁。

before-quit 设置退出状态，再执行 gracefulShutdown：终止活动的远程导入 worker、等待本地核心 shutdown，最后销毁托盘。还有 10 秒超时强制 app.exit(1)，因此不能承诺退出一定完整保存。窗口状态保存本身也是 best effort。

做练习时，用独立开发数据目录，分别验证打开、隐藏、重新显示、正常退出、核心启动失败与退出超时。记录实际 PID、URL、可见窗口与保存结果；没有运行的路径不要写成已验证功能。数据层的显式保存边界可继续阅读 [sql.js 实战](/blog/sqljs-wasm-basics)。

分享：

##  相关文章

[##  Fastify 与 Electron：桌面服务的启动和退出边界

跟踪 ChatCrystal 的本地、云端与引导模式，验证 CommonJS 动态加载和端口竞争，核对窗口、安全头及服务关闭顺序。          曾亮懿       2026年10月7日      阅读约 5 分钟

#ChatCrystal

#Node.js

#Electron](/blog/fastify-electron-service-lifecycle)    [##  Electron 托盘与窗口状态：隐藏、恢复和退出的边界

对照 ChatCrystal 当前托盘菜单、窗口位置校验与退出流程，理解状态持久化能恢复什么，以及仍需测试的失败场景。          曾亮懿       2026年10月5日      阅读约 4 分钟

#ChatCrystal

#Electron

#Node.js](/blog/electron-tray-window-state)    [##  AI 对话格式差异：事件、快照与跨库消息定位

对照五个固定适配器，区分 JSONL 容器、事件语义与 SQLite 键布局，用两个内存数据库观察消息索引、孤立数据和未知版本。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#Node.js

#SQLite](/blog/ai-conversation-format-divergence)

##  从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录，欢迎交流实现细节。

[访问我的 GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Electron 入门：主进程、页面与有限的 IPC 能力",
  "description": "沿 ChatCrystal 的模式选择、BrowserWindow、preload 和 Fastify 启动路径，理解桌面宿主与 Web 页面的权限和生命周期边界。",
  "url": "https://zengliangyi.com/blog/electron-app-basics/",
  "image": "https://zengliangyi.com/images/blog/electron-app-basics.svg",
  "datePublished": "2026-10-04T00:00:00.000Z",
  "dateModified": "2026-10-04T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/blog/electron-app-basics/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "首页",
      "item": "https://zengliangyi.com/"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "文章",
      "item": "https://zengliangyi.com/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Electron 入门：主进程、页面与有限的 IPC 能力",
      "item": "https://zengliangyi.com/blog/electron-app-basics/"
    }
  ]
}
```
