Electron 应用开发 - 基础概念

2024/1/13 ElectronDesktop学习笔记

# 主进程和渲染进程

Electron 继承了 Chromium 的多进程架构,Chromium 始于主进程,从主进程派生出多个渲染进程。

渲染进程可以理解为浏览器窗口,主进程保存对渲染进程的引用,可以根据需要创建或销毁渲染进程。

# 主进程

主进程是 Electron 应用的核心,通常由一个 main.js 定义,可以在 package.json 中指定。

{
  "main": "main.js"
}
1
2
3

main.js 作为程序的入口,负责管理整个应用的生命周期、窗口管理、原生功能调用等。

可以使用 app 模块来管理应用程序的生命周期:

const { app } = require("electron")

app.on("ready", () => {
  console.log("App is ready")
})

app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit()
  }
})
1
2
3
4
5
6
7
8
9
10
11

# app 常用生命周期

  • will-finish-launching - 当 Electron 完成基本的初始化时触发
  • ready - 当 Electron 完成初始化时触发
    • 在此事件中,可以创建浏览器窗口、设置菜单、注册全局快捷键等
  • window-all-closed - 当所有窗口都关闭时触发
    • 在 macOS 上,即使所有窗口都关闭,应用程序也不会退出,而是保持活动状态
    • 在 Windows 和 Linux 上,所有窗口关闭时,应用程序将退出
  • before-quit - 当应用程序开始退出之前触发
    • 在此事件中,可以取消退出
  • will-quit - 当应用程序开始退出时触发
  • quit - 当应用程序退出时触发
    • 执行一些清理操作

# 创建窗口

const { BrowserWindow } = require("electron")

const win = new BrowserWindow({ width: 800, height: 600 })

win.loadURL("https://www.cellinlab.com")

win.on("closed", () => {
  win = null
})

win.once("ready-to-show", () => {
  win.show()
})
1
2
3
4
5
6
7
8
9
10
11
12
13

BrowserWindow 常用生命周期:

  • closed - 当窗口关闭时触发
  • focus - 当窗口获得焦点时(激活)触发
  • show - 当窗口显示时触发
  • hide - 当窗口隐藏时触发
  • maximize - 当窗口最大化时触发
  • minimize - 当窗口最小化时触发

# 调用原生功能

主进程中可以调用原生功能,如创建系统托盘、创建菜单、注册全局快捷键等。

const { clipboard, globalShortcut, Menu } = require("electron")

// write text to clipboard
clipboard.writeText("Hello Electron")
clipboard.readText()

// register global shortcut
globalShortcut.register("CommandOrControl+X", () => {
  console.log("CommandOrControl+X is pressed")
})

