Electron 入门:Web 应用打包成桌面软件
本文面向想把 Web 应用打包成桌面软件的前端开发者。预计阅读时间12 分钟最终效果理解 Electron 的核心概念主进程、渲染进程、安全模型掌握 BrowserWindow 配置、服务器嵌入和窗口状态持久化。Electron 是什么Electron 把 ChromiumChrome 的内核和 Node.js 打包到了一起。你的 Web 前端跑在 Chromium 里你的后端逻辑跑在 Node.js 里两者通过 IPC进程间通信连接。VS Code、Slack、Discord 都是 Electron 应用。对 Web 开发者来说最大的好处是你已有的 HTML/CSS/JS/React 代码可以直接复用不需要学 Swift 或 C。最小项目结构一个 Electron 项目至少需要三样东西my-app/ package.json # 入口指向 main.js main.js # 主进程创建窗口、管理生命周期 preload.js # 预加载脚本安全地暴露 API 给前端 index.html # 你的 Web 页面package.json里的main: main.js告诉 Electron 从哪个文件启动。运行npx electron .就能打开一个桌面窗口里面显示你的 HTML。主进程 vs 渲染进程这是 Electron 最核心的概念主进程Main Process运行main.js的 Node.js 环境。它能访问文件系统、操作系统 API、创建窗口。一个应用只有一个主进程。渲染进程Renderer Process每个BrowserWindow里跑的是一个独立的 Chromium 页面和你在浏览器里打开网页一样。它不能直接访问 Node.js API。两个进程各司其职。主进程负责管家工作窗口、托盘、菜单、系统交互渲染进程负责展示工作UI、用户交互。它们通过contextBridge安全地通信。BrowserWindow 配置创建窗口的核心是new BrowserWindow(options)。ChatCrystal 的配置如下constwinnewBrowserWindow({width:state.width,// 从保存的状态恢复或默认 1280height:state.height,// 默认 800x:state.x,y:state.y,minWidth:900,// 最小宽度防止窗口被拖得太小minHeight:600,show:false,// 先不显示等页面加载完再显示title:ChatCrystal,icon:iconPath,webPreferences:{preload:path.join(__dirname,preload.js),contextIsolation:true,nodeIntegration:false,sandbox:true,},});几个关键点show: false配合ready-to-show事件使用避免窗口先闪一下白屏再加载内容。minWidth/minHeight保证 UI 不会被压到变形。webPreferences是安全相关的配置下面专门讲。安全设置Electron 的安全模型遵循一个原则渲染进程不应该有特权。三个配置项实现这一点contextIsolation: true渲染进程的 JavaScript 和 preload 脚本运行在不同的上下文里。即使网页被注入恶意代码它也无法访问 preload 脚本里的 Node.js 对象。nodeIntegration: false渲染进程里不能直接require(fs)之类的 Node.js 模块。这是关闭的因为网页内容可能来自用户输入比如 AI 对话内容如果能执行任意 Node.js 代码就是远程代码执行漏洞。sandbox: true进一步限制让渲染进程连 Chromium 的扩展 API 都用不了只保留最基本的 Web 能力。那渲染进程怎么和主进程通信通过 preload 脚本// preload.tsimport{contextBridge}fromelectron;contextBridge.exposeInMainWorld(electronAPI,{isElectron:true,versions:{electron:process.versions.electron,node:process.versions.node,chrome:process.versions.chrome,},});contextBridge.exposeInMainWorld是唯一安全的方式它把指定的对象挂到window.electronAPI上。前端代码可以读window.electronAPI.isElectron来判断自己是不是跑在 Electron 里但无法访问任何危险的 Node.js API。CSP内容安全策略是另一层防护。ChatCrystal 在生产环境设置了严格的 CSP 头session.defaultSession.webRequest.onHeadersReceived((details,callback){callback({responseHeaders:{...details.responseHeaders,Content-Security-Policy:[default-src self; script-src self; style-src self unsafe-inline; img-src self data: blob:; font-src self data:; connect-src self http://localhost:* ws://localhost:*; object-src none; base-uri self,],},});});CSP 告诉浏览器只允许加载同源的脚本禁止内联脚本script-src self禁止插件object-src none。这对 ChatCrystal 尤其重要因为它会渲染 AI 对话内容必须防止 XSS 注入。注意开发环境跳过了 CSP因为 Vite 的 HMR热更新需要注入内联脚本。嵌入 Fastify 服务器很多 Electron 应用只是展示静态页面但 ChatCrystal 需要一个后端服务器来处理数据库、向量搜索等逻辑。做法是把 Fastify 服务器直接嵌入主进程asyncfunctionstartServer(port:number){constserverEntrypathToFileURL(path.join(app.getAppPath(),server,dist,server,src,index.js),).href;constserverModuleawaitFunction(specifier,return import(specifier),)(serverEntry);returnserverModule.createServer({port,host:127.0.0.1});}这里有个技巧Function(specifier, return import(specifier))是一个绕过 Electron 主进程 CJS 限制的 workaround。Electron 的主进程默认是 CommonJS 模块不能直接用await import()加载 ESM 模块。通过Function构造器可以绕过这个限制。启动时先检测端口如果 3721 被占用就随机分配一个functionfindFreePort(preferred:number):Promisenumber{returnnewPromise((resolve,reject){constsrvnet.createServer();srv.listen(preferred,127.0.0.1,(){srv.close(()resolve(preferred));});srv.on(error,(){constsrv2net.createServer();srv2.listen(0,127.0.0.1,(){constport(srv2.address()asnet.AddressInfo).port;srv2.close(()resolve(port));});});});}然后创建窗口加载服务器的 URLmainWindowcreateWindow();consturldevUrl||http://localhost:${serverPort};awaitmainWindow.loadURL(url);开发模式下devUrl指向 Vite 开发服务器http://localhost:13721生产模式下指向内嵌的 Fastify 服务器。窗口状态持久化用户把窗口拖到副屏、调了大小下次打开应该恢复到原来的位置和尺寸。ChatCrystal 用一个 JSON 文件实现functionloadWindowState():WindowState{try{constdatareadFileSync(getWindowStatePath(),utf-8);returnJSON.parse(data);}catch{return{width:1280,height:800,isMaximized:false};}}functionsaveWindowState(win:BrowserWindow):void{constisMaximizedwin.isMaximized();constboundsisMaximized?(lastNormalBounds??win.getBounds()):win.getBounds();writeFileSync(getWindowStatePath(),JSON.stringify(state));}保存的路径是app.getPath(userData)/window-state.json这是 Electron 提供的用户数据目录跨平台且不会和应用代码混在一起。还有一个细节如果用户拔掉了外接显示器上次保存的窗口位置可能在屏幕外面。所以恢复时要检查if(state.x!undefinedstate.y!undefined){constdisplaysscreen.getAllDisplays();constvisibledisplays.some((d){constbd.bounds;return(state.x!b.x-50state.x!b.xb.widthstate.y!b.y-50state.y!b.yb.height);});if(!visible){state.xundefined;// 重置位置让系统自动放置state.yundefined;}}- 50的容差是为了处理窗口边缘刚好贴着屏幕边界的情况。单实例锁桌面应用通常只允许运行一个实例。用户双击图标时如果已经有实例在跑应该把已有窗口激活而不是再开一个。constgotLockapp.requestSingleInstanceLock();if(!gotLock){app.quit();}拿到锁的实例继续运行。没拿到锁的直接退出。同时监听second-instance事件当用户再次尝试启动时把已有窗口显示出来app.on(second-instance,(){if(mainWindow){if(mainWindow.isMinimized())mainWindow.restore();mainWindow.show();mainWindow.focus();}});应用生命周期Electron 的生命周期事件串起了整个应用的运行逻辑。ChatCrystal 的启动流程在app.whenReady()里app.whenReady().then(async(){// 1. 确定数据目录constdataDirgetDataDir();mkdirSync(dataDir,{recursive:true});// 3. 设置环境变量process.env.ELECTRONtrue;process.env.DATA_DIRdataDir;if(app.isPackaged){process.env.ELECTRON_PACKAGEDtrue;}// 4. 设置 CSP生产环境// 5. 检测端口serverPortawaitfindFreePort(3721);// 6. 启动 Fastify 服务器开发模式跳过服务器单独运行if(!process.env.VITE_DEV_URL){constserverawaitstartServer(serverPort);serverShutdownserver.shutdown;}// 7. 创建窗口、加载页面、创建托盘mainWindowcreateWindow();awaitmainWindow.loadURL(url);createTray(mainWindow,serverPort);});退出时before-quit事件触发优雅关闭app.on(before-quit,(e){if(!isQuitting){e.preventDefault();isQuittingtrue;consttimeoutsetTimeout((){app.exit(1);// 10 秒超时强制退出},10000);gracefulShutdown().finally((){clearTimeout(timeout);app.quit();});}});gracefulShutdown依次关闭 Fastify 服务器和系统托盘。10 秒超时是为了防止关闭流程卡死——如果数据库保存之类的事情出了问题应用不会永远挂在那里。窗口关闭时不是真正退出而是隐藏到托盘win.on(close,(e){saveWindowState(win);if(!isQuitting){e.preventDefault();win.hide();}});用户通过托盘菜单的Quit才真正退出这时isQuitting已经被设为trueclose事件不会被拦截。下一步你现在了解了 Electron 的核心概念主进程与渲染进程的分工、BrowserWindow 配置、安全模型、服务器嵌入、窗口状态持久化、单实例锁和生命周期管理。如果你想深入IPC 通信ipcMain.handle/ipcRenderer.invoke用于渲染进程调用主进程的功能比如打开文件对话框自动更新electron-updater可以实现应用内更新打包发布electron-builder可以打包成 Windows 安装包NSIS、macOS DMG、Linux AppImage性能优化懒加载窗口、减少主进程阻塞操作ChatCrystal 的完整 Electron 代码在electron/目录下可以直接作为参考项目。从一个能跑的最小结构开始逐步加上你需要的功能这是最快的学习路径。项目地址github.com/ZengLiangYi/ChatCrystal