前言
大家好,我是JS-Man
这篇文章将教小伙伴们搭建一个较完善的Electron+React的框架。
脚手架结构清晰功能健全,打包工具使用的是官方推荐的Electron Forge。
本文目录分为三大块内容:
- Electron的基础使用和原理。
- 使用Electron Forge搭建脚手架。
- 项目实战
如果使用过Electron的,可以跳过第一部分。
1. Electron基础
来自官网的介绍:

Electron将浏览器内核和Node.js打包进了客户端里,所以说对于会三件套的前端来说,上手Electron是0成本的,基础教程的目录结构如下。

下面开始我们的基础教程:
1.1 主进程
npm init
后安装下electronnpm install electron --save-dev
在package.json 中指定main文件 是 Electron 应用的入口。 这个文件控制主程序 (main process) ,它运行在 Node.js 环境里,负责控制应用的生命周期、显示原生界面、执行特殊操作并管理渲染器进程 (renderer processes)。
此时package.json如下:
js
{
"name": "my-electron-app",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"start": "electron .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"electron": "^35.1.5"
}
}
electron .
这个命令会告诉 Electron 在当前目录下寻找主脚本,并以开发模式运行它。
1.2 网页加载BrowserWindow
在Electron中,每个窗口展示一个页面,可以是本地HTML也可以是远程URL,本地文件的场景居多,在根目录创建一个index.html
文件:
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<meta
http-equiv="X-Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<title>来自 Electron 渲染器的问好!</title>
</head>
<body>
<h1>来自 Electron 渲染器的问好!</h1>
<p>👋</p>
<p id="info"></p>
</body>
</html>
有了一个网页,可以将其加载到Electron的BrowserWindow。 修改main.js为:
js
// app模块控制着您应用程序的事件生命周期。
// BrowserWindow这个模块创建和管理 app 的窗口。
// Electron 28 起,Electron 支持 ECMAScript 模块
const { app, BrowserWindow } = require('electron')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600
})
win.loadFile('index.html')
}
// 在 Electron 中,只有在 app 模块的ready事件(event)触发后才能创建 BrowserWindows 实例
app.whenReady().then(() => {
createWindow()
})
此时执行npm run start
已经可以启动客户端了,每个页面都在独立的进程中运行。
通过 Node.js 的 process.platform
变量,可以针对特定平台运行特定代码,此文不作讨论。
1.3 预加载脚本

也就是说,预加载脚本是主进程和渲染进程通信的桥梁。
从 Electron 20 开始,预加载脚本默认沙盒化 ,不再拥有完整 Node.js 环境的访问权,预加载脚本在渲染器加载网页之前注入。
为了演示这一概念,将会创建一个将应用中的 Chrome、Node、Electron 版本号暴露至渲染器的预加载脚本:
新建一个 preload.js
文件。该脚本通过 versions
这一全局变量,将 Electron 的 process.versions
对象暴露给渲染器。
js
// preload.js
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron
// 除函数之外,我们也可以暴露变量
})
为了将脚本附在渲染进程上,在 BrowserWindow 构造器中使用 webPreferences.preload
传入脚本的路径。
js
// main.js
const { app, BrowserWindow } = require('electron')
const path = require('node:path')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
})
此时,渲染器就能全局访问versions
了,创建一个renderer.js
:
js
// renderer.js
const information = document.getElementById('info')
information.innerText = `本应用正在使用 Chrome (v${versions.chrome()}), Node.js (v${versions.node()}), 和 Electron (v${versions.electron()})`
在index.html
里加载:
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<meta
http-equiv="X-Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<title>来自 Electron 渲染器的问好!</title>
</head>
<body>
<h1>来自 Electron 渲染器的问好!</h1>
<p>👋</p>
<p id="info"></p>
</body>
<script src="./renderer.js"></script>
</html>
此时客户端界面如下:

