Electron-d-1.0

Electron

一、构建第一个electron应用

1.初始化

npm init -y //初始化项目
npm install --save-dev electron //下载electron到开发依赖

2.创建入口index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
    We are using Node.js <span id="node-version"></span>,
    Chromium <span id="chrome-version"></span>,
    and Electron <span id="electron-version"></span>.
  </body>
</html>

3.创建入口文件

//main.js
const { app,BrowserWindow } = require('electron')

const createWindow = () => {
    const win = new BrowserWindow({
      width: 800,
      height: 600
    })
  
    win.loadFile('index.html')
  }

app.whenReady().then(() => {
    createWindow()
  })

4.创建窗口应用

Package.json配置

//Package.json
{
  "name": "electron-app",
  "version": "1.0.0",
  "description": "一个处于实验阶段的app",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "keywords": [],
  "author": "alanmf",
  "license": "MIT",
  "devDependencies": {
    "electron": "^21.3.0"
  }
}
npm start //执行 electron .

为了更方便页面更新我们使用nodemon

nodemon是一个自动重启node应用的工具,当监听的文件或监听目录下的文件发生修改时,自动重启应用

npm i nodemon -D

更改package.json

//Package.json
{
  "name": "electron-app",
  "version": "1.0.0",
  "description": "一个处于实验阶段的app",
  "main": "main.js",
  "scripts": {
    "start": "nodemon --exec electron ."
  },
  "keywords": [],
  "author": "alanmf",
  "license": "MIT",
  "devDependencies": {
    "electron": "^21.3.0"
  }
}

二、electron核心

1.electron核心概念

主进程;启动项目时运行的main.js脚本就是我们说的主进程,在主进程运行的脚步可以创建web页面的形式展示GUI,主进程只有一个。

渲染进程:每个Electron的页面都在运行着自己的进程,这样的进程称之为渲染进程(基于Chromium的多进程结构)。

主进程使用BrowserWindow创建实例,主进程销毁后,对应的渲染进程会被终止。主进程与渲染进程通过 IPC 的方式(事件驱动)进行通信。

2.electron窗口控制台

//打开开发者工具
win.webContents.openDevTools();
//暂时关闭安全警告(不推荐)
process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
//配置CSP(阻挡一部分安全警告)
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">

3.主进程事件生命周期

关闭所有窗口时退出应用 Windows & Linux

在Windows和Linux上,关闭所有窗口通常会完全退出一个应用程序。

