概要

作为前端开发者,我们早已熟悉 React+TS 开发 Web 应用的流程,但如果需要将前端页面转化为可安装的桌面应用,Electron 无疑是最佳选择 —— 它让我们无需学习 C++/C# 等桌面开发语言,仅凭前端技术栈就能开发跨平台桌面应用。本文作为系列上篇,将从 Electron 核心原理讲起,一步步拆解如何将 Electron 与 React+TS 项目深度融合,掌握主进程、渲染进程、预加载脚本的核心通信逻辑。

一、Electron 核心认知:是什么?为什么值得学?

1. Electron 定义:前端开发桌面应用的 “桥梁”

Electron 是由 GitHub 开发的跨平台桌面应用开发框架,核心是将「Chromium 浏览器内核 + Node.js 运行时」打包成可执行文件:

  • 前端层:你可以用 React/Vue/TS/JS 等熟悉的前端技术开发 UI 界面(和写网页完全一致);
  • 系统层:内置 Node.js 运行时,可直接调用文件、打印机、系统对话框等桌面原生能力;
  • 跨平台:一套代码可打包为 Windows(.exe)、macOS(.dmg)、Linux(.deb)应用,无需针对不同系统单独开发。

2. Electron 核心特性(前端开发者的 “福利”)

特性核心价值
技术栈复用直接复用 React+TS 技术栈,无需学习新语言
原生系统能力调用文件系统、打印机、剪贴板、系统托盘等 Web 应用无法访问的原生 API
跨平台兼容一套代码适配 Windows/macOS/Linux,降低多端开发成本
完整 Web 生态支持 HTML/CSS/JS/TS 所有特性,可直接复用 npm 生态中的前端库
调试体验友好内置 Chromium 调试工具,保留前端开发者熟悉的断点、控制台调试方式

3. Electron 核心架构:双进程模型(理解通信的关键)

Electron 的核心是「主进程(Main Process)」和「渲染进程(Renderer Process)」的分工协作,这也是理解 Electron 与 React 通信的基础:

(1)主进程(Main Process)
  • 定位:整个应用的 “大脑”,应用启动时唯一创建的进程;
  • 核心职责:管理应用生命周期(启动 / 关闭 / 窗口创建)、调用系统原生 API(文件、打印机、窗口)、创建渲染进程;
  • 运行环境:Node.js 环境,拥有完全的系统操作权限;
  • 核心文件:通常是 main.js/main.ts,作为 Electron 应用的入口文件。
(2)渲染进程(Renderer Process)
  • 定位:应用的 “界面层”,就是我们写的 React+TS 前端页面;
  • 核心职责:渲染 UI、处理用户交互(点击 / 输入)、执行前端业务逻辑;
  • 运行环境:Chromium 浏览器环境,默认权限受限(无法直接调用系统 API);
  • 数量:一个应用可以有多个渲染进程(比如多窗口),每个窗口对应一个渲染进程。
(3)进程通信(IPC):主进程与渲染进程的 “对话方式”

主进程和渲染进程运行在不同环境,无法直接共享变量 / 调用函数,必须通过 Electron 提供的 IPC(Inter-Process Communication)机制通信:

  • 渲染进程 → 主进程:渲染进程(React)发起请求(比如 “读取本地文件”),主进程处理后返回结果;
  • 主进程 → 渲染进程:主进程主动推送消息(比如 “系统托盘被点击”),渲染进程响应;
  • 核心原则:系统级操作(如文件读写、打印)必须放在主进程执行,渲染进程仅负责交互和展示。

二、Electron 与 React+TS 项目融合:从搭建到通信

1. 项目基础搭建

(1)初始化 React+TS 项目

首先用 Create React App 初始化一个标准的 React+TS 项目(也可使用 Vite,核心逻辑一致):

# 创建 React+TS 项目
npx create-react-app electron-react-ts --template typescript
cd electron-react-ts
# 安装 Electron 依赖(开发依赖)
npm install electron --save-dev
# 安装 cross-env 区分开发/生产环境
npm install cross-env --save-dev
# 安装 concurrently 同时启动 React 和 Electron
npm install concurrently --save-dev
# 安装 wait-on 等待 React 服务启动后再启动 Electron
npm install wait-on --save-dev

