欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

前端必学桌面开发:Electron+React开发桌面应用(1W字以上超详细)

最编程 2024-05-07 09:47:09
...

本篇教程是使用Electron + React18进行开发,这里主要讲electron的使用。首先我们需要一个react的项目环境,react的项目搭建及开发教程可以参考我的react专栏里的文章:react相关技术这里都有

前言

Electron是一个开源的跨平台桌面应用程序开发框架,允许开发者使用前端 Web 技术(HTML、CSS 和 JavaScript)来构建桌面应用程序
背景和起源
Electron 最初由 GitHub 公司开发,最早用于构建 GitHub Desktop。随着其成功,Electron 逐渐成为一个受欢迎的开发框架,许多知名应用程序如 Visual Studio Code、Slack、WhatsApp 等也使用 Electron 构建。
基本原理
Electron 使用 Chromium 渲染引擎来显示 Web 内容,同时结合 Node.js 来提供对操作系统的访问和控制。这使得开发者能够使用 Web 技术来构建桌面应用程序,同时还能够利用底层操作系统的功能。
主要特点

  1. 跨平台: Electron 应用程序可以在多个操作系统(如 Windows、macOS、Linux)上运行,因为它们在不同平台上共享相同的核心代码。
  2. 前端技术: 开发者可以使用熟悉的前端技术,如 HTML、CSS 和 JavaScript,来构建用户界面。
  3. Node.js 集成: 通过 Node.js,开发者可以在应用程序中使用 JavaScript 来处理文件系统、网络通信、操作系统 API 等等。
  4. 自定义性: 开发者可以通过使用原生的 Web 技术和样式,创建非常定制化的用户界面。
  5. 社区支持: 有一个活跃的社区,提供了大量的插件和库,以帮助开发者构建更强大、更高效的应用程序。

核心组件

  1. 主进程(Main Process): 这是应用程序的主要控制中心,运行 Node.js 环境,负责管理和控制所有的渲染进程和窗口。
  2. 渲染进程(Renderer Process): 每个 Electron 窗口对应一个独立的渲染进程,它们运行在 Chromium 渲染引擎中,负责显示用户界面。
  3. 主窗口(Main Window): 主窗口是应用程序的主界面,通常是一个 Chromium 窗口,用来显示 Web 内容。
  4. 系统托盘图标(Tray): 允许在桌面右下角显示小图标,提供应用程序的快速访问和交互。

开发流程

  1. 使用 HTML、CSS 和 JavaScript 创建用户界面。
  2. 在主进程中使用 Node.js 进行应用程序的逻辑控制。
  3. 通过与底层操作系统 API 进行交互,实现文件操作、网络通信等功能。
  4. 使用 Electron 打包工具将应用程序打包成特定平台的可执行文件。

核心架构图解

Electron安装

安装electron

首先,我们需要在一个常规的React项目中,安装electron,为了使我们功能代码部分和electron窗口部分更清晰,我们可以在项目的根目录新建一个desktop文件夹,专门用来存放electron部分代码和资源。目录结构大概如图所示:
image.png
我们cd desktopdesktop文件夹下,执行npm init -y初始化包管理器,然后安装electron相关包:
electron:electron核心包
cross-env:cross-env 是一个用于设置跨平台环境变量的工具。它可以在 Windows、Linux 和 macOS 等操作系统上提供一致的环境变量设置方式,使得在不同平台上运行脚本时能够保持一致的行为。
electron-builder:electron-builder 是一个用于打包、构建和部署 Electron 应用程序的强大工具

npm i electron cross-env electron-builder

Electron App生命周期,创建窗口,应用运行

App生命周期

