人人都会AI编程

21.5 进程守护、崩溃自恢复、异常降级方案

更新时间:2026-07-11

桌面应用与 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.onunhandledrejectionwindow.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 综合架构建议

一个完备的进程守护与异常恢复方案,应当整合多个层次:

  1. 渲染进程内部:错误边界 + 全局异常捕获,防止单个组件错误导致白屏。
  2. 渲染进程崩溃:主进程监听 render-process-gone,自动销毁并重建窗口。
  3. 主进程崩溃:外部守护脚本或 PM2 监听并自动重启。
  4. 长期运行状态监控:主进程定时向渲染进程发送心跳,若渲染进程无响应则主动强制重载(webContents.forcefullyCrashRenderer()reload())。
  5. 日志与上报:所有异常信息通过主进程收集,写入本地日志文件或上报到远程监控平台,方便后续排查。

通过这套分层的防护体系,你的 Electron 应用就具备了接近原生软件的稳定性表现——用户几乎感知不到崩溃的存在,即便出现极端情况,也能在几秒内自动恢复到可用状态,而不是无助地看到应用直接闪退。