// create menu
const template = [
  {
    label: "Edit",
    submenu: [
      {
        label: "Copy",
        accelerator: "CmdOrCtrl+C",
        role: "copy",
      },
      {
        label: "Paste",
        accelerator: "CmdOrCtrl+V",
        role: "paste",
      },
    ],
  },
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 渲染进程

渲染进程是 Electron 应用的 UI 线程,每个渲染进程对应一个窗口,通常由 HTML、CSS、JavaScript 构建。

渲染进程和主进程之间通过 IPC 通信,渲染进程通过 remote 模块调用主进程的功能。

# 预加载脚本 preload.js

预加载脚本包含了那些执行于渲染进程上下文中的脚本,且先于网页内容开始加载。在 preload.js 中,不仅可以使用 Node.js 的 API,还可以使用 Electron 渲染进程的 API 和 DOM API。还可以通过 IPC 与主进程通信,实现调用主进程的功能。

preload.js 虽然运行于渲染器的环境中,却因此拥有了更多的权限。

// main.js
const { BrowserWindow } = require("electron")

const win = new BrowserWindow({
  webPreferences: {
    preload: path.join(__dirname, "preload.js"),
  },
})
1
2
3
4
5
6
7
8
// preload.js
const fs = require("fs")

window.myAPI = {
  fsExists: fs.existsSync,
}
1
2
3
4
5
6
<script>
  window.myAPI.fsExists("path")
</script>
1
2
3

需要注意的是,从 Electron 12 开始,contextIsolation 默认为 truepreload.js 中的全局变量不会注入到页面中,需要通过 contextBridge 暴露 API。

# 进程间通信

# ipcMain & ipcRenderer

ipcMain 是一个仅在主进程中以异步方式工作的模块,处理与渲染进程的通信。

ipcRenderer 是一个仅在渲染进程中以异步方式工作的模块,处理与主进程的通信。

以上两者都继承自 EventEmitter,可以通过 on 方法监听事件,通过 send 方法发送事件。

EventEmitter.on("channel", (event, arg) => {})

EventEmitter.send("channel", arg)
1
2
3

# 渲染进程 -> 主进程

渲染进程通过 ipcRenderer 发送消息给主进程:

  • ipcRenderer.send(channel, ...args)

    • 发信息

      // in renderer process
      const { ipcRenderer } = require("electron")
      
      ipcRenderer.send("message", "Hello Electron")
      
      1
      2
      3
      4
    • 接收信息

      // in main process
      const { ipcMain } = require("electron")
      
      ipcMain.on("message", (event, arg) => {
        console.log(arg) // Hello Electron
      })
      
      1
      2
      3
      4
      5
      6
    • 回复信息

      // in main process
      const { ipcMain } = require("electron")
      
      ipcMain.on("message", (event, arg) => {
        console.log(arg) // Hello Electron
        event.reply("reply", "Hello Renderer")
      })
      
      1
      2
      3
      4
      5
      6
      7
      // in renderer process
      const { ipcRenderer } = require("electron")
      
      ipcRenderer.on("reply", (event, arg) => {
        console.log(arg) // Hello Renderer
      })
      
      1
      2
      3
      4
      5
      6
  • ipcRenderer.invoke(channel, ...args)

    • 发信息

      // in renderer process
      const { ipcRenderer } = require("electron")
      
      ipcRenderer.invoke("message", "Hello Electron").then((result) => {
        console.log(result) // Hello Renderer
      })
      
      1
      2
      3
      4
      5
      6
    • 接收信息并回复

      // in main process
      const { ipcMain } = require("electron")
      
      ipcMain.handle("message", async (event, arg) => {
        console.log(arg) // Hello Electron
        return "Hello Renderer"
      })
      
      1
      2
      3
      4
      5
      6
      7
  • ipcRenderer.sendSync(channel, ...args)

    • 发信息

      // in renderer process
      const { ipcRenderer } = require("electron")
      
      const result = ipcRenderer.sendSync("message", "Hello Electron")
      console.log(result) // Hello Renderer
      
      1
      2
      3
      4
      5
    • 接收信息

      // in main process
      const { ipcMain } = require("electron")
      
      ipcMain.on("message", (event, arg) => {
        console.log(arg) // Hello Electron
        event.returnValue = "Hello Renderer"
      })
      
      1
      2
      3
      4
      5
      6
      7

# 主进程 -> 渲染进程

主进程通过 webContents 主动发送消息给渲染进程:

  • webContents.send(channel, ...args)

    • 发信息

      // in main process
      const { BrowserWindow } = require("electron")
      
      const win = new BrowserWindow()
      
      win.webContents.send("message", "Hello Renderer")
      
      1
      2
      3
      4
      5
      6
    • 接收信息

      // in renderer process
      const { ipcRenderer } = require("electron")
      
      ipcRenderer.on("message", (event, arg) => {
        console.log(arg) // Hello Renderer
      })
      
      1
      2
      3
      4
      5
      6

# 渲染进程 -> 渲染进程

默认情况下,渲染进程之间是无法直接通信的,但可以通过主进程转发消息实现。

  • 主进程转发

    // main.js
    const { ipcMain } = require("electron")
    
    function createWin_1() {
      let win = new BrowserWindow()
      win.loadURL("win_1.html")
      win.on("closed", () => {
        win = null
      })
      return win
    }
    
    function createWin_2() {
      let win = new BrowserWindow()
      win.loadURL("win_2.html")
      win.on("closed", () => {
        win = null
      })
      return win
    }
    
    app.on("ready", () => {
      const win_1 = createWin_1()
      const win_2 = createWin_2()
    
      ipcMain.on("msgFromWin_1", (event, arg) => {
        win_2.webContents.send("msgToWin_2", arg)
      })
    
      ipcMain.on("msgFromWin_2", (event, arg) => {
        win_1.webContents.send("msgToWin_1", arg)
      })
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <!-- win_1.html -->
    <script>
      const { ipcRenderer } = require("electron")
    
      ipcRenderer.send("msgFromWin_1", "Hello Win_2")
    
      ipcRenderer.on("msgToWin_1", (event, arg) => {
        console.log(arg)
      })
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!-- win_2.html -->
    <script>
      const { ipcRenderer } = require("electron")
    
      ipcRenderer.send("msgFromWin_2", "Hello Win_1")
    
      ipcRenderer.on("msgToWin_2", (event, arg) => {
        console.log(arg)
      })
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • 使用 MessagePort

    • MessagePort 是基于 MDN 的 Web API 实现,可以在渲染进程直接创建,也可以在主进程创建。
    • MessagePort 可以通过 postMessage 方法发送消息,通过 onmessage 监听消息。
    // in main process
    const { BrowserWindow, app, MessageChannelMain } = require("electron")
    
    app.whenReady().then(async () => {
      const win_1 = new BrowserWindow({
        show: false,
        webPreferences: {
          contextIsolation: false,
          preload: "preload_1.js",
        },
      })
    
      const win_2 = new BrowserWindow({
        show: false,
        webPreferences: {
          contextIsolation: false,
          preload: "preload_2.js",
        },
      })
    
      const { port1, port2 } = new MessageChannelMain()
    
      win_1.once("ready-to-show", () => {
        win_1.webContents.postMessage("port", null, [port1])
      })
    
      win_2.once("ready-to-show", () => {
        win_2.webContents.postMessage("port", null, [port2])
      })
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // in renderer process
    const { ipcRenderer } = require("electron")
    
    ipcRenderer.on("port", (event, ports) => {
      window.electronMsgPort = ports[0]
    
      window.electronMsgPort.onmessage = (event) => {
        console.log(event.data)
      }
    
      window.electronMsgPort.postMessage("Hello Win_2")
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

# 原生能力

# Electron 原生 GUI

  • BrowserWindow 应用窗口
  • Tray 应用托盘
  • Menu 应用菜单
  • Notification 应用通知
  • dialog 应用对话框

# Electron 操作系统 API

  • clipboard 剪贴板
  • globalShortcut 全局快捷键
  • screen 屏幕
  • desktopCapturer 音视频捕获
  • powerMonitor 电源管理

# Nodejs API

可以在主进程和渲染进程中使用 Nodejs API,但需要注意的是,渲染进程中的 Nodejs API 是通过 contextBridge 暴露的,需要在 preload.js 中使用。

# Nodejs 调用原生功能

# 使用 C++ 扩展 Nodejs

  • 原生模块

    当一个 Node.js 的 C++ 模块在 OSX 下编译会得到一个后缀是 _.node 的模块,本质上是 _.dylib 的动态链接库;而在 Windows 上本质上是 \*.dll 的动态链接库。在 Linux 下则是 .so 的动态链接库。

  • node-gyp
    • node-gyp 是一个跨平台的命令行工具,用于编译 Node.js 的 C++ 模块。
  • 编写 C++ 扩展的方式
    • NAN(Native Abstractions for Node.js)
      • 提供了一组跨不同版本的 Node.js API 的抽象,帮助开发者编写跨版本兼容的 Node.js C++ 扩展。
    • N-API(Node-API)
      • Node.js 提供的一组 API,用于编写跨版本兼容的 Node.js C++ 扩展。
    • Native Addon

# 使用 node-ffi-napi 调用动态链接库

node-ffi-napi 是一个使用纯 JavaScript 调用动态链接库的 Node.js 模块。可以用于在不编写任何 C++ 代码的情况下,调用动态链接库。

# 使用 Rust 构建 Node 原生模块

除了使用 C++ 扩展 Nodejs,还可以使用 Rust 构建 Node 原生模块。

# Nodejs 调用 OS 脚本

可以使用 Nodejs 调用系统集成好的一些能力。

# WinRT

WinRT 是 Windows Runtime 的简称,是 Windows 8 引入的一种新的应用程序编程模型,用于开发 Windows 8 应用程序。它提供了一系列的 API,用于访问 Windows 系统的各种功能,如文件系统、网络、设备、传感器等。

在 Windows 中,可以使用 NodeRT 调用 WinRT API。

# AppleScript

AppleScript 是 macOS 系统的脚本语言,可以用于控制 macOS 系统的各种应用程序。

在 macOS 中,可以使用 node-applescript 调用 AppleScript。

# Shell

Shell 提供了与操作系统交互的接口,可以通过 Shell 调用系统命令。

可以通过 Nodejs 的 child_process 模块调用 Shell。

# Electron 跨平台兼容性

# Electron Native API 的平台差异

Electron 的 Native API 在不同平台上的实现是不同的,需要注意的是,不同平台上的实现可能会有差异。

# 操作系统的差异

操作系统本身存在差异,在调用不同操作系统的 API 时,需要注意差异。

const { platform } = require("process")

if (platform === "darwin") {
  // macOS
} else if (platform === "win32") {
  // Windows
} else if (platform === "linux") {
  // Linux
}
1
2
3
4
5
6
7
8
9

# 用户习惯的差异

不同操作系统的用户习惯是不同的,需要注意根据用户习惯调整应用程序的行为。如:在 macOS 上,关闭窗口并不会退出应用程序,而在 Windows 和 Linux 上,关闭窗口会退出应用程序。

# 文件路径的差异

不同操作系统的文件路径是不同的,需要注意文件路径的差异。

const { app } = require("electron")

app.getPath("appData")
// macOS: /Users/<user>/Library/Application Support
// Windows: C:\Users\<user>\AppData\Roaming
// Linux: /home/<user>/.config
1
2
3
4
5
6

在进行文件路径拼接时,可以使用 path.join 方法,避免手动拼接路径。

const path = require("path")

path.join("foo", "bar", "baz/asdf", "quux", "..")
// macOS: foo/bar/baz/asdf
// Windows: foo\bar\baz\asdf
// Linux: foo/bar/baz/asdf
1
2
3
4
5
6
最近更新: 10 个月前