在electron应用的运行过程中存在着自己的生命周期,在不同的生命周期中我们可以做对应的事情,下面介绍一些常用的生命周期,electron的生命周期通过electron中的app实例监听,我们在desktop目录下新建一个index.js文件,作为electron的入口文件,并在其中监听应用的各个生命周期
ready
触发时机:当 Electron 初始化完成并且应用程序准备好创建浏览器窗口时。
作用:通常用于初始化应用程序的主要界面和一些基础设施。
示例:在 ready 事件中创建主窗口和初始化托盘。
certificate-error
触发时机:当在加载网页时发生证书错误时。
作用:可以在这个事件中拦截证书错误并决定是否继续加载页面。
示例:在证书错误时阻止默认行为并返回 true 以继续加载页面。
before-quit
触发时机:当用户尝试退出应用程序时,通常是通过关闭所有窗口或者点击关闭按钮。
作用:在应用程序退出之前执行一些清理操作。
示例:可以在这个事件中执行一些清理或保存操作。
window-all-closed
触发时机:所有应用程序窗口都被关闭时。
作用:在此事件中通常用于在应用程序完全退出之前保留某些功能。
示例:在 macOS 下通常会保留菜单栏。
activate
触发时机:在点击 Dock 图标(macOS)或者任务栏图标(Windows)时。
作用:通常用于在所有窗口都已关闭的情况下,重新创建主窗口。
示例:在 macOS 下,当点击 Dock 图标时,可以重新创建主窗口。
quit
触发时机:应用程序即将退出时。
作用:在应用程序退出之前执行最后的清理操作。
示例:在这个事件中可以销毁托盘或其他资源。
will-quit
触发时机:在应用程序即将退出时,但在 quit 事件之前。
作用:在应用程序退出之前执行一些清理或保存操作。
示例:在这个事件中可以执行一些清理或保存操作。
will-finish-launching
触发时机:在应用程序即将完成启动时。
作用:可以在此事件中执行一些在应用程序完全启动之前需要完成的操作。
示例:在这个事件中可以初始化一些启动时需要的资源。

const { app } = require('electron')
const { createMainWindow } = require('./windows/mainWindow')

app.on('ready', () => {
    createMainWindow()
})
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
    event.preventDefault()
    callback(true)
})
app.on('before-quit', () => {
    console.log('app before-quit')
})
app.on('window-all-closed', function () {
    console.log('window-all-closed')
})
app.on('activate', function () {
    console.log('activate')
})
app.on('quit', function () {
    console.log('quit')
    getTray() && getTray().destroy()
})
app.on('will-quit', function () {
    console.log('will-quit')
})
app.on('will-finish-launching', function () {
    console.log('will-finish-launching')
})

创建窗口

我们在desktop文件夹中创建一个windows文件夹,里面存放每个窗口的相关代码(我们项目中通常不止一个窗口),我们在windows文件夹中创建一个mainWindow.js文件,用于创建一个简单的窗口

// 在主进程中.
const { BrowserWindow } = require('electron')
const path = require('path')

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

// Load a remote URL
win.loadURL('http://localhost:8000/')

// Or load a local HTML file
win.loadFile(path.resolve(__dirname, '../../../build/index.html'))

其中loadURL用于加载一个服务器地址,运行后将会在窗口中显示该地址的内容,我们这里的http://localhost:8000/是代码运行的本地环境地址
loadFile是加载一个静态文件,该文件就是渲染层代码打包后的入口文件

应用运行

由于创建窗口需要在app.on('ready', () => {})中,因此我们可以把创建窗口封装成一个函数并导出,在app.on('ready', () => {})中执行,例如:
封装mainWindow.js

const { BrowserWindow, ipcMain } = require('electron')
const path = require('path')

const isDevelopment = process.env.NODE_ENV === 'development'
let mainWindow = null

function createMainWindow() {
    mainWindow = new BrowserWindow({
        width: 1160,
        height: 752,
        minHeight: 632,
        minWidth: 960,
        show: false,
        frame: false,
        title: 'Harbour',
        webPreferences: {
            nodeIntegration: true,
            preload: path.resolve(__dirname, '../utils/contextBridge.js')
        },
        icon: path.resolve(__dirname, '../assets/logo.png')
    })

    if (isDevelopment) {
        mainWindow.loadURL('http://localhost:8000/')
    } else {
        const entryPath = path.resolve(__dirname, '../../build/index.html')
        mainWindow.loadFile(entryPath)
    }

    mainWindow.once('ready-to-show', () => {
        mainWindow.show()
    })
}

