本节摘要:Electron 的构建体系要同时服务主进程(Node 目标)、preload(受限沙箱目标)与前端(浏览器目标),主流方案是 Vite 三配置加 TypeScript 全覆盖。本节讲三个目标的构建差异、TS 类型如何统一两个世界(尤其 preload 桥的类型共享)、以及调试工具链的接入方式,并给出一个把老项目迁到 Vite 的实操案例。
先看三个目标对构建器的真实诉求。主进程跑在 Node 里:产物要 CommonJS 兼容(或处理 ESM 外部依赖),第三方包里的原生模块绝不能被打包器内联,必须保持外置。preload 目标产物越小越稳:理想是单文件、零依赖、沙箱可用。前端目标:一切照 Web 项目办理,代码分割、资源指纹、HMR 全都要。
一份配置同时满足"别打包这个"和"尽力分割那个",注定拧巴。所以现代方案的共识是三份配置各管一段,再由一条编排命令串起来。以 Vite 系方案为例,三个目标的关注点分列如下:
| 目标 | external 策略 | 产物形态 | 特殊关注 |
|---|---|---|---|
| 主进程 | electron 与原生模块外置 | 单文件或少量 chunk | 启动速度(第五章) |
| preload | 极简依赖 | 尽量单文件 | 沙箱可用 API 子集 |
| 前端 | 常规打包 | 静态资源目录 | HMR、代码分割 |

TS 在 Electron 项目里的最大价值不是类型本身,而是把主进程与前端"接壤处"的契约显式化——具体说就是 preload 暴露的桥接口。前端调用 window.bridge.xxx 时如果没有类型,参数拼错只能到运行时才炸;有了共享类型定义,编辑器当场标红。
做法是把桥的类型抽成一份声明,两端共用:
// 共享类型:描述桥上有哪些方法、参数与返回值长什么样 export interface AppBridge { readConfig(key: string): Promise<string | null>; saveConfig(key: string, value: string): Promise<void>; pickDirectory(): Promise<string | null>; onImportProgress(cb: (p: { done: number; total: number }) => void): () => void; } // 前端全局声明:让 window.bridge 获得完整类型 declare global { interface Window { bridge: AppBridge } }
// preload 实现处:用同一个接口约束自己,漏实现或多暴露都会编译报错 import { contextBridge, ipcRenderer } from 'electron'; import type { AppBridge } from './bridge-types'; const bridge: AppBridge = { readConfig: (key) => ipcRenderer.invoke('config:get', key), saveConfig: (key, value) => ipcRenderer.invoke('config:set', key, value), pickDirectory: () => ipcRenderer.invoke('dialog:pick-dir'), onImportProgress: (cb) => { const listener = (_e: unknown, p: { done: number; total: number }) => cb(p); ipcRenderer.on('import:progress', listener as never); return () => ipcRenderer.removeListener('import:progress', listener as never); } }; contextBridge.exposeInMainWorld('bridge', bridge);
主进程的 handle 端同样可以对齐这份契约。三个目标用同一份接口定义,IPC 通道就从"字符串魔法"升级成"编译期可验证的合同"——第四章安全审计时,这份合同还是能力清单的直接素材。
前端的调试就是 DevTools(窗口里打开即可),真正的知识点在主进程。主进程跑在 Node 上,调试方式是启动时带 inspector 参数,然后用任何支持 Node 调试的客户端连上去断点。脚手架或插件通常封装好了开关:
# 以调试模式启动:主进程将暴露 Node 调试端口 $ npm start -- --inspect Debugger attached. 主进程断点:在 IDE 里连 localhost:9229,可对 handle 处理器下断点 前端断点:窗口 DevTools,两边的调用栈互不可见(进程隔离的又一投影)
日志是另一条生命线。console.log 在开发期够用,但主进程日志打印在终端,用户机器上没有终端——生产期要靠日志库把主进程与渲染进程的日志统一落盘到用户数据目录,便于远程诊断(第七章还会接上崩溃报告)。开发期一个实用习惯:给 IPC 通道的调用统一加一层开发日志,进出消息各记一行,排查"桥上没反应"类问题效率极高。
背景:一个 Webpack 时代的 Electron 项目,构建要四十多秒,HMR 只覆盖前端,preload 改动要手动重启,团队苦之久矣。
操作:迁移按目标分三步走,每步独立验证。先迁前端管线(风险最低,页面行为可回归测试);再迁 preload(量小,重点验证沙箱下产物可用);最后迁主进程(重点验证原生模块没有被错误内联——迁移后启动即崩,九成是它)。每步迁完跑一轮完整的手工回归清单。
结果:构建从四十秒降到八秒左右,preload 改动自动触发窗口重载,主进程改动自动重启。代价是三天迁移工时加一份回归清单的维护。
解读:分目标迁移的顺序设计(低风险→高风险)比"一口气全换"稳得多。变式:如果项目重度依赖 Webpack 特有插件链(自定义 loader 生态),迁移动机就要重新评估——构建速度的收益要除以迁移风险系数。
流水线全速运转。下一节开始装配核心零件:app、BrowserWindow、Menu、Tray 四大件的工程化用法。