为了实现这一点,你需要监听 app 模块的 'window-all-closed'事件。如果用户不是在 macOS(darwin) 上运行程序,则调用 app.quit()`。

app.on('window-all-closed', () => {
  //macOS用户在关闭窗口时不会关闭应用
  if (process.platform !== 'darwin') app.quit()
})

当 Linux 和 Windows 应用在没有窗口打开时退出了,macOS 应用通常即使在没有打开任何窗口的情况下也继续运行,并且在没有窗口可用的情况下激活应用时会打开新的窗口。

为了实现这一特性,监听 app 模块的 activate 事件。如果没有任何浏览器窗口是打开的,则调用 createWindow() 方法。

因为窗口无法在 ready 事件前创建,你应当在你的应用初始化后仅监听 activate 事件。 通过在您现有的 whenReady() 回调中附上您的事件监听器来完成这个操作。

app.whenReady().then(() => {
  createWindow()
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

这样,我们的窗口的控件就完整了(实现macOS用户-没有窗口打开时创建新窗口-关闭窗口不关闭应用,点击程序坞应用打开新窗口)

4.通过预加载脚本从渲染器访问Node.js

现在,最后要做的是输出Electron的版本号和它的依赖项到你的web页面上。

在主进程通过Node的全局 process 对象访问这个信息是微不足道的。 然而,你不能直接在主进程中编辑DOM,因为它无法访问渲染器 文档 上下文。 它们存在于完全不同的进程!

这是将 预加载 脚本连接到渲染器时派上用场的地方。 预加载脚本在渲染器进程加载之前加载,并有权访问两个 渲染器全局 (例如 windowdocument) 和 Node.js 环境。

创建一个名为 preload.js 的新脚本:

//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])
  }
})

上面的代码访问 Node.js process.versions 对象,并运行一个基本的 replaceText 辅助函数将版本号插入到 HTML 文档中。

要将此脚本附加到渲染器流程,请在你现有的 BrowserWindow 构造器中将路径中的预加载脚本传入 webPreferences.preload 选项。

// include the Node.js 'path' module at the top of your file
const path = require('path')

// modify your existing createWindow() function
const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  win.loadFile('index.html')
}
// ...

这里使用了两个Node.js概念:

  • __dirname字符串指向当前正在执行脚本的路径 (在本例中,它指向你的项目的根文件夹)。
  • path.join API 将多个路径联结在一起,创建一个跨平台的路径字符串。

我们使用一个相对当前正在执行JavaScript文件的路径,这样相对路径将在开发模式和打包模式中都将有效。

5.contextBridge

contextBridge模块有以下方法:

contextBridge.exposeInMainWorld(apiKey, api)

  • apiKey string - 将 API 注入到 窗口 的键。 API 将可通过 window[apiKey] 访问。
  • api any - 你的 API可以是什么样的以及它是如何工作的相关信息如下。

应用开发接口(API)

提供给 exposeInMainWorldapi 必须是一个 FunctionstringnumberArrayboolean;或一个键为字符串,值为一个 FunctionstringnumberArrayboolean的对象;或其他符合相同条件的嵌套对象。

Function 类型的值被代理到其他上下文中,所有其他类型的值都会被 复制冻结。 在 API 中发送的任何数据 /原始数据将不可改变,在桥接器其中一侧的更新不会导致另一侧的更新。

代码应用:

//preload.js+
const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('myApi',{
    platform:process.platform
  })

那我们就可以在页面上去访问window上的方法了

//app.js
console.log(window.myApi.platform) //darwin

三、主进程与渲染进程通信

进程间通信 (IPC) 是在 Electron 中构建功能丰富的桌面应用程序的关键部分之一。 由于主进程和渲染器进程在 Electron 的进程模型具有不同的职责,因此 IPC 是执行许多常见任务的唯一方法,例如从 UI 调用原生 API 或从原生菜单触发 Web 内容的更改。

在 Electron 中,进程使用 ipcMainipcRenderer模块,通过开发人员定义的“通道”传递消息来进行通信。 这些通道是 任意 (您可以随意命名它们)和 双向 (您可以在两个模块中使用相同的通道名称)的。

1.渲染器进程到主进程(单向 )

要将单向 IPC 消息从渲染器进程发送到主进程,您可以使用 ipcRenderer.sendAPI 发送消息,然后使用 ipcMain.onAPI 接收。

我们下边就按照官网的例子来说吧!

(1) 使用 ipcMain.on 监听事件

在主进程(main.js)中,使用 ipcMain.on API 在 set-title 通道上设置一个 IPC 监听器:

//main.js+
const { ipcMain } = require('electron')
 ipcMain.on('set-title', (event, title) => {
      const webContents = event.sender
      const win = BrowserWindow.fromWebContents(webContents)
      win.setTitle(title)
    })
(2) 通过预加载脚本暴露 ipcRenderer.send

要将消息发送到上面创建的 IPC 监听器,可以使用 ipcRenderer.send API。 默认情况下,渲染器进程没有权限访问 Node.js 和 Electron 模块。 作为应用开发者,需要使用 contextBridge API 来选择要从预加载脚本中暴露哪些 API。

在您的预加载脚本中添加以下代码,向渲染器进程暴露一个全局的 window.electronAPI 变量。

//preload.js+
const { contextBridge,ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
    setTitle: (title) => ipcRenderer.send('set-title', title)
})

此时,将能够在渲染器进程中使用 window.electronAPI.setTitle() 函数。

(3) 构建渲染器进程 UI

在 BrowserWindow 加载的我们的 HTML 文件中,添加一个由文本输入框和按钮组成的基本用户界面:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>Hello World!</title>
  </head>
  <body>
    Title: <input id="title"/>
    <button id="btn" type="button">Set</button>
    <script src="./renderer/app.js"></script>
  </body>
</html>

将在导入的 renderer.js 文件中添加几行代码,以利用从预加载脚本中暴露的 window.electronAPI 功能:

//app.js
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
    const title = titleInput.value
    window.electronAPI.setTitle(title)
});

到目前的代码为止,我们的一个渲染进程到主进程的进程通信就已经实现了,我们可以试一试我们的UI界面功能

总结:

!!!我们上面有一些地方不理解的方法和API,我们现在讲一下

上面的 回调函数有两个参数:一个 IpcMainEvent 结构和一个 title 字符串。 每当消息通过 set-title 通道传入时,此函数找到附加到消息发送方的 BrowserWindow 实例,并在该实例上使用 win.setTitle API。

我们只是为了更简单的理解进程通信,具体 API 见官方文档吧!

2.渲染器进程到主进程(双向)

双向 IPC 的一个常见应用是从渲染器进程代码调用主进程模块并等待结果。 这可以通过将 ipcRenderer.invokeipcMain.handle 搭配使用来完成。

(1)使用 ipcMain.handle 监听事件

在主进程(main.js)中,我们将创建一个 handleFileOpen() 函数,它调用 dialog.showOpenDialog 并返回用户选择的文件路径值。 每当渲染器进程通过 dialog:openFile 通道发送 ipcRender.invoke 消息时,此函数被用作一个回调。 然后,返回值将作为一个 Promise 返回到最初的 invoke 调用。

//main.js
const { app,BrowserWindow,ipcMain,dialog } = require('electron')
const path = require('path')

//回调函数
async function handleFileOpen() {
  const { canceled, filePaths } = await dialog.showOpenDialog()
  if (canceled) {
    return
  } else {
    return filePaths[0]
  }
}

const createWindow = () => {
    const win = new BrowserWindow({
      width: 800,
      height: 600,
      webPreferences: {
        preload: path.join(__dirname, 'preload.js')
      }
    })
    win.loadFile('index.html') 
    win.webContents.openDevTools()
    
  }

  app.whenReady().then(() => {
    //监听,执行回调
    ipcMain.handle('dialog:openFile', handleFileOpen)
    createWindow()
    app.on('activate', () => {
      if (BrowserWindow.getAllWindows().length === 0) createWindow()
    })
  })
  app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') app.quit()
  })
(2)通过预加载脚本暴露 ipcRenderer.invoke

在预加载脚本中,我们暴露了一个单行的 openFile 函数,它调用并返回 ipcRenderer.invoke('dialog:openFile') 的值。 我们将在下一步中使用此 API 从渲染器的用户界面调用原生对话框。

//preload.js
const { contextBridge,ipcRenderer } = require('electron')
 contextBridge.exposeInMainWorld('electronAPI', {
    openFile: () => ipcRenderer.invoke('dialog:openFile')
})
(3)构建渲染器进程 UI

最后,让我们构建加载到 BrowserWindow 中的 HTML 文件。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>Dialog</title>
  </head>
  <body>
    <button type="button" id="btn">Open a File</button>
    File path: <strong id="filePath"></strong>
    <script src='./renderer/app.js'></script>
  </body>
</html>

用户界面包含一个 #btn 按钮元素,将用于触发我们的预加载 API,以及一个 #filePath 元素,将用于显示所选文件的路径。需要在渲染器进程脚本中编写几行代码

//app.js
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')

btn.addEventListener('click', async () => {
  const filePath = await window.electronAPI.openFile()
  filePathElement.innerText = filePath
})

到目前的代码为止,我们的一个渲染进程到主进程的双向进程通信就已经实现了,我们可以试一试我们的UI界面功能

总结:

ipcRenderer.invoke API (老方法了,但依然好用)是在 Electron 7 中添加的,作为处理渲染器进程中双向 IPC 的一种开发人员友好的方式。 但这种 IPC 模式存在几种替代方法。

使用 ipcRenderer.send我们用于单向通信的 ipcRenderer.send API 也可用于双向通信。 这是在 Electron 7 之前通过 IPC 进行异步双向通信的推荐方式。

//preload.js
// 您也可以使用 `contextBridge` API
// 将这段代码暴露给渲染器进程
const { ipcRenderer } = require('electron')

ipcRenderer.on('asynchronous-reply', (_event, arg) => {
  console.log(arg) // 在 DevTools 控制台中打印“pong”
})
ipcRenderer.send('asynchronous-message', 'ping')
//main.js
ipcMain.on('asynchronous-message', (event, arg) => {
  console.log(arg) // 在 Node 控制台中打印“ping”
  // 作用如同 `send`,但返回一个消息
  // 到发送原始消息的渲染器
  event.reply('asynchronous-reply', 'pong')
})

这种方法有几个缺点:

  • 您需要设置第二个 ipcRenderer.on 监听器来处理渲染器进程中的响应。 使用 invoke,您将获得作为 Promise 返回到原始 API 调用的响应值。
  • 没有显而易见的方法可以将 asynchronous-reply 消息与原始的 asynchronous-message消息配对。 如果您通过这些通道非常频繁地来回传递消息,则需要添加其他应用代码来单独跟踪每个调用和响应。

使用 ipcRenderer.sendSyncAPI也是可以替代的,但不推荐

ipcRenderer.sendSync API 向主进程发送消息,并 同步 等待响应。

//main.js
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
  console.log(arg) // 在 Node 控制台中打印“ping”
  event.returnValue = 'pong'
})
//perload.js
// 您也可以使用 `contextBridge` API
// 将这段代码暴露给渲染器进程
const { ipcRenderer } = require('electron')

const result = ipcRenderer.sendSync('synchronous-message', 'ping')
console.log(result) // 在 DevTools 控制台中打印“pong”

这份代码的结构与 invoke 模型非常相似,但出于性能原因,我们建议避免使用此 API。 它的同步特性意味着它将阻塞渲染器进程,直到收到回复为止。(容易阻塞渲染器进程)

3.主进程到渲染器进程

将消息从主进程发送到渲染器进程时,需要指定是哪一个渲染器接收消息。 消息需要通过其 WebContents实例发送到渲染器进程。 此 WebContents 实例包含一个 send 方法,其使用方式与 ipcRenderer.send 相同。

(1)使用 webContents 模块发送消息

我们需要首先使用 Electron 的 Menu 模块在主进程中构建一个自定义菜单,该模块使用 webContents.send API 将 IPC 消息从主进程发送到目标渲染器。

//main.js
const { app,BrowserWindow,ipcMain,Menu } = require('electron')
const path = require('path')

const createWindow = () => {
    const win = new BrowserWindow({
      width: 800,
      height: 600,
      webPreferences: {
        preload: path.join(__dirname, 'preload.js')
      }
    })

    const menu = Menu.buildFromTemplate([
      {
        label: app.name,
        submenu: [
        {
          click: () => win.webContents.send('update-counter', 1),
          label: 'Increment',
        },
        {
          click: () => win.webContents.send('update-counter', -1),
          label: 'Decrement',
        }
        ]
      }
  
    ])

    Menu.setApplicationMenu(menu)
    win.loadFile('index.html') 
    win.webContents.openDevTools()
    
  }

  app.whenReady().then(() => {
    ipcMain.on('counter-value', (_event, value) => {
      console.log(value) // will print value to Node console
    })
    createWindow()
    app.on('activate', () => {
      if (BrowserWindow.getAllWindows().length === 0) createWindow()
    })
  })

  app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') app.quit()
  })
(2)通过预加载脚本暴露 ipcRenderer.on

与前面的渲染器到主进程的示例一样,我们使用预加载脚本中的 contextBridgeipcRenderer 模块向渲染器进程暴露 IPC 功能:

//preload.js
const { contextBridge,ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
    handleCounter: (callback) => ipcRenderer.on('update-counter', callback)
})

加载预加载脚本后,渲染器进程应有权访问 window.electronAPI.handleCounter() 监听器函数。

(3)构建渲染器进程 UI

我们将在加载的 HTML 文件中创建一个接口,其中包含一个 #counter元素,我们将使用该元素来显示值:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>Menu Counter</title>
  </head>
  <body>
    Current value: <strong id="counter">0</strong>
    <script src='./renderer/app.js'></script>
  </body>
</html>

为了更新 HTML 文档中的值,我们将添加几行 DOM 操作的代码,以便在每次触发 update-counter 事件时更新 #counter 元素的值。

//app.js
const counter = document.getElementById('counter')
window.electronAPI.handleCounter((event, value) => {
    const oldValue = Number(counter.innerText)
    const newValue = oldValue + value
    counter.innerText = newValue
    event.sender.send('counter-value', newValue)
})

在上面的代码中,我们将回调传递给从预加载脚本中暴露的 window.electronAPI.onUpdateCounter 函数。 第二个 value 参数对应于我们传入 webContents.send 函数的 1-1,该函数是从原生菜单调用的。

到目前的代码为止,我们的一个主进程到渲染器进程的进程通信就已经实现了,我们可以试一试我们的UI界面功能

总结:

对于从主进程到渲染器进程的 IPC,没有与 ipcRenderer.invoke 等效的 API。 不过,可以从 ipcRenderer.on 回调中将回复发送回主进程。

我们可以对前面例子的代码进行略微修改来演示这一点。 在渲染器进程中,使用 event 参数,通过 counter-value 通道将回复发送回主进程。

//app.js
const counter = document.getElementById('counter')
window.electronAPI.handleCounter((event, value) => {
    const oldValue = Number(counter.innerText)
    const newValue = oldValue + value
    counter.innerText = newValue
    event.sender.send('counter-value', newValue)
})

在主进程中,监听 counter-value 事件并适当地处理它们。

//main.js
ipcMain.on('counter-value', (_event, value) => {
      console.log(value) // will print value to Node console
  })

4.渲染器进程到渲染器进程

没有直接的方法可以使用 ipcMainipcRenderer 模块在 Electron 中的渲染器进程之间发送消息。 为此,有两种选择:

  • 将主进程作为渲染器之间的消息代理。 这需要将消息从一个渲染器发送到主进程,然后主进程将消息转发到另一个渲染器。
  • 从主进程将一个 MessagePort 传递到两个渲染器。 这将允许在初始设置后渲染器之间直接进行通信。

参考链接:

[Electron官网]: “https://www.electronjs.org/zh/docs/latest/tutorial/quick-start

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2022-2023 alan_mf
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信