module.exports = { createMainWindow }

代码解析:我们这里使用process.env.NODE_ENV的值判断当前的运行环境,这里的运行环境需要说明一下,当我们使用下面配置的npm run dev-electron运行时,该值为"development",当我们将渲染层代码打包后,使用npm run prod-electron运行时,该值为"production",然而当我们使用electron-builder打包出来的安装包运行时,该值不存在为undefined。因此只有当该值是"development"时我们才加载一个我们渲染层启动的服务地址,其他两种情况下我们都需要加载我们渲染层打包后的入口文件即:build目录下的index.html
我们在窗口触发"ready-to-show"时显示窗口是为了使加载时的白屏时间不被用户看到

index.js导入并在app.on('ready', () => {})中执行

const { app } = require('electron')
const { createMainWindow } = require('./windows/mainWindow')

app.on('ready', () => {
    createMainWindow()
})

此时我们就可以运行electron,我们在package.json中配置运行命令

{
    "scripts": {
        "dev-electron": "cross-env NODE_ENV=development electron main/index.js",
        "prod-electron": "cross-env NODE_ENV=production electron main/index.js",
    }
}

执行命令,启动开发环境

npm run dev-electron 

运行成功,出现如下窗口(窗口内部内容可自行定义)
image.png
使用npm run prod-electron命令可以启动生产环境,该生产环境指的是渲染层的功能代码使用webpack打包后的代码,使其渲染到窗口中。真正的生产环境应该是下面介绍的使用electron-builder打包后的应用程序,此时process.env.NODE_ENV为undefined

Electron应用打包

上面我们启动electron的应用都是使用的node_modules中的electron包,我们想要得到一个真正可以安装的安装包,还需要使用第三方打包工具进行打包,上面有提到过,我们将使用electron-builder打包成可安装的安装包。上面我们已经安装了electron-builder,下面我们需要在package.json中配置build属性来自定义安装配置。(限于自身设备问题,这里只介绍在Windows系统的打包配置,electron可以打包成各种安装包,使其可以在mac,Linux系统上运行,其他系统的配置可自行查阅资料。)下面我们介绍一下配置内容和各个配置含义。
package.json完整配置

{
  "name": "desktop",
  "productName": "Harbour",
  "version": "1.0.0",
  "description": "",
  "main": "main/index.js",
  "scripts": {
    "dev-electron": "cross-env NODE_ENV=development electron main/index.js",
    "prod-electron": "cross-env NODE_ENV=production electron main/index.js",
    "build-electron-win64": "electron-builder -w --x64"
  },
  "build": {
    "productName": "Harbour",
    "appId": "harbour.electron.app",
    "files": [
      "build/**/*",
      "main/**/*"
    ],
    "directories": {
      "output": "dist"
    },
    "nsis": {
      "oneClick": false,
      "allowElevation": true,
      "allowToChangeInstallationDirectory": true,
      "installerIcon": "./main/assets/logo.ico",
      "uninstallerIcon": "./main/assets/logo.ico",
      "installerHeaderIcon": "./main/assets/logo.png",
      "createDesktopShortcut": true,
      "createStartMenuShortcut": true,
      "shortcutName": "Harbour"
    },
    "win": {
      "icon": "./main/assets/logo.ico",
      "artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
      "target": "nsis"
    },
    "electronDist": "./electron"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "cross-env": "^7.0.3",
    "electron": "^26.1.0",
    "electron-builder": "^24.6.3"
  }
}