这里大概率会出现安装缓慢,或者显示安装完成但其实由于网络波动,无法正常使用的情况。
安装失败,可以创建 .npmrc 文件设置镜像

registry=https://registry.npmmirror.com
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/

另外不建议使用 pnpm 安装 electron 相关依赖,实际使用中发现,pnpm 就算使用镜像也会出现安装不完整的情况,最最最关键是他并不会报错,npm 不会出现这种情况

(2)调整 package.json 配置

修改 package.json,添加 Electron 启动 / 打包脚本,并配置应用基本信息:

{
  "name": "electron-react-ts",
  "version": "0.1.0",
  "main": "src/electron/main.ts", // 指定 Electron 主进程入口
  "homepage": "./", // 打包 React 时适配 Electron 路径
  "scripts": {
    "start:react": "react-scripts start", // 启动 React 开发服务
    "start:electron": "cross-env NODE_ENV=development electron .", // 启动 Electron
    "dev": "concurrently \"npm run start:react\" \"wait-on http://localhost:3000 && npm run start:electron\"", // 同时启动
    "build:react": "react-scripts build", // 打包 React
    "build:electron": "electron-builder", // 打包 Electron(需安装 electron-builder)
    "build": "npm run build:react && npm run build:electron"
  },
  "build": { // electron-builder 打包配置
    "appId": "com.electron.react.ts.demo",
    "productName": "ElectronReactTSDemo",
    "directories": {
      "output": "dist"
    },
    "files": [
      "build/**/*",
      "src/electron/**/*"
    ],
    "win": {
      "target": "nsis"
    },
    "mac": {
      "target": "dmg"
    }
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "electron": "^36.5.0",
    "electron-builder": "^26.0.12",
    "@types/electron": "^1.6.10",
    "concurrently": "^8.2.2",
    "cross-env": "^7.0.3",
    "wait-on": "^7.0.1"
  }
}

2. 主进程开发:src/electron/main.ts

主进程是 Electron 应用的入口,负责创建窗口、管理应用生命周期、注册 IPC 通信事件。我们创建 src/electron/main.ts 文件,编写核心逻辑:

import { app, BrowserWindow, ipcMain, dialog } from 'electron';
import path from 'path';

// 声明主窗口变量,避免被垃圾回收
let mainWindow: BrowserWindow | null = null;

/**
 * 创建 Electron 主窗口(核心优化:增加异常捕获、动态安全配置)
 */
const createMainWindow = () => {
  try {
    // 区分开发/生产环境(统一环境变量判断)
    const isDev = process.env.NODE_ENV === 'development';

    // 1. 创建浏览器窗口
    mainWindow = new BrowserWindow({
      width: 1200,
      height: 800,
      minWidth: 800,
      minHeight: 600,
      title: 'Electron+React+TS 示例',
      webPreferences: {
        nodeIntegration: false, // 永远关闭(核心安全原则)
        contextIsolation: true, // 永远开启(隔离上下文)
        sandbox: !isDev, // 🔥 核心优化:开发关闭沙箱,生产开启沙箱
        // 🔥 优化:使用 path.resolve 确保预加载脚本路径绝对化
        preload: path.resolve(__dirname, './preload.ts'),
        // 可选:生产环境禁用远程模块(Electron 14+ 已默认移除)
        enableRemoteModule: false,
      },
    });

    // 2. 加载 React 页面(🔥 优化:路径拼接更鲁棒)
    if (isDev) {
      // 开发环境:加载 React 本地开发服务
      mainWindow.loadURL('http://localhost:3000');
      // 仅开发环境打开调试工具
      mainWindow.webContents.openDevTools({ mode: 'detach' });
    } else {
      // 生产环境:使用 app.getAppPath() 适配打包后路径,避免层级依赖
      const buildPath = path.join(app.getAppPath(), 'build', 'index.html');
      mainWindow.loadFile(buildPath);
      // 生产环境禁用调试工具
      mainWindow.webContents.setDevToolsWebContents(null);
    }

    // 3. 窗口关闭事件:释放主窗口引用
    mainWindow.on('closed', () => {
      mainWindow = null;
    });

    // 4. 注册 IPC 通信事件(🔥 优化:传递 mainWindow 解耦)
    registerIpcHandlers(mainWindow);

    // 可选:捕获窗口加载异常
    mainWindow.webContents.on('did-fail-load', (_, errorCode, errorDesc) => {
      dialog.showErrorBox('页面加载失败', `错误码:${errorCode},原因:${errorDesc}`);
    });

  } catch (error) {
    // 🔥 核心优化:捕获窗口创建异常,避免应用崩溃
    console.error('创建主窗口失败:', error);
    dialog.showErrorBox('启动失败', `应用窗口创建失败:${(error as Error).message}`);
    app.quit();
  }
};