1.4 进程间通信
Electron 的主进程和渲染进程有着清楚的分工并且不可互换。 这代表着无论是从渲染进程直接访问 Node.js 接口,亦或者是从主进程访问 HTML 文档对象模型 (DOM),都是不可能的。
解决这一问题的方法是使用进程间通信 (IPC)。可以使用 Electron 的 ipcMain
模块和 ipcRenderer
模块来进行进程间通信。
为了从你的网页向主进程发送消息,你可以使用 ipcMain.handle
设置一个主进程处理程序(handler),然后在预处理脚本中暴露一个被称为 ipcRenderer.invoke
的函数来触发该处理程序(handler)。
首先,在预处理脚本中设置 invoke
调用:
js
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
ping: () => ipcRenderer.invoke('ping')
// 除函数之外,我们也可以暴露变量
})
然后在主进程中处理监听器:
js
// main.js
const { app, BrowserWindow, ipcMain } = require('electron/main')
const path = require('node:path')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.handle('ping', () => 'pong')
createWindow()
})
发送器与接收器设置完成之后,渲染进程就可以和主进程通信了:
js
// renderer.js
const func = async () => {
const response = await window.versions.ping()
console.log(response) // 打印 'pong'
}
func()
客户端里打印如下:

2 搭建脚手架
2.1 Electron Forge
直接使用Electron Forge官方推荐的脚手架,官方有四个模版:

这里我们使用webpack-typescript
,直接执行npx create-electron-app@latest my-app --template=webpack-typescript,目录结构如下:

关于webpack的配置可以先不看,重点关注下forge.config.ts这个文件,里面指定了preload和html的入口文件,和第一部分章节里不一样的是,renderer也帮我们指定了。
2.2 集成React
我们手动安装下React相关依赖:npm i react@18 react-dom@18 react-router-dom@5 @types/react-router-dom@5
。
接着增加app.tsx
,渲染器使用react加载,app.tsx
代码如下:
js
// app.tsx
import { createRoot } from 'react-dom/client'
import React from 'react'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
import ComponentLoader from './components/ComponentLoader'
import RobotGrab from './pages/robot-grab'
import NotFound from './components/NotFound'
const App: React.FC = () => {
return (
<Router>
<React.Suspense fallback={<ComponentLoader />}>
<Switch>
<Route path="/" component={RobotGrab} />
<Route path="/robot-grab" component={RobotGrab} />
<Route path="*" component={NotFound} />
</Switch>
</React.Suspense>
</Router>
)
}
const root = createRoot(document.getElementById('root'))
root.render(<App />)
修改index.html
如下:
js
// index.html
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Electron</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
同时,renderer.ts
也需要加载app.tsx
:
js
// Add this to the end of the existing file
import './app'
现在就可以使用react进行渲染了,完整的项目结构如下:

3.项目实战
下面进入一个实际的业务场景,一个自动化脚本的需求,脚本使用了Playwright,集成在了Electron里。
首先使用React画页面,添加一个按钮
js
// src/pages/grab/index.tsx
const handleAction = () => {
window.robotGrab
.launch()
.then((result: unknown) => {
const res = result as { success?: boolean; message?: string }
if (res && res.success) {
setMessage(res.message || '成功连接到Chrome浏览器')
setBrowserStatus('ready')
} else {
setMessage(res?.message || '连接失败,请检查Chrome是否已启动并开启远程调试')
setBrowserStatus(null)
}
})
.catch((error: Error) => {
console.error('启动失败', error)
setMessage(`启动失败: ${error.message || '连接Chrome失败'}`)
setBrowserStatus(null)
})
}
<button onClick={() => handleAction()}>启动浏览器</button>
调用的launch就是preload.ts注入进window的:
js
// src/preload.ts
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('robotGrab', {
launch: () => ipcRenderer.invoke('launch'),
})
接着在主进程里处理:
js
// src/index.ts
// 处理启动脚本
ipcMain.handle('launch', async () => {
try {
console.log(`尝试启动脚本,调试端口: ${debugPort}`)
await openOrLoginScript(debugPort)
return { success: true, message: '成功连接到Chrome并打开页面' }
} catch (error) {
console.error('启动失败:', error)
return {
success: false,
message: `启动失败: ${error.message || '未知错误'}`,
error: error.toString(),
}
}
})
实际的脚本执行放在scripts文件夹里,这里就不粘贴脚本代码了。
总结
看完这篇文章的小伙伴,对Electron肯定能够上手使用了,具体的业务代码没有在本文呈现,也没有必要,如果大家对Electron比较感兴趣,那我会出一篇Electron原理篇,深入剖析下主进程、渲染进程和Electron的弊端。