配置解释

  1. productName:指定了您的应用程序的产品名称,通常用于构建过程中生成的安装程序文件名等地方。
  2. appId:指定了您的应用程序的唯一标识符,这个值在打包和部署时会用到。
  3. files:指定打包时所需打包的文件
  4. directories.output:指定了输出目录的路径,即构建后的文件将会保存在 dist 目录中。
  5. nsis:指定了 NSIS(Nullsoft Scriptable Install System)打包的相关配置。
    1. oneClick:指定是否启用一键安装模式。
    2. allowElevation:是否允许提升权限进行安装。
    3. allowToChangeInstallationDirectory:是否允许用户更改安装目录。
    4. installerIcon:安装程序的图标文件路径。
    5. uninstallerIcon:卸载程序的图标文件路径。
    6. installerHeaderIcon:安装程序的头部图标文件路径。
    7. createDesktopShortcut:是否在桌面上创建快捷方式。
    8. createStartMenuShortcut:是否在开始菜单中创建快捷方式。
    9. shortcutName:创建的快捷方式的名称。
  6. win:指定了 Windows 平台的配置。
    1. icon:指定应用程序的图标文件路径。
    2. artifactName:定义生成的构建文件的命名规则模板。
    3. target:指定构建的目标平台,这里是 NSIS。
  7. electronDist:指定了预先下载的 Electron 包的路径。

特别注意
这里有几个需要特别注意的点:

  1. 首先我们用的logo.ico文件尺寸大小至少是256*256
  2. 由于打包时需要使用electron的相关包文件,为了提高打包速度,我们一般会提前下载与我们node_modules相同版本的.zip包,然后打包时使用electronDist指定打包用的文件目录,可以缩减打包时间
  3. 自定义artifactName,该名称就是打包后我们可安装的.exe可执行文件的名称
  4. electron-builder打包原理实际上是将package.json同目录的所有文件进行整体打包输出,如下图所示,在package.json同级目录下有一些文件夹我们是不需要进行打包的,其中dist下是我们上次打包输出的内容,electron是我们预下载的打包所需的.zip包,node_modules下面是我们开发时所用的依赖包,这些都不需要打包进去。因此我们需要指定我们打包时所需要打包的文件夹,此时就需要用到package.json里面build配置中的files属性,如上配置,我们只需要将build目录下的文件和main下面的文件打包即可。
  5. 这里的build目录下是渲染层的代码,main下面都是我们主进程的代码

image.png
打包后的内容
image.png
dist目录下就是打包生成的内容,其中第一个红框的Harbour.exe是可直接执行的文件,无需安装第二个红框中的.exe可执行文件就是可安装的文件,在文件夹中,双击即可进入安装流程。

Electron常用API详解

使用BrowserWindow创建窗口

创建窗口常用配置Option

在我们创建窗口时可以配置很多自定义配置,下面是一些常用配置及解析:

  1. width 和 height:用于设置窗口的初始宽度和高度。
  2. x 和 y:控制窗口的初始位置,以屏幕坐标为基准。
  3. fullscreen:布尔值,指定窗口是否以全屏模式启动。
  4. resizable:布尔值,控制用户是否可以调整窗口大小。
  5. minWidth 和 minHeight:指定窗口的最小宽度和最小高度。
  6. maxWidth 和 maxHeight:指定窗口的最大宽度和最大高度。
  7. frame:布尔值,指定是否显示窗口的外部框架(包括标题栏和控制按钮)。
  8. title:用于设置窗口的标题。
  9. icon:指定窗口的图标文件路径。
  10. backgroundColor:用于设置窗口的背景颜色。
  11. webPreferences:用于配置窗口的 Web 集成选项,例如启用 Node.js、预加载脚本等。
  12. nodeIntegration:指定是否在渲染进程中启用 Node.js 集成,允许在渲染进程中使用 Node.js API。
  13. contextIsolation:启用上下文隔离,将渲染进程的环境与主进程隔离开来,以提高安全性。
  14. preload:指定一个预加载的 JavaScript 文件的路径,该文件在渲染进程运行之前加载。
  15. devTools:指定是否允许在窗口中打开开发者工具。
  16. webSecurity:指定是否启用同源策略,限制页面对其他源的请求。
  17. alwaysOnTop:布尔值,控制窗口是否始终保持在顶部。
  18. fullscreenable:布尔值,指定窗口是否可以进入全屏模式。
  19. show:布尔值,指定创建窗口后是否立即显示。
  20. transparent:布尔值,指定窗口是否支持透明度。
  21. parent 和 modal:用于实现模态窗口的行为。
  22. closable:布尔值,指定用户是否可以关闭窗口。
  23. focusable:布尔值,指定窗口是否可以获得焦点。
  24. minimizable 和 maximizable:控制窗口是否可以最小化和最大化。
  25. skipTaskbar:布尔值,控制窗口是否在任务栏中显示。
