electron + react快速搭建桌面端应用及主进程与渲染进程通信
这是一篇记录 electron + react 从零到1的项目搭建流程,命令优化,主进程与渲染进程之间的数据通信,以及常见报错问题:fs.existsSync is not funtion
前言
头两天接到一个小伙伴的请求帮助哈,他想在electron + react 实现的界面中,通过一个图片或者视频,点击下载按钮然后把资源下载到本地,下次再打开的时候读取本地的数据,这个功能在我们生活中很常见,用的最多的就是QQ, 微信这类实时通信的软件了。
这个问题本身不复杂,但是难就难在怎么去读取本地地址呢,以及存储呢?
如果对elelctron的基础没有一个认知的话,那么无疑这个问题还是有些不好处理。
所以本篇笔记就着重讲解一下关于electron + react 创建一个基础项目的流程以及注意事项。
如果你问为什么不把上面的那个问题给解决掉,那是因为已经有前人把这个问题给处理过了【前人文章】,所以不再赘述。
功能实现
找一块空地
俗话说,建房子总的先找块地吧,不然怎么创建呢,对吧。
那么找一个你喜欢或者习惯的目录去创建一个空文件夹,名称自己想一个,只要自己懂就行(正式项目建议语义化,私人的随意),毕竟以后的项目都是在这个目录下。
可以鼠标右键创建,也可以命令行创建,您高兴就好,随您的意。
具体操作太简单,不讲了。
初始化一个package.json
在上面创建的目录下打开shell命令窗口,在命令行中输入:
npm init
然后在窗口中依据提示填写相关信息即可,这样子就能够在根目录下得到一个package.json文件。
安装Electron
- 下载Electron依赖
yarn add electron
- 根目录创建 main.js 文件及具体内容
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const createWindow = () => {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
preload: path.join(__dirname, 'preload.js')
}
})
// 显示入口文件内容
mainWindow.loadFile('public/index.html')
// 打开调试模式
mainWindow.webContents.openDevTools();
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
- 根目录下package.json文件
{
...
"main": "main.js",
"scripts": {
...
}
}
- 根目录下创建 index.html 文件及具体内容
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>你好!</title>
</head>
<body>
<h1>你好!</h1>
我们正在使用 Node.js <span id="node-version"></span>,
Chromium <span id="chrome-version"></span>,
和 Electron <span id="electron-version"></span>.
</body>
</html>
- 根目录下创建 preload.js 文件及具体内容
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency])
}
})
- package.json稍作修改
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"el": "electron .",
},
- 运行命令
yarn el
- 运行效果
安装React
注意:这一步的所有操作都是在上面electron的基础上进行进一步的修改操作,没有新起项目。
-
下载 React 依赖
- 可以通过create-react-app实现
create-react-app [项目名称]
- 可以在package.json中指定使用react相关版本(本次采用)
"dependencies": { ... "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1", ... },
-
根目录创建react相关目录
只要写过vue或者react的都知道,前端页面相关文件都是在根目录src目录下,所以此处我们也不例外,同样在根目录下创建src目录,用于存放前端界面相关逻辑文件。然后再这个目录下创建界面入口文件App.jsx,具体内容如下:
import React, { useEffect } from 'react'; import { Button } from 'antd'; const App = () => { return ( <> <div>11111112224455</div> <Button>qqqq</Button> </> ); } export default App;
-
package.json稍作修改
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "el": "electron .", "start:web": "react-scripts start", },
-
运行命令
yarn start:web
-
运行效果
通过与上面electron目录运行结果对比,会发现缺少Node.js , Chromium , 和 Electron .对应的版本号,这是因为react是直接运行在浏览器端,浏览器是不能访问node相关的一部分api所导致的。
功能拆分
上面两个项目都能够正常跑起来了,但是还达不到初始目标,希望的是electron与react能够结合在一起使用,那怎么操作呢?不着急,请接着往下看。
把electron 和 react 分别用各自的目录存放,公共文件单独找个文件放。
基于以上目的,把上面的两个文件给合并抽离,形成最终文件目录,如下图:
直接这样放置,就能够跑起来吗?
简直不要太天真 O(∩_∩)O哈哈~
- 目录结构发生变化
- 并且还要把 electron 和 react 结合起来使用,还要实现react项目代码修改,桌面端应用界面也要跟着修改(想想react都有哪些特性)
- 还要实现react界面读取 / 操作系统资源功能
既然如此就继续改改,以期能够实现定下的目标。说到这里就要明白项目是怎么运行的,入口在哪里,运行命令在哪里等等。
废话不多说,请接着往下看。
package.json 文件修改
// 由原来的 main.js 改成如下,这是 electron 运行入口文件
"main": "electron/main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"el": "electron .",
"start:web": "react-scripts start"
},
electron - main.js 文件修改
...
const createWindow = () => {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
preload: path.join(__dirname, 'preload.js')
}
})
/*
以前项目运行的仅仅是一个固定的html文件,现在要结合react,所以此处加载的是一个运行地址;
此处可以通过环境去配置具体加载什么页面;
加载这个地址,必须要让这个地址的项目运行起来,否则桌面端会出现白屏。
*/
// mainWindow.loadFile('public/index.html') 【废弃不用】
mainWindow.loadURL('http://localhost:3000/')
// 打开调试控制台
mainWindow.webContents.openDevTools();
}
...
react 运行根目录 index.html 文件修改
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>你好!</title>
</head>
<body>
<h1>你好!</h1>
我们正在使用 Node.js <span id="node-version"></span>,
Chromium <span id="chrome-version"></span>,
和 Electron <span id="electron-version"></span>.
<!-- 添加 react 挂载根节点 -->
<div id="root"></div>
</body>
</html>
接下来就看看修改后的运行效果
能够看到浏览器端和桌面端基本一致,至于蓝色圆圈圈起来部分的版本号在浏览器没有显示出来,则是因为浏览器不支持NodeJs的一些api,至于原因这大概率是处于浏览器安全机制的考虑,具体原因可以自行查阅相关文档,这里就不再详述。
命令一把梭
通过以上方式基本就实现了一个 Electron + React 的桌面端项目,但是就这个运行方式感觉还是不太安逸,非常的麻烦,具体问题如下:
- 需要跑两个命令
- 关掉 electron 窗口后,端口仍被占有的情况
- 需要 3000 端口跑起来了再刷新一下 electron 才会有内容
- 浏览器会打开一个 3000 端口的 tab 页, electron 会弹出加载了3000端口内容的窗口,理想状态下只需要保留 electron 中的窗口就好了
根据上面4点问题,也不是没有办法解决,一把梭打开应用完全没有问题。
请看具体实现方式:
- 下载 npm 库 concurrently,这个主要是解决上面第一和第二个问题,同时同步,一次可以完美的运行多个命令。
- 下载 npm 库 wait-on 字面意思,就是等待什么执行完之后再执行什么命令,解决我们的第三个问题。
- 下载 npm 库 cross-env, 这个大家应该是很熟悉的,经常有用到的。主要解决一些环境变量的问题,但是这次我们使用它是为了利用它的 BROWSER=none 属性来解决上面提到的第四个问题,不打开浏览器中的 tab 页。
综上所述,最后修改package.json文件的运行命令。
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"el": "electron .",
"start:web": "react-scripts start",
"start": "concurrently \"(cross-env BROWSER=none react-scripts start)\" \"(wait-on http://localhost:3000/ && electron .)\""
},
主进程与渲染进程通信
最后,说一下主进程与渲染进程之间的通信问题,有人可能要问,啥是主进程,啥是渲染进程?
这里我就说一下我个人的理解,如有不正确,请评论指正。
主进程:electron 下main.js运行的这个文件,主要管理桌面端运行相关的事件,例如:
- 创建桌面端窗口;
- 创建弹框窗口;
- 读取本地系统文件等信息
- 加载资源等
- 运行shell打开第三方软件
- 放入托盘等相关事件
- 其他
综上所述,全是与运行平台系统相关的操作,平台例如:windows, OSX, linux。
渲染进程:主要就是界面中能够看到的界面。例如我们这个demo中src目录下的App.js 和 public目录下的index.html。
既然搞明白了主进程与渲染进程,那么我们再将代码给修改修改。
主进程 - main.js
const createWindow = () => {
const mainWindow = new BrowserWindow({
...
webPreferences: {
nodeIntegration: true, // 此处需要打开,否则调用node相关api会报错
...
}
})
}
// 这是一个订阅与发布模式
ipcMain.on('sendMsg', (e, params) => {
// 接收数据
console.log('这是我接收到的数据::',e, params)
console.log('可以获取项目文件地址::', app.getAppPath())
})
渲染进程 - App.jsx(App.jsx属于渲染进程,但是不等于渲染进程,这是一个从属关系)
import React, { useEffect } from 'react';
import { Button } from 'antd';
// 此处需要特别注意,很重要
const {ipcRenderer} = window.require('electron')
const App = () => {
return (
<>
<div>11111112224455</div>
<Button onClick={() => {
// 发送数据
ipcRenderer.send('sendMsg', {a:10})
}}>qqqq</Button>
</>
);
}
export default App;
如上方式就实现了主进程与渲染进程之间的数据通信,接下来请看运行效果:
注意:在主进程打印输出的文件,不会在桌面端控制台输出,只会在命令行窗口中打印输出。
关于渲染进程中使用electron相关api,这里特别说明一下,如果不熟悉这块,很容易踩坑。
报错 fs.existsSync is not a function
报错原因:
因为webpack默认产出目标是web平台的 js,其混淆了nodejs的标准模块系统,导致引入nodejs的模块时出现问题。
解决方式:
即通过使用window.require代替require来引入electron,因为前者不会被webpack编译,在渲染进程require关键字就是表示node模块的系统;
在浏览器环境中使用 nodejs api
electron将nodejs api挂载在了window对象上,来与底层进行通信,所以需要调用window上的require函数来引入 nodejs 包。
const electron = window.require('electron')
const process = window.require('process')
const fs = window.require('fs')
const Https = window.require('https')
在浏览器环境中使用app对象
在electron主进程中使用app对象直接require electron就行了
const { app } = require('electron')
在渲染进程中使用app对象则需要这样引入:
const electron = window.require('electron');
const app = electron.remote.app;
更多推荐
所有评论(0)