在本文中,我们将使用Electron和React创建一个简单的桌面应用程序。这将是一个名为“ scratchpad”的小型文本编辑器,类似于FromScratch,它会在您键入时自动保存更改。我们将通过使用Electron Forge(Electron团队提供的最新构建工具)来确保应用程序的安全性。
Electron Forge是“用于创建,发布和安装现代Electron应用程序的完整工具”。它提供了一个方便的开发环境,并配置了构建用于多个平台的应用程序所需的一切(尽管我们在本文中不会涉及)。
我们假设您知道Electron和React是什么,尽管您无需了解这些内容即可随文章一起学习。
设置
本教程假定您在计算机上安装了Node。如果不是这种情况,请转到官方下载页面[https://nodejs.org/en/download/]并为您的系统获取正确的二进制文件,或者使用版本管理器(例如nvm)。我们还将假设可以正常安装Git。
我将在下面使用两个重要术语:“ main”和“ renderer”。电子应用程序由Node.js JavaScript文件“管理”。该文件称为“主”进程,它负责与操作系统相关的所有事情,并负责创建浏览器窗口。这些浏览器窗口运行Chromium,被称为Electron的“渲染器”部分,因为它实际上是在屏幕上渲染某些内容的部分。
现在让我们开始建立一个新项目。由于我们要使用Electron Forge和React,因此我们将转到Forge网站,并查看集成React的指南。
首先,我们需要使用webpack模板设置Electron Forge。我们可以在一个终端命令中执行以下操作:
$ npx create-electron-app scratchpad --template=webpack
运行该命令将花费一些时间,因为它会设置并配置从Git到Webpack到package.json
文件的所有内容。完成后,我们cd
进入该目录,这是我们看到的内容:
scratchpad git:(master) ls node_modules package.json src webpack.main.config.js webpack.renderer.config.js webpack.rules.js
我们将跳过node_modules
和package.json
,在浏览该src
文件夹之前,让我们浏览一下webpack文件,因为这里有3个文件。这是因为Electron实际上运行两个JavaScript文件:一个用于Node.js部分,即“ main”,它是在其中创建浏览器窗口并与操作系统的其余部分通信的;而Chromium部分则称为“ renderer”,即屏幕上实际显示的部分。
第三个Webpack文件-webpack.rules.js
是在Node.js和Chromium之间进行任何共享配置的位置,以避免重复。
好的,现在是时候查看该src
文件夹了:
src git:(master) ls index.css index.html main.js renderer.js
不太复杂:一个HTML和CSS文件,以及一个用于main和渲染器的JavaScript文件。很好看 我们将在文章的后面部分中介绍这些内容。
添加React
配置webpack可能非常艰巨,因此幸运的是,我们可以在很大程度上遵循将React集成到Electron中的指南。我们将从安装所需的所有依赖关系开始。
首先,devDependencies
:
npm install --save-dev @babel/core @babel/preset-react babel-loader
其次是React和React-dom作为常规依赖项:
npm install --save react react-dom
安装了所有依赖项之后,我们需要教webpack支持JSX。我们可以使用webpack.renderer.js
或来执行此操作webpack.rules.js
,但是我们将按照指南进行操作,并将以下加载程序添加到中webpack.rules.js
:
module.exports = [ ... { test: /\.jsx?$/, use: { loader: 'babel-loader', options: { exclude: /node_modules/, presets: ['@babel/preset-react'] } } },];
好的,应该可以。让我们通过打开src/renderer.js
它的内容并将其替换为以下内容来快速对其进行测试:
import './app.jsx';import './index.css';
然后创建一个新文件src/app.jsx
并添加以下内容:
import React from 'react'; import ReactDOM from 'react-dom'; ReactDOM.render(<h2>Hello from React in Electron!</h2>, document.body);
我们可以通过npm start
在控制台中运行来测试它是否有效。如果它打开一个显示“ Electro in Reactn的问候!”的窗口,那么一切就很好了。
您可能已经注意到,当窗口显示时,devtools已打开。这是因为main.js
文件中的这一行:
mainWindow.webContents.openDevTools();
暂时将其保留是很好的,因为它将在我们工作时派上用场。main.js
在配置其安全性和其他设置时,我们将在本文后面进行介绍。
至于控制台中的错误和警告,我们可以放心地忽略它们。在安装React组件时,document.body
确实会受到第三方代码的干扰,但是我们不是一个网站,并且不会运行任何不是我们的代码。Electron也会向我们发出警告,但稍后我们将对其进行处理。
建立我们的功能
提醒一下,我们将构建一个小的暂存器:一个小的应用程序,可以在键入时保存输入的内容。
首先,我们将添加CodeMirror和react-codemirror,以便获得一个易于使用的编辑器:
npm install --save react-codemirror codemirror
让我们设置CodeMirror。首先,我们需要打开src/renderer.js
并导入并需要一些CSS。CodeMirror附带了两个不同的主题,因此请选择一个您喜欢的主题,但是在本文中,我们将使用Material主题。您的renderer.js现在应如下所示:
import 'codemirror/lib/codemirror.css'; import 'codemirror/theme/material.css'; import './app.jsx'; import './index.css';
请注意我们如何在CodeMirror CSS之后导入自己的文件。我们这样做是为了以后可以更轻松地覆盖默认样式。
然后在我们的app.jsx
文件中CodeMirror
,如下所示导入组件:
import CodeMirror from 'react-codemirror';
在app.jsx
添加CodeMirror的地方创建一个新的React组件:
const ScratchPad = () => { const options = { theme: "material" }; const updateScratchpad = newValue => { console.log(newValue) } return <CodeMirror value="Hello from CodeMirror in React in Electron" onChange={updateScratchpad} options={options} />;}
还要替换render函数来加载我们的ScratchPad组件:
ReactDOM.render(<ScratchPad />, document.body);
现在启动应用程序时,应该会看到一个文本编辑器,其中包含文本“来自Electron中的CodeMirror的Hello”。当我们键入它时,更新将显示在我们的控制台中。
我们还看到的是有一个白色边框,并且我们的编辑器实际上并未填充整个窗口,因此让我们对此进行一些处理。在执行此操作时,我们将在index.html
和index.css
文件中进行一些内务处理。
首先,在中index.html
,让我们删除body元素内的所有内容,因为我们还是不需要它。然后,将标题更改为“ Scratchpad”,以使标题栏不会显示“ Hello World!”。应用加载时。
我们还将添加一个Content-Security-Policy
。这意味着本文中要讨论的内容太多了(MDN进行了很好的介绍,但这本质上是防止第三方代码执行我们不希望发生的事情的一种方法。在这里,我们告诉它仅允许脚本来自我们的来源(文件),仅此而已。
总而言之,我们index.html
将非常空白,看起来像这样:
<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title>Scratchpad</title> <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> </head> <body></body></html>
现在,移至index.css
。我们可以删除其中的所有内容,并将其替换为:
html, body { position: relative; width:100vw; height:100vh; margin:0; background: #263238;}.ReactCodeMirror,.CodeMirror { position: absolute; height: 100vh; inset: 0;}
这可以做两件事:
-
它删除默认情况下body元素具有的边距。
-
它使CodeMirror元素与窗口本身具有相同的高度和宽度。
-
它为主体元素添加了相同的背景色,因此可以很好地融合。
注意我们如何使用inset,这是top,right,bottom和left值的简写CSS属性。由于我们知道我们的应用程序将始终在Chromium 89版中运行,因此我们可以使用现代CSS而不用担心支持!
因此,这非常好:我们有一个可以启动的应用程序,您可以在其中输入内容。甜的!
除非,当我们关闭应用程序并再次重新启动它时,一切都消失了。我们希望写入文件系统,以便保存我们的文本,并且我们希望尽可能安全地执行此操作。为此,我们现在将重点转移到main.js
文件上。
现在,您可能还已经注意到,即使我们在html
和body
元素上添加了背景色,在加载应用程序时窗口仍然是白色的。那是因为加载index.css
文件需要花费几毫秒的时间。为了改善外观,我们可以在创建浏览器窗口时将其配置为具有特定的背景颜色。因此,让我们进入main.js
文件并添加背景色。更改您的mainWindow
外观,如下所示:
const mainWindow = new BrowserWindow({ width: 800, height: 600, backgroundColor: "#263238",});
现在,当您开始时,白色的闪光应该消失了!
将暂存器保存在磁盘上
当我在本文前面解释Electron时,我使它比实际要简单一些。尽管Electron具有主渲染过程,但近年来实际上存在第三个上下文,即预加载脚本。
preload脚本背后的想法是,它充当主程序(可以访问所有Node.js API)和渲染器(绝对不能!)之间的桥梁。在预加载脚本中,我们可以添加可以与主进程通信的函数,然后以不影响应用程序安全性的方式将它们公开给渲染器进程。
因此,让我们概述一下我们想做的事情:
-
当用户进行更改时,我们希望将其保存到磁盘。
-
当应用程序启动时,我们要从磁盘加载回该存储的内容,并确保它显示在我们的CodeMirror编辑器中。
首先,我们将编写代码,使我们可以将内容加载并存储到磁盘中的main.js
文件中。该文件已经导入了Node的path
模块,但是我们还需要导入fs
以处理文件系统。将此添加到文件的顶部:
const fs = require('fs');
然后,我们需要为存储的文本文件选择一个位置。在这里,我们将使用该appData
文件夹,该文件夹是您的应用自动创建的用于存储信息的位置。您可以使用此app.getPath
功能来获得它,因此让我们在函数之前在文件中添加一个filename
变量:main.js
createWindow
const filename = `${app.getPath('userData')}/content.txt`;
之后,我们将需要两个功能:一个用于读取文件,另一个用于存储文件。我们将它们称为loadContent
和saveContent
,这是它们的外观:
const loadContent = async () => { return fs.existsSync(filename) ? fs.readFileSync(filename, 'utf8') : '';}const saveContent = async (content) => { fs.writeFileSync(filename, content, 'utf8');}
它们都是使用内置fs
方法的一线工具。对于loadContent
,我们首先需要检查文件是否已经存在(因为第一次启动时它将不存在!),如果不存在,我们可以返回一个空字符串。
saveContent
甚至更简单:调用它时,我们writeFile
使用文件名,内容进行调用,并确保将其存储为UTF8。
现在我们有了这些功能,我们需要将它们连接起来。而这些通信的方式是通过IPC(进程间通信)。让我们接下来进行设置。
设置IPC
首先,我们需要ipcMain
从Electron导入,因此请确保您的require('Electron')
输入内容main.js
如下所示:
ipcMain.on("saveContent", (e, content) =>{ saveContent(content); });
IPC使您可以将消息从渲染器发送到主服务器(反之亦然)。在saveContent
函数下方,添加以下内容:
ipcMain.on("saveContent", (e, content) =>{ saveContent(content);});
当我们收到saveContent
来自渲染器的消息时,我们将saveContent
使用获得的内容来调用该函数。非常简单。但是我们怎么称呼那个功能呢?那就是事情变得有点复杂的地方。
我们不希望渲染器文件访问所有这些内容,因为那将是非常不安全的。我们需要添加一个可以与main.js
文件和渲染器文件对话的中介程序。这就是预加载脚本可以执行的操作。
让我们preload.js
在src
目录中创建该文件,然后mainWindow
像这样链接它:
const mainWindow = new BrowserWindow({ width: 800, height: 600, backgroundColor: "#263238", webPreferences: { preload: path.join(__dirname, 'preload.js'), }});
然后在预加载脚本中,添加以下代码:
const { ipcRenderer, contextBridge } = require("electron"); contextBridge.exposeInMainWorld( 'scratchpad', { saveContent: (content) => ipcRenderer.send('saveContent', content) } )
contextBridge.exposeInMainWorld
让我们saveContent
在renderer.js
文件中添加一个函数,而无需使整个Electron和Node可用。这样,渲染器只知道内容saveContent
却不知道如何保存或保存在何处。第一个参数“ scratchpad”是saveContent
将在其中可用的全局变量。要在我们的React应用程序中调用它,请执行window.scratchpad.saveContent(content);
。
现在就开始吧。我们打开app.jsx
文件并更新updateScratchpad
函数,如下所示:
const updateScratchpad = newValue => { window.scratchpad.saveContent(newValue); };
就是这样。现在,我们所做的每个更改都将写入磁盘。但是,当我们关闭并重新打开应用程序时,它再次为空。我们也需要在初次启动时加载内容。
打开应用程序时加载内容
我们已经在中编写了loadContent
函数main.js
,因此我们将其连接到我们的UI。因为不需要响应,所以我们使用了IPCsend
并on
保存了内容,但是现在我们需要从磁盘获取文件并将其发送到渲染器。为此,我们将使用IPCinvoke
和handle
功能。invoke
返回一个可以用handle
函数返回的值解决的promise 。
首先,在处理程序main.js
下方的文件中写入处理saveContent
程序:
ipcMain.handle("loadContent", (e) => { return loadContent(); });
在我们的preload.js
文件中,我们将调用此函数并将其公开给我们的React代码。在exporeInMainWorld
属性列表中,添加第二个属性content
:
contextBridge.exposeInMainWorld( 'scratchpad', { saveContent: (content) => ipcRenderer.send('saveContent', content), content: ipcRenderer.invoke("loadContent"), });
在我们的代码中,app.jsx
我们可以用达成目标window.scratchpad.content
,但这是一个承诺,因此await
在加载之前,我们需要这样做。为此,我们将ReactDOM渲染器包装在异步IFFE中,如下所示:
(async () => { const content = await window.scratchpad.content; ReactDOM.render(<ScratchPad text={content} />, document.body); })();
我们还更新了ScratchPad
组件,以使用文本prop作为我们的初始值:
const ScratchPad = ({text}) => { const options = { theme: "material" }; const updateScratchpad = newValue => { window.scratchpad.saveContent(newValue); }; return ( <CodeMirror value={text} onChange={updateScratchpad} options={options} /> ); };
一切就在这里:我们已经成功集成了Electron和React,并创建了一个小型应用程序,用户可以键入该应用程序,并且该应用程序会自动保存,而无需给暂存器提供对我们不希望提供的文件系统的任何访问权限。
我们做完了,对不对?好吧,我们可以做一些事情来使它看起来更像“应用程序”。
“更快”加载
您可能已经注意到,当您打开应用程序时,需要花费一些时间才能看到文本。看起来不太好,所以最好等待应用程序加载,然后再显示它。这将使整个应用程序感觉更快,因为您不会看到不活动的窗口。
首先,我们添加show: false
到new BrowserWindow
调用中,并为ready-to-show
事件添加一个侦听器。在那里,我们展示并聚焦我们创建的窗口:
const mainWindow = new BrowserWindow({ width: 800, height: 600, backgroundColor: "#263238", show: false, webPreferences: { preload: path.join(__dirname, 'preload.js'), }});mainWindow.once('ready-to-show', () => { mainWindow.show(); mainWindow.focus();});
在main.js
文件中时,我们也将删除openDevTools
呼叫,因为我们不想向用户显示该呼叫:
mainWindow.webContents.openDevTools();
现在启动应用程序时,将显示应用程序窗口,其中已包含内容。好多了!
生成和安装应用程序
现在应用程序完成了,我们可以构建它了。Electron Forge已经为此创建了一个命令。Run npm run make
and Forge将为您当前的操作系统构建一个应用程序和安装程序,并将其放置在“ out”文件夹中,无论是.exe
,.dmg
还是,您都可以进行安装.deb
。
如果您使用的是Linux,并且遇到有关的错误rpmbuild
,请安装“ rpm”软件包,例如sudo apt install rpm
在Ubuntu上。如果您不想制作rpm安装程序,也可以从中的制造商处删除“ @ electron-forge / maker-rpm”块package.json
。
这会错过一些基本的东西,例如代码签名,公证和自动更新,但是我们将在以后的文章中介绍。
这是整合Electron和React的真正最小的例子。我们可以使用应用程序本身做更多的事情。这里有一些想法供您探索:
-
添加一个很酷的桌面图标。
-
基于操作系统设置(通过媒体查询或使用Electron提供的nativeTheme api)创建暗和亮模式支持。
-
使用mousetrap.js或Electron的菜单加速器和globalShortcuts添加快捷方式。
-
存储并恢复窗口的大小和位置。
-
与服务器而不是磁盘上的文件同步。