const { BrowserWindow } = require('electron');

const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    x: 100,
    y: 100,
    fullscreen: false,
    resizable: true,
    minWidth: 400,
    minHeight: 300,
    frame: true,
    title: 'My Electron App',
    icon: '/path/to/icon.png',
    backgroundColor: '#ffffff',
    webPreferences: {
        nodeIntegration: true,
        contextIsolation: false,
        preload: 'path/to/preload.js',
        devTools: true,
        webSecurity: true
    },
    alwaysOnTop: false,
    fullscreenable: true,
    show: true,
    transparent: false,
    closable: true
});

mainWindow.loadFile('index.html');

窗口常用的实例事件

窗口有很多实例事件,使用window.on来监听,可以在这些事件触发时做一切操作例如下面是一些常用的实例事件:
close
触发时机:窗口即将关闭时触发,但实际关闭前。
作用:允许执行一些在窗口关闭前的清理操作,或者阻止窗口关闭。
closed
触发时机:窗口已经关闭时触发。
作用:通常用于释放资源或执行一些在窗口关闭后的最终操作。
resize
触发时机:窗口大小发生变化时触发。
作用:允许在窗口大小变化时执行一些操作。
move
触发时机:窗口位置发生变化时触发。
作用:允许在窗口位置变化时执行一些操作。
focus
触发时机:窗口获得焦点时触发。
作用:允许在窗口获得焦点时执行一些操作。
blur
触发时机:窗口失去焦点时触发。
作用:允许在窗口失去焦点时执行一些操作。
minimize
触发时机:窗口被最小化时触发。
作用:允许在窗口最小化时执行一些操作。
maximize
触发时机:窗口被最大化时触发。
作用:允许在窗口最大化时执行一些操作。
unmaximize
触发时机:窗口从最大化状态恢复时触发。
作用:允许在窗口从最大化状态恢复时执行一些操作。
ready-to-show
触发时机:当窗口完成初始化并且准备好显示时触发。
作用:允许在窗口已准备好显示之后执行一些操作。这通常在窗口加载内容后并准备好显示时触发,用于控制窗口的显示时机。
show
触发时机:当窗口被显示时触发。
作用:允许在窗口显示时执行一些操作。
hide
触发时机:当窗口被隐藏时触发。
作用:允许在窗口隐藏时执行一些操作。
enter-full-screen
触发时机:当窗口进入全屏模式时触发。
作用:允许在窗口进入全屏模式时执行一些操作。
leave-full-screen
触发时机:当窗口离开全屏模式时触发。
作用:允许在窗口离开全屏模式时执行一些操作。

// main.js

const { app, BrowserWindow } = require('electron');
let mainWindow;

function createMainWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true
    }
  });

  // 加载你的 HTML 文件
  mainWindow.loadFile('index.html');

  // 事件: 关闭
  mainWindow.on('close', (event) => {
    // 允许或阻止窗口关闭
    // event.preventDefault();
    // 执行清理操作
  });

  // 事件: 关闭后
  mainWindow.on('closed', () => {
    // 释放资源或执行最终操作
    mainWindow = null;
  });

  // 事件: 调整大小
  mainWindow.on('resize', () => {
    // 在窗口调整大小时执行操作
  });

  // 事件: 移动
  mainWindow.on('move', () => {
    // 在窗口移动时执行操作
  });

  // 事件: 获得焦点
  mainWindow.on('focus', () => {
    // 在窗口获得焦点时执行操作
  });

  // 事件: 失去焦点
  mainWindow.on('blur', () => {
    // 在窗口失去焦点时执行操作
  });

  // 事件: 最小化
  mainWindow.on('minimize', () => {
    // 在窗口最小化时执行操作
  });

  // 事件: 最大化
  mainWindow.on('maximize', () => {
    // 在窗口最大化时执行操作
  });

  // 事件: 还原
  mainWindow.on('unmaximize', () => {
    // 在窗口从最大化状态还原时执行操作
  });

  // 事件: 准备好显示
  mainWindow.on('ready-to-show', () => {
    // 在窗口准备好显示后执行操作
    mainWindow.show();
  });

  // 事件: 显示
  mainWindow.on('show', () => {
    // 在窗口显示时执行操作
  });

  // 事件: 隐藏
  mainWindow.on('hide', () => {
    // 在窗口隐藏时执行操作
  });

  // 事件: 进入全屏模式
  mainWindow.on('enter-full-screen', () => {
    // 在窗口进入全屏模式时执行操作
  });

  // 事件: 离开全屏模式
  mainWindow.on('leave-full-screen', () => {
    // 在窗口离开全屏模式时执行操作
  });
}

窗口常用的实例属性

窗口自身存在很多的实例属性,可以使我们获取到窗口的一些当前状态。下面是一些常用的实例属性。

  1. win.id - 窗口的唯一ID。
  2. win.webContents - 包含窗口网页内容的BrowserWindowProxy对象。
  3. win.devToolsWebContents - 用于开发者工具窗口的webContents。
  4. win.minimizable - 是否允许最小化窗口,默认为true。
  5. win.maximizable - 是否允许最大化窗口,默认为true。
  6. win.fullScreenable - 是否允许全屏窗口,默认为true。
  7. win.resizable - 是否允许改变窗口大小,默认为true。
  8. win.closable - 是否允许关闭窗口,默认为true。
  9. win.movable - 是否允许移动窗口,默认为true。
  10. win.alwaysOnTop - 是否永远置顶,默认为false。
  11. win.modal - 是否为模态窗口,默认为false。
  12. win.title - 窗口标题。
  13. win.defaultWidth/Height - 窗口默认宽高。
  14. win.width/height - 窗口当前宽高。
  15. win.x/y - 窗口左上角坐标。

窗口常用的实例方法

  1. win.loadURL(url)- 加载指定URL到窗口中,通常用于加载本地文件或远程网页。
  2. win.webContents.send(channel, ...args)- 在窗口之间发送异步消息。channel 是一个字符串,用于标识消息的类型,...args 是要传递的参数。
  3. win.show()- 显示窗口,通常与 hide() 方法配合使用。
  4. win.hide()- 隐藏窗口。
  5. win.close()- 关闭窗口
  6. win.minimize()- 最小化窗口
  7. win.maximize()- 最大化窗口
  8. win.restore()- 恢复窗口大小和位置。
  9. win.setSize(width, height[, animate])- 设置窗口的宽度和高度。
  10. win.setPosition(x, y[, animate])- 设置窗口的位置。
  11. win.getTitle()- 获取窗口的标题。
  12. win.setTitle(title)- 设置窗口的标题。
  13. win.setMenu(menu)- 设置窗口的菜单。
  14. win.setResizable(resizable)- 设置窗口是否可以改变大小。
  15. win.setAlwaysOnTop(flag[, level[, relativeLevel]])- 将窗口置顶。
  16. win.setMenu(null)- 隐藏窗口的菜单栏。
  17. win.setProgressBar(progress)- 设置窗口的任务栏进度条。
  18. win.focus()- 将窗口置于前台并获得焦点。
  19. win.isVisible()- 返回窗口是否可见。
  20. win.isFullScreen()- 返回窗口是否全屏。
  21. win.isMaximized()- 返回窗口是否最大化。
  22. win.webContents.executeJavaScript(code[, userGesture])- 在窗口的渲染进程中执行一段 JavaScript 代码。
  23. win.openDevTools([options])- 打开开发者工具。

