Electron+React框架搭建以及基础使用

前言

大家好,我是JS-Man

这篇文章将教小伙伴们搭建一个较完善的Electron+React的框架。

脚手架结构清晰功能健全,打包工具使用的是官方推荐的Electron Forge

本文目录分为三大块内容:

  1. Electron的基础使用和原理。
  2. 使用Electron Forge搭建脚手架。
  3. 项目实战

如果使用过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的弊端。

相关推荐
布列瑟农的星空15 分钟前
大话设计模式——关注点分离原则下的事件处理
前端·后端·架构
yvvvy34 分钟前
前端必懂的 Cache 缓存机制详解
前端
北海几经夏1 小时前
React自定义Hook
前端·react.js
龙在天1 小时前
从代码到屏幕,浏览器渲染网页做了什么❓
前端
TimelessHaze1 小时前
【performance面试考点】让面试官眼前一亮的performance性能优化
前端·性能优化·trae
yes or ok1 小时前
前端工程师面试题-vue
前端·javascript·vue.js
我要成为前端高手1 小时前
给不支持摇树的三方库(phaser) tree-shake?
前端·javascript
Noxi_lumors2 小时前
VITE BALABALA require balabla not supported
前端·vite
周胜22 小时前
node-sass
前端
aloha_2 小时前
Windows 系统中,杀死占用某个端口(如 8080)的进程
前端