/**
 * 注册 IPC 通信处理器(🔥 优化:接收 mainWindow 参数,解耦全局变量)
 * @param window 主窗口实例
 */
const registerIpcHandlers = (window: BrowserWindow | null) => {
  // 示例1:监听渲染进程的“获取应用信息”请求
  ipcMain.handle('app:get-info', () => {
    return {
      name: app.getName(),
      version: app.getVersion(),
      platform: process.platform, // win32/macos/linux
      isDev: process.env.NODE_ENV === 'development',
    };
  });

  // 示例2:监听渲染进程的“选择本地文件”请求(🔥 优化:使用入参 window,而非全局变量)
  ipcMain.handle('file:select', async () => {
    if (!window) {
      dialog.showErrorBox('错误', '主窗口未初始化,无法选择文件');
      return null;
    }

    try {
      const result = await window.showOpenDialog({
        title: '选择文件',
        filters: [
          { name: '文本文件', extensions: ['txt', 'md'] },
          { name: '所有文件', extensions: ['*'] },
        ],
        properties: ['openFile'],
        // 可选:生产环境限制文件选择路径
        defaultPath: isDev ? undefined : app.getPath('documents'),
      });

      return result.canceled ? null : result.filePaths[0];
    } catch (error) {
      console.error('选择文件失败:', error);
      dialog.showErrorBox('选择文件失败', (error as Error).message);
      return null;
    }
  });

  // 可选:添加 IPC 处理器清理逻辑(避免内存泄漏)
  app.on('will-quit', () => {
    ipcMain.removeHandler('app:get-info');
    ipcMain.removeHandler('file:select');
  });
};

/**
 * 应用生命周期管理(🔥 优化:完善多开处理、异常兜底)
 */
// 防止应用多开(完善逻辑:多开时激活已有窗口)
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
  app.quit();
} else {
  app.on('second-instance', () => {
    // 当第二个实例启动时,激活已有主窗口
    if (mainWindow) {
      if (mainWindow.isMinimized()) mainWindow.restore();
      mainWindow.focus();
    }
  });

  // 应用就绪后创建主窗口
  app.whenReady().then(createMainWindow);

  // 所有窗口关闭时退出应用(macOS 除外)
  app.on('window-all-closed', () => {
    // 可选:macOS 下隐藏 Dock 图标(增强体验)
    if (process.platform === 'darwin') {
      app.dock.hide();
    }
    if (process.platform !== 'darwin') {
      app.quit();
    }
  });

  // macOS 点击 Dock 图标时重新创建窗口
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createMainWindow();
    } else {
      // 激活已有窗口
      BrowserWindow.getAllWindows()[0].focus();
    }
  });

  // 捕获应用未处理异常,避免崩溃
  process.on('uncaughtException', (error) => {
    console.error('未处理异常:', error);
    dialog.showErrorBox('应用异常', `发生未处理错误:${error.message}`);
    app.quit();
  });
}

// 可选:生产环境禁用硬件加速(解决部分显卡兼容问题)
if (process.env.NODE_ENV !== 'development') {
  app.disableHardwareAcceleration();
}

3. 预加载脚本:src/electron/preload.ts(核心桥梁)

预加载脚本(preload)是 Electron 中最容易被忽视但最重要的文件 —— 它运行在渲染进程中,但拥有访问 Node.js API 和 IPC 通信的能力,同时受上下文隔离保护,是连接主进程和 React 渲染进程的安全桥梁。

(1)preload 的核心作用
  • 安全暴露主进程能力:避免渲染进程直接访问 ipcRenderer,仅暴露业务所需的 API;
  • 隔离上下文:防止渲染进程中的恶意代码访问主进程变量;
  • 统一通信入口:集中管理主进程和渲染进程的通信事件,便于维护。