创建右下角托盘

对于一个桌面应用来说,右下角的系统托盘必不可少,electron应用的系统托盘使用tray这个api实现,下面是封装的专门处理系统托盘的文件
systemTray.js

const { app, Tray, Menu } = require('electron')
const path = require('path')
const { getMainWindow, mainWindowIsExist } = require('./windows/mainWindow')

let tray = null
const iconPath = path.resolve(__dirname, './assets/logo.png')

function initTray() {
    tray = new Tray(iconPath)

    const contextMenu = Menu.buildFromTemplate([
        {
            label: '打开应用', click: () => {
                mainWindowIsExist() && getMainWindow().show()
            }
        },
        { label: '退出应用', click: () => { app.quit() } },
    ])
    
    tray.setToolTip('Harbour') // 设置鼠标悬停时显示的提示信息
    tray.setContextMenu(contextMenu)
    
    tray.on('click', () => {
        mainWindowIsExist() && getMainWindow().show()
    })
}

function getTray() {
    return tray
}

module.exports = { initTray, getTray }

代码解析

  1. iconPath获取托盘图标路径,这里注意一定要使用path.resolve生产绝对路径否则打包成安装包后会无法找到该文件导致报错
  2. Menu.buildFromTemplate是electron的一个方法,用来创建一个菜单,菜单的label是显示的内容,click是点击后触发的事件
  3. tray.setToolTip('Harbour')是用来设置鼠标悬停时显示的提示信息
  4. tray.setContextMenu(contextMenu)将使用Menu.buildFromTemplate创建出的菜单设置为托盘菜单
  5. tray.on('click', () => {}) 当点击托盘的时候触发的事件,我们这里是将mainWindowshow出来

初始化系统托盘
系统托盘的初始化需要在app.on('ready')之后,因此我们将初始化系统托盘的方法封装好导出,在app.on('ready')中执行

const { app } = require('electron')
const { createMainWindow } = require('./windows/mainWindow')
const { initTray, getTray } = require('./systemTray')

app.on('ready', () => {
    createMainWindow()
    initTray()
})

应用层和主进程通信(ipcMain,ipcRender)

应用层和主进程之间的通信流程是:

  1. 应用层使用ipcRender.send方法将事件及数据传递到主进程
  2. 主进程使用ipcMain.on或者ipcMain.once方法监听事件并获取数据
  3. 主进程使用ipcMain.removeListener移除事件监听或者ipcMain.removeAllListeners移除所有事件监听
  4. 主进程使用窗口实例的webContents.send方法将事件和数据传递到应用层
  5. 应用层使用ipcRender.on或者ipcRender.once监听事件并获取数据
  6. 应用层使用ipcRenderer.removeListener移除事件监听或者ipcRenderer.removeAllListeners移除所有事件监听

图解如下

将ipcRender,process注入到应用层

我们知道ipcMain和ipcRender都是electron的Api,要想在应用层使用ipcRender就需要先将其注入到应用层,在electron中使用contextBridge.exposeInMainWorld方法将electron的Api注入到应用层,注入之后我们就可以在应用层的window*问注入的属性。我们这里将ipcRender和process两个属性注入到应用层,分别用来实现通信和判断当前运行环境。
封装contextBridge.js文件

const { contextBridge, ipcRenderer } = require('electron')

/**
 * contextBridge.exposeInMainWorld的作用就是将主进程的某些API注入到渲染进程,
 * 供渲染进程使用(主进程并非所有的API或对象都能注入给渲染进程,需要参考文档)
 * ipcRenderer 渲染进程通过window.ipcRenderer调用
 */
