在这里插入图片描述

前言

头两天接到一个小伙伴的请求帮助哈,他想在electron + react 实现的界面中,通过一个图片或者视频,点击下载按钮然后把资源下载到本地,下次再打开的时候读取本地的数据,这个功能在我们生活中很常见,用的最多的就是QQ, 微信这类实时通信的软件了。

这个问题本身不复杂,但是难就难在怎么去读取本地地址呢,以及存储呢?

如果对elelctron的基础没有一个认知的话,那么无疑这个问题还是有些不好处理。

所以本篇笔记就着重讲解一下关于electron + react 创建一个基础项目的流程以及注意事项。

如果你问为什么不把上面的那个问题给解决掉,那是因为已经有前人把这个问题给处理过了【前人文章】,所以不再赘述。

功能实现

找一块空地

俗话说,建房子总的先找块地吧,不然怎么创建呢,对吧。

那么找一个你喜欢或者习惯的目录去创建一个空文件夹,名称自己想一个,只要自己懂就行(正式项目建议语义化,私人的随意),毕竟以后的项目都是在这个目录下。

可以鼠标右键创建,也可以命令行创建,您高兴就好,随您的意。

具体操作太简单,不讲了。

初始化一个package.json

在上面创建的目录下打开shell命令窗口,在命令行中输入:

npm init

然后在窗口中依据提示填写相关信息即可,这样子就能够在根目录下得到一个package.json文件。

安装Electron

  1. 下载Electron依赖
yarn add electron
  1. 根目录创建 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()
})

  1. 根目录下package.json文件
{
	...
	"main": "main.js",
	"scripts": {
		...
	}
}
  1. 根目录下创建 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>
  1. 根目录下创建 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])
    }
  })
  1. package.json稍作修改
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "el": "electron .",
  },
  1. 运行命令
yarn el
  1. 运行效果
    在这里插入图片描述

安装React

注意:这一步的所有操作都是在上面electron的基础上进行进一步的修改操作,没有新起项目。

  1. 下载 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",
        ...
      },
    
  2. 根目录创建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;
    
    
  3. package.json稍作修改

    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
      "el": "electron .",
      "start:web": "react-scripts start",
    },
    
  4. 运行命令

    yarn start:web
    
  5. 运行效果
    在这里插入图片描述
    通过与上面electron目录运行结果对比,会发现缺少Node.js , Chromium , 和 Electron .对应的版本号,这是因为react是直接运行在浏览器端,浏览器是不能访问node相关的一部分api所导致的。

功能拆分

上面两个项目都能够正常跑起来了,但是还达不到初始目标,希望的是electron与react能够结合在一起使用,那怎么操作呢?不着急,请接着往下看。

把electron 和 react 分别用各自的目录存放,公共文件单独找个文件放

基于以上目的,把上面的两个文件给合并抽离,形成最终文件目录,如下图:

在这里插入图片描述

直接这样放置,就能够跑起来吗?

简直不要太天真 O(∩_∩)O哈哈~

  1. 目录结构发生变化
  2. 并且还要把 electron 和 react 结合起来使用,还要实现react项目代码修改,桌面端应用界面也要跟着修改(想想react都有哪些特性)
  3. 还要实现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 的桌面端项目,但是就这个运行方式感觉还是不太安逸,非常的麻烦,具体问题如下:

  1. 需要跑两个命令
  2. 关掉 electron 窗口后,端口仍被占有的情况
  3. 需要 3000 端口跑起来了再刷新一下 electron 才会有内容
  4. 浏览器会打开一个 3000 端口的 tab 页, electron 会弹出加载了3000端口内容的窗口,理想状态下只需要保留 electron 中的窗口就好了

根据上面4点问题,也不是没有办法解决,一把梭打开应用完全没有问题。

请看具体实现方式:

  1. 下载 npm 库 concurrently,这个主要是解决上面第一和第二个问题,同时同步,一次可以完美的运行多个命令。
  2. 下载 npm 库 wait-on 字面意思,就是等待什么执行完之后再执行什么命令,解决我们的第三个问题。
  3. 下载 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;
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