桌面应用与 Web 应用在错误处理上的最大区别在于:用户无法简单地“刷新页面”。如果一个未被捕获的异常导致窗口白屏或应用闪退,用户只能重启程序,体验极差。因此,Electron 应用必须建立一套全局异常捕获机制,确保在任何意外发生时,程序至少能优雅降级,而不是直接崩溃。
本节将分别覆盖主进程和渲染进程的异常捕获策略,并给出白屏场景的应对方案。
21.3.1 主进程的全局异常处理
主进程是应用的心脏,一旦它崩溃,所有窗口都会消失。Node.js 提供了两个全局事件来兜底未捕获的异常。
未捕获的同步异常
使用 process.on('uncaughtException') 可以捕获所有未被 try/catch 包裹的同步错误。
// main.js(主进程入口)
process.on('uncaughtException', (error) => {
console.error('主进程未捕获异常:', error);
// 记录到日志文件
logToFile(`uncaughtException: ${error.stack}`);
// 友好提示用户(如果还能弹出窗口)
dialog.showErrorBox('发生错误', '应用遇到了一个意外问题,建议重新启动。');
// 这里的策略通常是:记录日志后,让应用继续运行或优雅退出
// 如果错误严重,可调用 app.quit()
});
真实建议:不要在此处盲目地重新创建窗口或继续执行,因为错误可能已经污染了主进程的状态。许多成熟应用的做法是记录错误并提示用户重启,或自动重启应用(使用
app.relaunch()和app.exit())。
未捕获的 Promise 拒绝
从 Node.js 15 开始,未处理的 Promise 拒绝会触发 unhandledRejection 事件。
process.on('unhandledRejection', (reason, promise) => {
console.error('主进程未处理的 Promise 拒绝:', reason);
logToFile(`unhandledRejection: ${reason?.stack || reason}`);
// 根据需要决定是否退出
// 对于非致命的 API 调用失败,可以只记录日志
});
注意:在生产环境中,不要依赖默认行为(打印警告),必须显式监听这两个事件,否则应用可能在无任何提示的情况下直接退出。
结合日志系统
在主进程的异常处理中,最重要的动作是将错误信息写入本地文件。可以使用 electron-log 等库,或自己写一个简单的追加日志模块。
const fs = require('fs');
const path = require('path');
function logToFile(message) {
const logPath = path.join(app.getPath('userData'), 'error.log');
const line = `[${new Date().toISOString()}] ${message}\n`;
fs.appendFileSync(logPath, line);
}
21.3.2 渲染进程的全局异常处理
渲染进程中,未捕获的异常会导致当前窗口的 JavaScript 执行停止,通常表现为白屏或部分功能失效。通过监听 window 的 error 和 unhandledrejection 事件,并结合 Electron 的 webContents 事件,可以搭建完整的保护网。
捕获渲染进程中的同步错误和 Promise 拒绝
在一个渲染进程的预加载脚本或页面脚本中,可以设置全局守卫:
// preload.js 或直接在渲染进程的 HTML 中执行
window.addEventListener('error', (event) => {
const error = event.error || event.message;
console.error('渲染进程错误:', error);
// 发送错误信息给主进程进行日志记录
window.electronAPI.sendError({
type: 'renderer-error',
message: error?.stack || error,
url: event.filename,
line: event.lineno,
});
// 阻止默认行为,避免在控制台额外报错
event.preventDefault();
});
window.addEventListener('unhandledrejection', (event) => {
console.error('渲染进程未处理的 Promise 拒绝:', event.reason);
window.electronAPI.sendError({
type: 'renderer-unhandled-rejection',
reason: event.reason?.stack || event.reason,
});
event.preventDefault();
});
在预加载脚本中通过 contextBridge 暴露 sendError 接口:
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
sendError: (errorInfo) => ipcRenderer.send('report-error', errorInfo),
});
主进程接收错误并记录:
ipcMain.on('report-error', (event, errorInfo) => {
logToFile(`Renderer ${errorInfo.type}: ${errorInfo.message || errorInfo.reason}`);
});
利用 webContents 事件监听渲染进程崩溃
除了 JavaScript 层面的异常,渲染进程本身可能因为内存不足等原因崩溃。Electron 提供了专门的事件:
// main.js,创建窗口时
const mainWindow = new BrowserWindow({ /* ... */ });
mainWindow.webContents.on('crashed', (event) => {
console.error('渲染进程崩溃');
// 通常做法是重新加载窗口
mainWindow.loadFile('error.html'); // 或显示一个友好的崩溃界面
});
mainWindow.webContents.on('unresponsive', () => {
console.warn('渲染进程无响应');
// 可以考虑强制重载
mainWindow.webContents.forcefullyCrashRenderer();
});
mainWindow.on('unresponsive', () => {
// 备用处理
});
21.3.3 白屏处理策略
白屏是用户遇到最令人困惑的故障之一。可能的原因包括:
- 渲染进程加载 HTML 失败(路径错误、网络问题)。
- 窗口加载了
about:blank或空内容。 - 页面 JavaScript 出错导致整个应用失活。
- CSS 或 DOM 完全没渲染出来。
监听加载失败事件
在主进程中监听 did-fail-load 事件,可捕获加载阶段的错误:
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL) => {
console.error('页面加载失败:', errorDescription);
// 显示内置的错误页面
mainWindow.loadFile('error.html').catch((err) => {
// 甚至连错误页都加载失败,则显示纯文本
mainWindow.webContents.loadURL(`data:text/html,<h1>应用崩溃</h1><p>错误代码: ${errorCode}</p>`);
});
});
预置兜底错误页
在应用资源目录下放置一个 error.html,该页面不依赖任何外部脚本,仅包含基本的 HTML 和 CSS,用于告知用户重启或联系支持。
<!-- error.html -->
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>错误</title></head>
<body style="font-family: sans-serif; padding: 2rem;">
<h1>应用遇到了问题</h1>
<p>请尝试重启应用。如果问题持续,请联系支持团队。</p>
<button onclick="window.location.reload()">重新加载</button>
</body>
</html>
页面白屏自检脚本
也可以在前端代码中加入一个“心跳”检测,如果一定时间内页面主内容还未渲染,则触发重载。但需谨慎使用,避免正常加载时的误判。
// 渲染进程的入口 JavaScript
let loaded = false;
window.addEventListener('DOMContentLoaded', () => {
loaded = true;
});
setTimeout(() => {
if (!loaded) {
console.error('页面加载超时,可能是白屏');
window.electronAPI.sendError({
type: 'white-screen',
message: '页面加载超时',
});
// 由主进程决定是否重载窗口
}
}, 10000); // 10秒阈值
主进程处理白屏上报:
ipcMain.on('report-error', (event, info) => {
if (info.type === 'white-screen') {
// 可以尝试重新加载窗口
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
win.loadFile('index.html');
}
}
});
21.3.4 生产环境建议
- 日志收集:所有全局异常应不仅记录在本地,还应考虑上报到远程监控系统(如 Sentry、自建日志服务),以便分析崩溃趋势。
- 用户通知:在非致命错误时,用原生对话框(
dialog.showErrorBox)或自定义 UI 提示用户,避免静默失败。 - 自动恢复:对于渲染进程崩溃,可以尝试自动重载窗口;对于主进程崩溃,大多数方案是让用户手动重启,也可使用 Electron 的
autoUpdater类似机制触发静默重启。 - 测试覆盖:全局异常处理器本身应该通过模拟错误来测试其有效性,确保在生产环境中确实能兜底。
全局异常捕获不是“锦上添花”,而是桌面应用质量的底线。做好了它,你的应用才能从“有时候会闪退的网页壳”变成用户信赖的生产力工具。