contextBridge.exposeInMainWorld('ipcRenderer', {
    send: (channel, ...args) => {
        if (args?.length > 0) {
            ipcRenderer.send(channel, ...args)
        } else {
            ipcRenderer.send(channel)
        }
    },
    on: (channel, func) => {
        ipcRenderer.on(channel, func)
    },
    once: (channel, func) => {
        ipcRenderer.once(channel, func)
    },
    removeListener: (channel, func) => {
        ipcRenderer.removeListener(channel, func)
    },
    sendSync: (channel, ...args) => {
        if (args?.length > 0) {
            return ipcRenderer.sendSync(channel, ...args)
        } else {
            return ipcRenderer.sendSync(channel)
        }
    },
    invoke: (channel, ...args) => {
        try {
            return ipcRenderer.invoke(channel, ...args)
        } catch (error) {
            console.error(`Error invoking API: ${channel}`, error)
        }
    },
})

contextBridge.exposeInMainWorld('process', {
    NODE_ENV: process.env.NODE_ENV
})

这里我们将ipcRender的send,on,once,removeListener,sendSync,invoke方法及process.env.NODE_ENV注入到应用层,后续可在应用层进行使用
注意,该方法需要在应用层渲染时执行,因此我们刚好可以用到创建窗口中的option.webPreferences.preload来加载该文件,后续有案例代码。

应用层封装注入的Api

我们将ipcRender和process注入到应用层后,为了后期的维护我们可以将所有的方法再次进行封装,放在一个统一的文件中,
封装desktopUtils.ts

declare global {
    interface Window {
        ipcRenderer: {
            send: (...args: any[]) => void,
            on: (channel: string, listener: (...args: any[]) => void) => void,
            once: (channel: string, listener: (...args: any[]) => void) => void,
            removeListener: (channel: string, listener: (...args: any[]) => void) => void,
            sendSync: (...args: any[]) => any,
            invoke: (...args: any[]) => Promise<any>,
        },
        process: {
            NODE_ENV: 'development' | 'production'
        }
    }
}

type ArgsType = string | number | boolean | { [key: string]: any } | any[]

export const isDesktop = () => {
    return !!window.ipcRenderer
}

export const getProcessNodeEnv = () => {
    return window?.process.NODE_ENV
}

export const ipcRendererSend = (eventName: string, ...args: ArgsType[]) => {
    window.ipcRenderer?.send(eventName, ...args)
}

export const ipcRendererSendSync = (eventName: string, ...args: ArgsType[]) => {
    return window.ipcRenderer?.sendSync(eventName, ...args)
}

export const ipcRendererInvoke = (eventName: string, ...args: ArgsType[]) => {
    try {
        return window.ipcRenderer?.invoke(eventName, ...args)
    } catch (error) {
        console.error(`Error invoking IPC: ${eventName}`, error)
        return null
    }
}

export const ipcRendererOn = (eventName: string, listener: (...args: ArgsType[]) => void) => {
    window.ipcRenderer?.on(eventName, listener)
}

export const ipcRendererOnce = (eventName: string, listener: (...args: ArgsType[]) => void) => {
    window.ipcRenderer?.once(eventName, listener)
}

export const ipcRendererRemoveListener = (eventName: string, listener: (...args: ArgsType[]) => void) => {
    window.ipcRenderer?.removeListener(eventName, listener)
}

这里的isDesktop是用来判断当前是否是桌面端的,因为很多时候我们使用electron开发的桌面端应用需要兼容web端,由于应用层代码几乎相同,我们只需要在一些情况下特别处理桌面端的逻辑即可。由于web端的window上一定没有ipcRender这个属性,因此可以根据window.ipcRenderer来判断
getProcessNodeEnv是用来获取当前桌面端的运行环境的,这里可以返回当前是开发环境还是生产环境,如果是web端的话,直接用process.env.NODE_ENV即可判断

应用层发