跳到正文
曾亮懿 ZengLiangYi Blog

Electron 入门:主进程、页面与有限的 IPC 能力

沿 ChatCrystal 的模式选择、BrowserWindow、preload 和 Fastify 启动路径,理解桌面宿主与 Web 页面的权限和生命周期边界。

曾亮懿

曾亮懿

阅读约 4 分钟
目录 展开 收起

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

先分清运行位置

按 Electron 进程模型,主进程管理应用与窗口,渲染进程运行页面,另有 GPU 等辅助进程。preload 是页面加载前的桥接脚本,不是独立的后端服务进程。页面是否能调用 Node 能力,还取决于配置及暴露的接口。

固定 main.ts 定义 onboarding、local 与 cloud 模式。打包本地模式通过 importServerModule 加载编译后的后端,再在主进程调用 createServer;数据库与 Fastify 不因此获得一个独立 PID。开发时 VITE_DEV_URL 让页面指向 Vite,核心服务由外部启动。云端模式加载验证过的远端地址;本机历史上传另有子进程 worker。这些差异也影响 内存测量。

BrowserWindow 的配置要照当前代码读

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

这些设置限制页面的权限;它们不表示所有桥接方法都天然安全,也不表示页面只剩“最基本的 Web API”。上下文隔离文档 说明 preload 与页面使用不同 JavaScript 上下文。跨过这条边界的每个能力仍需要具体设计。

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

本地 preload 除 isElectron 与版本信息,还提供 windowControls 与 updates。下面是其中的源码摘录,不是可独立启动的完整应用:

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 注册。不能把某一类处理器的校验概括成所有 IPC 都使用同一策略。

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

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

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

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

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

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

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

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

分享:

相关文章

Fastify 与 Electron:桌面服务的启动和退出边界

跟踪 ChatCrystal 的本地、云端与引导模式,验证 CommonJS 动态加载和端口竞争,核对窗口、安全头及服务关闭顺序。

曾亮懿 曾亮懿
阅读约 5 分钟
#ChatCrystal #Node.js #Electron

Electron 托盘与窗口状态:隐藏、恢复和退出的边界

对照 ChatCrystal 当前托盘菜单、窗口位置校验与退出流程,理解状态持久化能恢复什么,以及仍需测试的失败场景。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #Electron #Node.js

AI 对话格式差异:事件、快照与跨库消息定位

对照五个固定适配器,区分 JSONL 容器、事件语义与 SQLite 键布局,用两个内存数据库观察消息索引、孤立数据和未知版本。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #Node.js #SQLite

从文章到代码

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