(2)编写 preload.ts
// src/electron/preload.ts
import { contextBridge, ipcRenderer } from 'electron';

/**
 * 定义暴露给渲染进程的 API 类型(TS 类型约束)
 */
interface ElectronAPI {
  // 获取应用信息
  getAppInfo: () => Promise<{
    name: string;
    version: string;
    platform: string;
  }>;
  // 选择本地文件
  selectFile: () => Promise<string | null>;
  // 示例:主进程主动向渲染进程发送消息的回调
  onMainMessage: (callback: (message: string) => void) => void;
}

/**
 * 核心:通过 contextBridge 暴露安全的 API 给渲染进程
 * 注意:仅暴露必要的 API,不要暴露整个 ipcRenderer!
 */
contextBridge.exposeInMainWorld(
  'electronAPI', // 全局变量名,React 中可通过 window.electronAPI 访问
  {
    // 获取应用信息(调用主进程的 app:get-info 事件)
    getAppInfo: () => ipcRenderer.invoke('app:get-info'),
    // 选择本地文件(调用主进程的 file:select 事件)
    selectFile: () => ipcRenderer.invoke('file:select'),
    // 监听主进程主动发送的消息
    onMainMessage: (callback) => {
      // 注册监听,回调函数接收主进程消息
      ipcRenderer.on('main:send-message', (_, message) => {
        callback(message);
      });
      // 返回取消监听的方法(避免内存泄漏)
      return () => ipcRenderer.off('main:send-message', callback);
    },
  } as ElectronAPI
);

/**
 * 扩展 Window 类型(TS 类型提示)
 * 让 React 中访问 window.electronAPI 时有类型提示
 */
declare global {
  interface Window {
    electronAPI: ElectronAPI;
  }
}

4. React+TS 与 Electron 通信:渲染进程开发

完成主进程和预加载脚本的开发后,我们在 React 组件中调用暴露的 electronAPI,实现前端界面与 Electron 原生能力的交互。

(1)类型声明补充(可选,增强 TS 提示)

在 src/types/electron.d.ts 中补充全局类型,让 TS 识别 window.electronAPI:

// src/types/electron.d.ts
declare global {
  interface Window {
    electronAPI: {
      getAppInfo: () => Promise<{
        name: string;
        version: string;
        platform: string;
      }>;
      selectFile: () => Promise<string | null>;
      onMainMessage: (callback: (message: string) => void) => void;
    };
  }
}

export {};
(2)React 组件中调用 Electron API

创建 src/components/ElectronDemo.tsx,实现与主进程的通信:

// src/components/ElectronDemo.tsx
import { useState, useEffect, useCallback } from 'react';

const ElectronDemo = () => {
  // 应用信息状态
  const [appInfo, setAppInfo] = useState<{
    name: string;
    version: string;
    platform: string;
  } | null>(null);
  // 选中的文件路径
  const [selectedFile, setSelectedFile] = useState<string | null>(null);
  // 主进程推送的消息
  const [mainMessage, setMainMessage] = useState<string>('');

  /**
   * 调用 Electron API 获取应用信息
   */
  const fetchAppInfo = useCallback(async () => {
    try {
      // 访问预加载脚本暴露的 electronAPI
      const info = await window.electronAPI.getAppInfo();
      setAppInfo(info);
    } catch (error) {
      console.error('获取应用信息失败:', error);
    }
  }, []);

  /**
   * 调用 Electron API 选择本地文件
   */
  const handleSelectFile = useCallback(async () => {
    try {
      const filePath = await window.electronAPI.selectFile();
      setSelectedFile(filePath);
    } catch (error) {
      console.error('选择文件失败:', error);
    }
  }, []);

  /**
   * 监听主进程主动推送的消息
   */
  useEffect(() => {
    // 注册主进程消息监听
    const offListener = window.electronAPI.onMainMessage((message) => {
      setMainMessage(message);
    });

    // 组件卸载时取消监听(避免内存泄漏)
    return () => {
      offListener();
    };
  }, []);

  return (
    <div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
      <h2>Electron + React + TS 通信示例</h2>

      {/* 1. 获取应用信息 */}
      <div style={{ margin: '20px 0' }}>
        <button
          onClick={fetchAppInfo}
          style={{ padding: '8px 16px', cursor: 'pointer' }}
        >
          获取应用信息
        </button>
        {appInfo && (
          <div style={{ marginTop: '10px', padding: '10px', border: '1px solid #eee' }}>
            <p>应用名称:{appInfo.name}</p>
            <p>应用版本:{appInfo.version}</p>
            <p>运行平台:{appInfo.platform}</p>
          </div>
        )}
      </div>

      {/* 2. 选择本地文件 */}
      <div style={{ margin: '20px 0' }}>
        <button
          onClick={handleSelectFile}
          style={{ padding: '8px 16px', cursor: 'pointer' }}
        >
          选择本地文件
        </button>
        {selectedFile && (
          <p style={{ marginTop: '10px' }}>选中的文件:{selectedFile}</p>
        )}
      </div>

      {/* 3. 监听主进程消息 */}
      <div style={{ margin: '20px 0', padding: '10px', border: '1px solid #eee' }}>
        <p>主进程推送的消息:{mainMessage || '暂无消息'}</p>
      </div>
    </div>
  );
};

