桌面应用与 Web 应用最大的不同在于:浏览器页面崩溃,刷新一下即可,用户不会觉得是你的程序“坏了”;可一个 Electron 应用如果某个窗口突然消失,或者整个程序闪退,用户就会直接归因于“这软件不稳定”。因此,在 Electron 项目中建立一套进程守护、崩溃自恢复与异常降级机制,不是“加分项”,而是必备的工程实践。
本节会从三个层次来构建这套保障体系:如何监控与守护渲染进程,如何在崩溃后快速恢复窗口,以及当某些功能不可用时如何优雅降级。
21.5.1 主进程守护渲染进程
Chromium 的多进程架构天然为渲染进程提供了隔离:一个窗口崩溃不会直接导致主进程退出。但默认情况下,Electron 不会自动帮你重启崩溃的窗口,需要主进程主动监听并处理。
基础方案:监听 render-process-gone 事件
从 Electron 6 开始,每个 BrowserWindow 实例都提供了一个 render-process-gone 事件。当渲染进程由于崩溃、被杀死或 OOM 等原因终止时,该事件会被触发。
// main.js
const { app, BrowserWindow, dialog } = require('electron');
function createWindow(url) {
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
},
});
win.loadURL(url);
// 监听渲染进程崩溃
win.webContents.on('render-process-gone', (event, details) => {
console.error(`渲染进程崩溃: ${details.reason}`);
// 避免默认的弹窗(Electron 默认会弹一个提示框)
event.preventDefault();
// 判断崩溃原因
if (details.reason === 'crashed' || details.reason === 'oom') {
// 尝试自动恢复
recoverFromCrash(win, url);
} else if (details.reason === 'killed') {
// 被系统或外部杀死的进程,记录日志即可
console.log('渲染进程被终止,不自动恢复');
}
});
}
恢复策略:重新创建窗口
恢复崩溃窗口时,不能简单地重新加载内存中那个已经“死掉”的 BrowserWindow 对象,而应该销毁旧实例并创建一个新的。
function recoverFromCrash(oldWin, url) {
// 获取旧窗口的位置和大小,尽量恢复原状
const [x, y] = oldWin.getPosition();
const [width, height] = oldWin.getSize();
// 销毁旧窗口
oldWin.destroy();
// 创建新窗口
const newWin = new BrowserWindow({
x,
y,
width,
height,
// 复用之前的 webPreferences 配置
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
},
});
newWin.loadURL(url);
// 可选:提示用户已自动恢复
newWin.webContents.on('did-finish-load', () => {
// 通过 IPC 告知渲染进程发生了恢复,方便清理脏数据
newWin.webContents.send('app:crash-recovered');
});
}
这种自动恢复对用户几乎是无感的——他们只会看到窗口闪了一下,然后内容重新加载。对于像聊天工具、文档编辑器这类对实时性要求不那么苛刻的应用,体验完全可以接受。
21.5.2 主进程自身的崩溃守护
渲染进程崩溃有主进程兜底,但主进程崩溃会导致整个应用直接退出。因此,对于需要长期运行的 Electron 应用(比如服务器监控面板、大屏幕展示系统),必须引入外部守护机制。
方案一:使用 child_process 启动守护进程
最简做法是编写一个轻量级的守护脚本(Node.js),由它来启动 Electron 主进程,并监听其退出状态。
// daemon.js
const { spawn } = require('child_process');
const path = require('path');
function startElectron() {
const electronPath = require('electron'); // 或直接指定 electron 可执行文件路径
const child = spawn(electronPath, [path.join(__dirname, 'main.js')], {
stdio: 'inherit',
env: process.env,
});
child.on('exit', (code, signal) => {
console.log(`Electron 主进程退出,code: ${code}, signal: ${signal}`);
if (code !== 0) {
console.log('非正常退出,3秒后重启');
setTimeout(startElectron, 3000);
} else {
console.log('正常退出,守护进程也退出');
process.exit(0);
}
});
child.on('error', (err) => {
console.error('启动 Electron 失败:', err);
process.exit(1);
});
}
startElectron();
用户实际打开的应该是这个守护脚本(也可以进一步打包成可执行文件)。这种方式适合一体机、信息亭、现场展示等需要长期无人值守运行的场景。
方案二:利用系统服务或 PM2 守护
如果应用的场景允许 Node.js 运行环境,可以直接使用 PM2 来管理 Electron 进程。
# 使用 PM2 启动 Electron 应用
pm2 start electron --name "my-app" -- main.js
PM2 会自动监控进程状态,支持崩溃重启、日志管理、开机自启等特性。但需要评估是否适合最终用户环境(通常更适合企业内部工具或开发阶段)。
21.5.3 渲染进程的优雅降级与错误处理
崩溃自恢复属于“事后补救”,更好的做法是在渲染进程内部捕获异常,让页面不至于直接崩溃。这需要结合前端的错误边界与 Electron 的环境特性。
全局异常捕获
在渲染进程的入口脚本中,注册 window.onunhandledrejection 和 window.onerror 事件,集中处理未捕获的异常和 Promise 拒绝。
// renderer.js(渲染进程)
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise 拒绝:', event.reason);
// 可以上报到日志服务,或通过 IPC 通知主进程
window.electronAPI.sendLog({
type: 'error',
message: event.reason?.message || 'Unhandled rejection',
});
// 阻止控制台报红但不影响后续代码执行
event.preventDefault();
});
window.onerror = (message, source, lineno, colno, error) => {
console.error('全局错误:', message);
window.electronAPI.sendLog({
type: 'error',
message: message,
stack: error?.stack,
});
// 返回 true 阻止默认的错误对话框(开发阶段除外)
return true;
};
React/Vue 错误边界
对于使用框架的渲染进程,应当利用组件级错误边界,让崩溃只影响局部 UI,而不是整个页面白屏。
React 示例:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// 上报错误日志
window.electronAPI.sendLog({
type: 'react-error-boundary',
message: error.message,
componentStack: errorInfo.componentStack,
});
}
handleRetry = () => {
this.setState({ hasError: false, error: null });
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<p>这部分内容加载出错</p>
<button onClick={this.handleRetry}>重试</button>
</div>
);
}
return this.props.children;
}
}
Vue 3 使用 onErrorCaptured 钩子实现类似效果。
降级策略:功能不可用时的处理
并非所有错误都会导致崩溃,有时是因为某些系统能力在当前平台不可用(如 Windows 上没有 touchBar,某些 Linux 环境没有托盘区)。这时需要优雅降级:
// 主进程:检查功能可用性
function createTrayIfAvailable() {
try {
// 某些 Linux 桌面环境可能不支持系统托盘
if (process.platform === 'linux') {
// 可以预先检查是否在支持的桌面环境中
const { session } = require('electron');
// 一种判断方式是尝试创建并捕获异常
}
const tray = new Tray('/path/to/icon.png');
return tray;
} catch (error) {
console.warn('系统托盘不可用,该功能已禁用');
// 通知渲染进程隐藏相关 UI
mainWindow.webContents.send('feature:tray-unavailable');
return null;
}
}
渲染进程收到通知后,可以隐藏或禁用“最小化到托盘”的选项,避免用户疑惑。
21.5.4 综合架构建议
一个完备的进程守护与异常恢复方案,应当整合多个层次:
- 渲染进程内部:错误边界 + 全局异常捕获,防止单个组件错误导致白屏。
- 渲染进程崩溃:主进程监听
render-process-gone,自动销毁并重建窗口。 - 主进程崩溃:外部守护脚本或 PM2 监听并自动重启。
- 长期运行状态监控:主进程定时向渲染进程发送心跳,若渲染进程无响应则主动强制重载(
webContents.forcefullyCrashRenderer()或reload())。 - 日志与上报:所有异常信息通过主进程收集,写入本地日志文件或上报到远程监控平台,方便后续排查。
通过这套分层的防护体系,你的 Electron 应用就具备了接近原生软件的稳定性表现——用户几乎感知不到崩溃的存在,即便出现极端情况,也能在几秒内自动恢复到可用状态,而不是无助地看到应用直接闪退。