【Electron+React+TS 实战】:从核心原理到项目融合(上)
文章目录
概要
作为前端开发者,我们早已熟悉 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 主进程的打印能力,实现无弹窗、自动打印的功能,覆盖小票打印、报表打印等实际业务场景,同时解决跨平台兼容、打印格式适配等问题。
更多推荐
所有评论(0)