export default ElectronDemo;
(3)在 App.tsx 中引入组件
// src/App.tsx
import './App.css';
import ElectronDemo from './components/ElectronDemo';

function App() {
  return (
    <div className="App">
      <ElectronDemo />
    </div>
  );
}

export default App;

5. 核心通信逻辑总结

Electron 与 React+TS 的通信本质是 “主进程 ↔ 预加载脚本 ↔ 渲染进程” 的三层交互,核心规则如下:

(1)渲染进程 → 主进程(主动请求)
React 组件 → window.electronAPI.xxx() →
preload.ts 中的 ipcRenderer.invoke('事件名') →
主进程 ipcMain.handle('事件名', 处理函数) →
返回结果 → React 组件
  • 适用场景:React 主动请求系统能力(如选择文件、获取系统信息);
  • 核心 API:ipcRenderer.invoke()(渲染进程)、ipcMain.handle()(主进程)。
(2)主进程 → 渲染进程(主动推送)
主进程 win.webContents.send('事件名', 数据) →
preload.ts 中的 ipcRenderer.on('事件名', 回调) →
暴露给 window.electronAPI → React 组件监听并更新 UI
  • 适用场景:主进程主动通知渲染进程(如系统托盘点击、后台任务完成);
  • 核心 API:webContents.send()(主进程)、ipcRenderer.on()(预加载脚本)。
(3)关键安全原则
  • 永远关闭 nodeIntegration: true,开启 contextIsolation: true;
  • 预加载脚本仅暴露必要 API,不直接暴露 ipcRenderer;
  • 组件卸载时取消 IPC 监听,避免内存泄漏。

6. 运行与调试

完成以上代码编写后,执行以下命令启动应用:

npm run dev

此时会同时启动 React 开发服务(http://localhost:3000)和 Electron 窗口,你可以:

  • 点击 “获取应用信息”:React 调用 Electron API 获取应用名称、版本、系统平台;
  • 点击 “选择本地文件”:Electron 弹出系统文件选择对话框,返回选中的文件路径;
  • (可选)在主进程中添加主动推送消息的逻辑,测试主进程→渲染进程通信:
    // 在 main.ts 的 createMainWindow 中添加
    setTimeout(() => {
      if (mainWindow) {
        mainWindow.webContents.send('main:send-message', '这是主进程主动推送的消息');
      }
    }, 3000);
    

小结

1. 核心知识点回顾

  • Electron 核心架构:双进程模型(主进程管系统能力,渲染进程管 React UI),IPC 是进程通信的基础;
  • 主进程作用:应用入口,负责创建窗口、管理生命周期、注册 IPC 事件、调用系统原生 API;
  • preload 核心价值:安全的通信桥梁,隔离上下文,仅暴露必要 API 给 React;
  • 通信逻辑:渲染进程通过 preload 暴露的 API 调用主进程能力,主进程通过 webContents.send 主动推送消息。

2. 下篇预告

本文重点讲解了 Electron 核心原理与 React+TS 项目的融合,掌握了基础的进程通信逻辑。在下篇中,我们将基于这套架构实现 “静默打印” 实战 —— 利用 Electron 主进程的打印能力,实现无弹窗、自动打印的功能,覆盖小票打印、报表打印等实际业务场景,同时解决跨平台兼容、打印格式适配等问题。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