Electron应用实践——前端该如何开发桌面应用

什么是 Electron

Electron(原名为Atom Shell)是GitHub开发的一个开源框架。是一个使用 JavaScript、HTML 和 CSS 构建跨平台的桌面应用程序框架。它基于 Node.js(后端) 和 Chromium(前端),被 Atom 编辑器和许多其他应用程序使用。Electron 兼容 Mac、Windows 和 Linux,可以构建出三个平台的应用程序。

为什么选择 Electron

有很多可以开发桌面应用的框架,比如TauriFlutterElectron等,

  • Tauri:打出来的包非常小,需要一定的 Rust 基础;
  • Flutter:一套代码通吃 web、iOS、Android、macOS、Windows、Linux 六大平台,需要学习新的语言 Dart;
  • Electron:纯 JavaScript 技术栈,生态非常成熟,前端同学快速上手,几乎无学习成本。

生态圈壮大,案例成熟且丰富

Electron生态很强大,有各种npm包和github库为开发应用程序提供解决方案。

Electron做桌面的案例也非常成熟了, Apps users love, built with Electron,其中包含很多很常用的应用,例如 Postman、VScode 等等。

前端工程师入门快

  • 基于 Node.js: 这就意味着,Node 这个大生态下的模块,Electron 都可以使用。
  • 跨平台: 可以同时开发 Web 应用和桌面应用,共享 UI、代码等资源,大大减少了工作量。

如何学习 Electron

Electron 是 Web 技术和 Node.js 技术的合体,对前端同学来讲非常容易上手。

Web 用于构建 UI 界面,这和平时我们用 React 或 Vue 开发项目没有任何区别,Electron 内置一个 chromium 浏览器,会启动一个渲染进程,把页面展示出来。

Node.js 则用于在主进程中调用 Electron 封装好的 API 来创建窗口、设置菜单、添加托盘、自定义协议、消息通知等。

第一章 工程搭建

第一步:项目初始化

安装以来

bash 复制代码
mkdir electron-demo
cd electron-demo
yarn init -y
yarn add electron --dev

安装时遇到 Electron无法从淘宝镜像下载安装,报错HTTPError Response code 404 (Not Found)的问题

项目启动

在 package.json 增加启动配置项

package.json 复制代码
"scripts": {
    "start": "electron ."
},

第二步:创建应用程序

在项目根目录下创建一个页面 index.html

index.html 复制代码
<!DOCTYPE html>
<html lang="en">
  <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>

想要将👆它加载进应用窗口中,可以通过electron提供的appBrowserWindow两个模块来实现。

  • app 模块,控制应用程序的生命周期。app.whenReady 等待应用程序就绪
  • BrowserWindow 模块,创建和管理应用程序窗口。

在根目录下创建入口文件index.js作为应用程序的入口文件,通过 loadFileindex.html加载进一个BrowserWindow实例中。

  • loadUrl(url) 用来加载 url 可以是远程地址 (例如 http://),也可以是 file:// 协议的本地HTML文件的路径.
  • loadFile(filePath) filePath 是一个与应用程序的根路径相关的HTML文件路径
index.js 复制代码
const { app, BrowserWindow } = require('electron')

app.whenReady().then(() => {
  // 等待应用程序就绪后再创建浏览器窗口
  // 只有app模块的 ready 事件被激发后才能创建浏览器窗口
  createWindow()
})

const createWindow = () => {
  // 创建一个 BrowserWindow 实例
  const win = new BrowserWindow()

  // 将 index.html 加载到一个新的 BrowserWindow 实例中
  win.loadFile('index.html')
}

此时,执行 yarn start 启动项目会弹出浏览器弹窗,加载的是 index.html 内容。

热更新

修改 package.json 中的 "start": "nodemon --watch index.js --exec electron ."

再次运行npm run start,当index.js内容变化时,就会自动重新执行electron .来重启应用。

第二章 开始实现功能

下面将主要介绍创建窗口、自定义协议、应用菜单、进程间通讯、消息通知几个常用模块。

1、创建窗口

通过 new BrowserWindow() 方法来创建一个窗口实例时,窗口属性 options 有很多:

  • width 窗口的宽度(以像素为单位)。 默认值为 800
  • height 窗口的高度(以像素为单位)。 默认值为 600
  • x 窗口相对于屏幕左侧的偏移量,默认值为将窗口居中
  • y 窗口相对于屏幕顶端的偏移量,默认值为将窗口居中
  • useContentSize width 和 height 将设置为 web 页面不包含边框的尺寸, 这意味着窗口的实际尺寸(包括窗口边框的大小)会稍微大一点。默认值为 false
  • center 窗口是否在屏幕居中,默认值为 false
  • minWidth minHeight 窗口的最小高度/高度,默认值为 0
  • maxWidth maxHeight 窗口的最大高度/高度,默认值不限
  • resizable 窗口是否可以调整大小,默认值为true
  • movable 窗口是否可移动,默认值为truemacOS Windows
  • minimizable 窗口是否可最小化,默认值为truemacOS Windows
  • maximizable 窗口是否可最大化,默认值为truemacOS Windows
  • closable 窗口是否可关闭,默认值为truemacOS Windows
  • focusable 窗口是否可以聚焦,默认值为true,在 Windows 中设置 focusable: false 也意味着设置了 skipTaskbar: true
  • alwaysOnTop窗口是否应始终位于其他窗口之上。默认值为false
  • fullscreen窗口是否应全屏显示。如果明确设置为false macOS 上的全屏按钮将被隐藏或禁用。默认值无
  • fullscreenable 窗口是否可以进入全屏模式。macOS 上表示最大化/缩放按钮是否可以切换全屏模式或最大化窗口,默认值为true
  • simpleFullscreen 在 macOS 上使用 pre-Lion 全屏。 默认值为 false,仅 macOS(没有试出来是什么功能)
  • skipTaskbar 是否在任务栏显示窗口,默认为false(没有试出来是什么功能)
  • hiddenInMissionControl 当用户切换到任务控制时是否应该隐藏窗口,仅 macOS(没有试出来效果)
  • title 窗口标题,默认为"Electron"<title> 标签设置的标题会覆盖这个属性
  • show 窗口在创建后是否显示,默认为true
  • frame 设置为 false 则会创建无边框窗口,默认为true
  • parent 指定父窗体,默认为null
  • modal 是否为模态窗体,只会当窗体是子窗体的时候,这个值才起作用。
  • acceptFirstMouse 点击 非活动窗口是否会穿透到 web contents,默认是false,仅 macOS
  • disableAutoHideCursor 是否在打字时隐藏光标,默认为false(没有试出来效果)
  • autoHideMenuBar 是否自动隐藏菜单栏,除非按了Alt键,默认为false(没有试出来效果)
  • enableLargerThanScreen 窗体是否能够比屏幕大,默认为falsee,仅 macOS
  • backgroundColor 窗体的背景颜色值,为十六进制数值,默认是#FFF
  • hasShadow 是否有阴影,默认为true
  • opacity 设置窗口的初始透明度,在 0.0(全透明)和 1.0(完全不透明)之间 ,Windows macOS
  • darkTheme 使用黑色主题,仅在 GTK+3的桌面环境工作,默认为false
  • transparent 是窗体透明,默认为false,在Windows上,仅在无边框窗口下起作用
  • type 窗体类型,默认是普通窗体
  • titleBarStyle 窗体标题栏的样式,默认为default
    • default macOS Windows 的标准标题栏
    • hidden 隐藏的标题栏,结果展示在完整大小的内容窗口。但在macOS内,标题栏样式仍然会暴露标准窗口左上方 的控制按钮("红绿灯")。
    • hiddenInset,隐藏的标题栏的另一种表现,比 hidden 距离窗口边缘稍微嵌入一点,仅 macOS
    • customButtonsOnHover,隐藏红绿灯,除非鼠标悬停在上面,experimental 实验性的,仅 macOS
  • trafficLightPosition 为无框窗口中的红绿灯按钮设置自定义位置,仅 macOS
  • roundedCorners 无边框窗口在 macOS 上,是否应该有圆角。默认值为 true。 属性设置为 false ,将阻止窗口是可全屏的,仅 macOS
  • thickFrame Windows 上的无框窗口使用WS_THICKFRAME 样式,会增加标准窗口框架。设置为 false 时将移除窗口的阴影和动画。默认值为 true。
  • vibrancy 增加一个振动效果到窗体上,仅 macOS
  • backgroundMaterial 设置 windows 窗口系统绘制的素材,可以设置auto, none, mica, acrylic or tabbed,仅 windows
  • zoomToPageWidth 默认是false。在macOS平台上,设置窗体上绿色按钮或者菜单栏上window-Zoom的菜单项行为。如果为true,则窗体将会扩展到适配网页页面内容的宽度。如果为false,则会扩展宽度到屏幕大小。这个值也将影响当调用 maximize() 方法的时候。
  • tabbingIdentifier macOS 选项卡组名称,允许使用原生选项卡打开窗口。Windows 中,有相同选项卡标识的将会组合在一起,这会添加一个原生新增选项卡按钮到你窗口的选项卡栏,同时 app 和窗口允许接收 new-window-for-tab 事件。
  • webPrefernces 设置网页功能设置 blog.csdn.net/qq_29069649...
创建无边框窗口

以下示例为打开一个初始位置在屏幕右上角 显示的,最小尺寸为 1100 * 900,无边框窗口:

main.js 复制代码
function createWindow() {
    // 获取屏幕的宽度
    const screenWidth = screen.getPrimaryDisplay().workAreaSize.width;
    // 定义窗口的宽高
    const width = 1100;
    const height = 900;
    // 计算窗口的初始 x 坐标(屏幕宽度 - 窗口宽度)
    const initialX = screenWidth - width;

    if (win) {
        win.setSize(width, height)
    } else {
        win = new BrowserWindow({
            width,
            height, // 设置窗口宽高
            x: initialX, // 设置窗口的初始位置为屏幕右边缘
            y: 0, // 设置窗口的初始位置为屏幕顶部
            resizable: false, // 不允许用户调整窗口大小
            minWidth: 800,
            minHeight: 600,
            titleBarStyle: 'hiddenInset', // 隐藏标题栏
            frame: false, // 隐藏标题栏
            enableLargerThanScreen: true, // 允许窗口大于屏幕
        })

        win.loadURL('https://juejin.cn/user/4476867080110957') // 加载页面
    }
}

app.whenReady().then(() => {
    createWindow();
})
  • 当设置 frame: false 打开一个frameless无边框窗口时,窗口是不可拖动的调整位置的,需要通过 CSS 设置 -webkit-app-region: drag; 来让指定区域可以拖动。-webkit-app-region: no-drag; 设置指定区域禁止拖动。
  • macOS中,titleBarStyle支持设置为customButtonsOnHover hiddenInset
  • 在windows系统中titleBarStylehidden,并且titleBarOverlaytrue或者对象时,应用窗口也会默认显示出window操作系统的窗口控件工具(最大化、最小化、关闭)
创建模态窗口
main.js 复制代码
const { BrowserWindow } = require('electron')

const top = new BrowserWindow()
const child = new BrowserWindow({
    parent: top,
    modal: true,
    show: false
})
child.loadURL('https://github.com')
child.once('ready-to-show', () => {
    child.show()
})
  • 模态窗口是禁用父窗口的子窗口
  • 要创建模态窗口,必须同时设置parentmodal属性

2、单实例运行

在默认情况下,Electron 应用就是多实例的。例如

  • 在 Windows 上,用户每双击一次 exe 就会开启一次应用;
  • macOS 上如果应用已经启动,双击应用程序并不会重新启动应用,但是用户可以通过右键应用程序,选择显示包内容,双击 Contents/MacOS/ 目录下的可执行文件,还是会启动一个新的实例。

如果想要应用最多只能创建一个实例,可以通过 requestSingleInstanceLock 抢占实例锁来实现。

main.js 复制代码
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
    app.quit()
}

requestSingleInstanceLock() 用于抢占实例运行锁,只有第一个启动的实例才返回 true,而一旦锁被强占之后,后续启动的其他实例再调用这个方法就会返回 false,此时执行 quit() 强制退出,来确保只有一个实例运行。

退出了第二个实例之后,在用户看来感觉像是没有唤醒应用,我们可以将已经启动的实例窗口显示到前台来,来优化这种体验。Electron 提供了 second-instance 事件来监听第二实例的启动行为,第一个实例可以在这里做出对应的行为,例如:将在后台的窗口显示出来,将最小化的窗口恢复。

main.js 复制代码
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
    app.quit()
} else {
    mainWindow.restore() // 从最小化窗口恢复
    mainWindow.show() // 从后台显示
}

3、自定义协议

桌面应用一般都有自己的协议,在浏览器地址栏输入应用协议头的链接,会弹出打开应用的弹窗,这就是通过注册自定义协议来实现的。

注册协议

给应用起一个唯一的协议名称,注册到系统中,这样就可以通过这个协议就可以唤醒应用了。Electron 是通过 setAsDefaultProtocolClient 方法将应用注册为协议(dog-clock://)的处理器。

main.js 复制代码
app.setAsDefaultProtocolClient('dog-clock');

注册完成后,当在浏览器中输入 dog-clock:// 会弹出打开应用的提示,点击「打开」 就可以启动并打开应用了,这种方式就是 scheme 唤起。

获取 scheme 参数

在通过 scheme 唤醒应用时可以携带一些参数,获取到参数并进行后续的处理。dog-clock:// 后边的参数用户可以完全自定义,例如模仿http地址query参数风格 dog-clock://helloworld?width=1280&height=700

在 macOS 和 Windows 上获取参数的方式不同。

  • macOS 通过监听 open-url 事件来获取 url 参数
  • windows 通过监听 second-instance 事件来获取参数

macOS

main.js 复制代码
app.on('open-url', (_, url) => {
    console.log('open-url: ', url); // open-url:  dog-clock://helloworld?width=1280&height=700
})

windows 平台 通过 scheme 启动应用的时候,会作为启动参数传递给应用程序,通过 process.argv 来拿到所有参数,格式是数组,其中有一项就是 url

main.js 复制代码
const url = process.argv.find(v => v.startsWith('dog-clock://'))
if (url) {
    console.log('通过 scheme 唤起', url)
}

如果是再次唤醒应用,可以监听 second-instance 事件,其中第二个参数和 process.argv 类似,其中也包含着 url 参数

main.js 复制代码
app.on('second-instance', (event, argv, workingDirectory) => {
    const url = argv.find(v => v.startsWith('dog-clock://'))
    if (url) {
        console.log('通过 scheme 唤起', url)
    }
}

4、设置菜单

从以下三种类型菜单分别介绍:应用内菜单、托盘菜单、右键菜单

设置应用内菜单项

Electron 中的 Menu 模块封装了菜单相关的各种方法,其中 buildFromTemplate 用于创建原生菜单,传入一个 MenuItem 对象数组,

main.js 复制代码
// 菜单栏模板
const menuBar = [{
    label: app.name,
    submenu: [
        { label: '关于', role: 'about' },
        {
            label: '检测更新',
            click: () => { checkUpdate() },
            accelerator: 'Command+Ctrl+U',
        },
        { type: 'separator' },
        { label: '隐藏 Dog Clock', role: 'hide' },
        { label: '隐藏其他应用', role: 'hideOthers' },
        { role: 'unhide' },
        { type: 'separator' },
        { label: '退出', accelerator: 'Command+Q', role: 'quit' }
    ]
}];

app.whenReady().then(() => {
    // 构建菜单项
    const menu = Menu.buildFromTemplate(menuBar);
    // 设置一个顶部菜单栏
    Menu.setApplicationMenu(menu);
})

MenuItem 对象有很多属性,其中最常用的有以下几个:

  • label 设置选项的标签名称
  • accelerator 设置快捷键
  • role 表示菜单项的角色,是Electron预定义的菜单项。如果设置了 role 用户自己设置的 click 会被忽略掉更多role请参考
  • type 表示菜单项的类型 可以是 normal, separator, submenu, checkboxradio
  • enabled 表示是否启用该项,这个选项可以动态的修改
  • click 设置点击菜单后触发的方法
  • submenu:定义子菜单

点击查看MenuItem详情

设置系统菜单在 macOS 和 windows 系统中有略微差别:

  • macOS 会自动设置第一个菜单,其菜单名与应用名相同,可以手动为 macOS 添加一个空白菜单
css 复制代码
    if (process.platform === 'darwin') {
        menuTemplate.unshift({ label: '' })
    }
  • macOS 系统上,应用所有窗口都共享左上角的菜单
  • Windows 和 Linux 系统是可以为 BrowserWindow 单独设置菜单
设置托盘菜单

在程序启动时,可以借助 Tray 模块来实现将应用程序加入到系统托盘,调用 setContextMenu 方法注册托盘菜单。

main.js 复制代码
// 确保路径和格式正确,建议使用 16x16 像素或 32x32 像素的图标
const iconPath = path.join(__dirname, '../../build/icons/32x32.png')

// 实例化 tray 对象,需要在托盘中显示的图标url作为参数
const tray = new Tray(iconPath);

// 点击托盘图标的事件,根据窗口的显示状态切换主窗口的显示和隐藏
tray.on('click', () => {
    if(win.isVisible()){
        win.hide()
    }else{
        win.show()
    }
})

const contextMenu = Menu.buildFromTemplate([
    { label: '退出', click: () => { app.quit() } }
])

// 设置右键托盘图标时的菜单,这里设置为只有一个退出选项
tray.on('right-click', () => {
    tray.popUpContextMenu(contextMenu)
})

// 给托盘对象设置菜单
tray.setContextMenu(contextMenu)
// 设置鼠标移到托盘中的图标上时显示的文本
tray.setToolTip('这是一个小狗闹钟');

通过 tray.setContextMenu(contextMenu) 方式设置托盘菜单的话,无论点击还是右键都会显示设置的菜单。

win.isVisible() 返回boolean 表示窗口是否在应用程序前台对用户可见

设置右键菜单

在渲染页面中触发右键时,通知主进程弹出【复制】菜单。后面会详细介绍进程之间的通讯。

通过 MenubuildFromTemplate 创建应用内菜单后,调用 popup 方法将菜单弹出。popup 接收以下参数:

  • window:指定窗口(默认是当前聚焦的窗口)
  • x:菜单位置横坐标(相对于窗口的 x 轴偏移,默认是鼠标位置的横坐标)
  • y:菜单位置纵坐标(相对于窗口的 y 轴偏移,默认是鼠标位置的纵坐标)
  • callback:菜单关闭回调函数
index.html 复制代码
const { ipcRendererSend } = window.electron;

window.addEventListener("contextmenu", (e) => {
    e.preventDefault(); 
    ipcRendererSend('mainWindow:contextMenu', e)
});
main.js 复制代码
ipcMain.on('mainWindow:contextMenu', (_, arg) => {
    const contextMenu = Menu.buildFromTemplate([
        { label: '复制', role: 'copy' }
    ]);
    contextMenu.popup({
        // window: BrowserWindow.getFocusedWindow(),
        callback: () => { console.log('menu closed callback') },
    });
})
Dock 菜单(macOS)

dock 栏菜单项也是通过 buildFromTemplate 来创建的,调用 Dock 模块提供的 dock.setMenu 方法来设置。

main.js 复制代码
const menu = Menu.buildFromTemplate(menuTemplate)
app.dock.setMenu(menu)

5、进程间通信

Electron 应用程序区分主进程和渲染进程,进程间通信就是IPC(Inter-Process Communication)。

进程之间的通讯主要是通过ipcRendereripcMain这两个模块实现的,其中ipcRenderer是在渲染进程中使用,ipcMain在主进程中使用。

如何在渲染进程中引入electron 的 ipcRenderer 模块呢?通过Electron窗口的preload方法引入预加载脚本。预加载(preload) 脚本先于网页内容开始加载,但执行在渲染器进程中。可以在 BrowserWindow 构造方法中的 webPreferences 选项中附加到主进程。

main.js 复制代码
const { BrowserWindow } = require('electron')
// ...  
const win = new BrowserWindow({
    webPreferences: {
        preload: path.join(__dirname, 'preload.js')
    }
})
// ...

预加载脚本与浏览器共享同一个全局 Window 接口,并且可以访问 Node.js API,所以它在全局 window 中暴露任意 API 来给网页使用。

上下文隔离

上下文隔离功能将确保您的 预加载脚本 和 Electron 的内部逻辑运行在 webcontent网页之外的独立的上下文环境里。这样有助于阻止网站访问 Electron 的内部组件,以及预加载脚本中访问权限较高的API。

main.js 复制代码
win = new BrowserWindow({
    webPreferences: {
        contextIsolation: false,
        preload: path.join(__dirname, 'preload.js')
    },
});

上下文隔离禁用 直接在全局的 window 中暴露任意属性。

preload.js 复制代码
// contextIsolation: false
window.myAPI = {
    desktop: true,
    doAThing: () => {
        console.log('I did a thing')
    }
}

上下文隔离启用 上下文隔离启用时需要通过contextBridge 模块可以用来安全地从独立运行、上下文隔离的预加载脚本中暴露 API 给正在运行的渲染进程。

preload.js 复制代码
const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('myAPI', {
    desktop: true,
    doAThing: () => {
        console.log('I did a thing')
    }
})

渲染进程直接调用 无论上下文隔离是否开启,渲染进行的调用方式是相同的。

index.html 复制代码
console.log(window.myAPI)
window.myAPI.doAThing()
渲染进程 到 主进程(单向)

上面我们创建过一个无边框的窗口,没有了系统的控制按钮,关闭窗口的功能就需要我们自行开发。当点击页面上的关闭按钮时,希望可以隐藏应用:

  1. 通过预加载脚本preload暴露ipcRendererSend: ipcRenderer.send
  2. 渲染进程向主进程发送 remindWindow:close 事件ipcRendererSend('remindWindow:close')
  3. 主窗口通过 ipcMain.on 监听 remindWindow:close 事件
preload.js 复制代码
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electron', {
    ipcRendererSend: (channel, data) => {
        ipcRenderer.send(channel, data);
    }
})
index.html 复制代码
const { ipcRendererSend } = window.electron;
const closeDom = document.querySelector('#control-buttons-close')
closeDom.addEventListener('click', (e) => {
    ipcRendererSend('remindWindow:close')
})
main.js 复制代码
ipcMain.on('remindWindow:close', () => {
    console.log('remindWindow:close: ');
    win.close();
});
主进程 到 渲染进程
  1. 主进程通过渲染进程的 webContents 实例向渲染进程发送消息。用法和渲染进程的 ipcRenderer.send 类似。
  2. 渲染进程通过预加载脚本暴露的 ipcRendereReceive: ipcRenderer.on 来监听主进程发来的消息
main.js 复制代码
win.loadFile(path.join(__dirname, '../index.html'))
win.webContents.send('message:something', '你好!我是主进程!')
preload.js 复制代码
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electron', {
    ipcRendereReceive: (channel, func) => {
        ipcRenderer.on(channel, (event, ...args) => func(...args));
    }
})
index.html 复制代码
ipcRendereReceive('message:something', (message) => {
    console.log('message:something: ', message);
    document.querySelector('.something').innerHTML = 
        `主进程发来的消息:${message}</span>`
})
渲染进程 到 主进程 (双向)

使用 invokehandle 来实现渲染进程和主进程的双向通讯。

  1. 渲染器进程通过 invoke 发送消息并等待主进程的响应
  2. 主进程通过 handle 响应消息,并返回响应结果
preload.js 复制代码
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electron', {
    ipcRendereInvoke: (channel, args) => {
        ipcRenderer.invoke(channel, args);
    }
})
index.html 复制代码
document.getElementById('get-windows').addEventListener('click', (e) => {
    ipcRendereInvoke('mainWindow:windowsCount').then((count) => {
        document.querySelector('.windows-count').innerHTML = count;
    })
})
main.js 复制代码
ipcMain.handle("mainWindow:windowsCount", (_) => {
    const windows = BrowserWindow.getAllWindows()
    return windows.length;
});
渲染进程 到 渲染进程

渲染进程之间通讯需要通过主进程作为中间者。例如,通知所有窗口改变主题,可以通过 BrowserWindow.getAllWindows 获取全部窗口实例,win.webContents.send 向所有窗口发送消息

main.js 复制代码
ipcMain.handle("mainWindow:changeTheme", (_, type) => {
    const windows = BrowserWindow.getAllWindows();
    windows.forEach(win => {
        nativeTheme.themeSource = type;
        win.webContents.send('message:changeTheme', nativeTheme.themeSource);
        // 不通知消息发送者窗口
        // if (win.webContents !== event.sender) {}
    });

    return nativeTheme.themeSource;
});

6、消息通知

每个操作系统都有自己的机制向用户显示通知,提供了 Notification 可以用来实现。

系统通知可以在电脑桌面弹出新消息通知,一般展示在电脑桌面的右上角。

主进程中显示通知
mail.js 复制代码
function showNotification () {
    new Notification({
        title: '小狗提醒您!',
        body: '现在时间8:00\n时间不早了,快回家去撸狗!',
        silent: true, // 系统默认的通知声音
        icon: path.join(__dirname, '../../build/icons/32x32.png'), // 通知图标
    }).show()
}

app.whenReady().then(showNotification)
在渲染进程中显示通知

通知可以直接在渲染进程中使用 Web Notifications API 显示。

render.js 复制代码
const openNotificationDom = document.getElementById('open-new-notification')
const outputDom = document.getElementById('output')

openNotificationDom.addEventListener('click', () => {
    new window.Notification(
        '小狗提醒您!',
        { body: '现在时间8:00\n时间不早了,快回家去撸狗!' }
    ).onclick = () => {
        outputDom.innerText = '收到,正在飞奔回家🏃'
    }
})

系统通知有一些局限性

  • 系统通知本身受系统设置控制,电脑系统设置禁止弹出通知,那么用户将无法收到通知信息
  • 样式单一,只有系统自带的样式
  • macOS 与window 有一些不支持的功能,new Notification([options])

如果想自定义消息通知,可以通过使用 BrowserWindow() 窗口来实现。设置好自定义通知的尺寸、位置、以及视图样式。

待解决问题

1、【已解决】macOS 系统菜单第一项设置label 但是显示还是【Electron】

以下方式均不生效:

  • package.json 中设置 name 和 productName
  • app.setName('Dog-Clock') 修改appName
  • Menu.setApplicationMenu([{label: app.name]) 修改默认菜单

可以手动为 macOS 添加一个空白菜单

2、【已解决】macOS 下设置启动图标

需要通过 app.dock.setIcon(iconPath) 方式设置

3、【已解决】win.loadFile 加载本地 html 页面的时候 style 标签不生效

需要给html文件设置内容安全协议:

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline';">

第三章 打包应用

Electron Builder

安装依赖

npm i electron-builder --D

配置选项

设置配置项有两种方式:

  • 第一种直接在package.json中添加build选项进行配置;
  • 第二种是在根目录下创建文件electron-builder.yml。通常使用的还是直接在package.json中进行配置。
package.json 复制代码
    "build": {
        "appId": "com.dog-clock",
        "directories": { // 输入输出目录相关的配置项
          "output": "dist", // 打包生成的目录,默认是dist
          "buildResources": "build" // 指定打包需要的静态资源,默认是build
        }
    },
    "scripts": {
        "build": "electron-builder",
    }

package.jsonscripts添加指令 "pack": "electron-builder"

运行打包命令 npm run pack

electron-builder 会自动识别当前的操作系统,打出系统对应的安装包。

  • Windows操作系统生成exe\msi;
  • Mac操作系统生成dmg

下面是常用的配置项

  • appId:应用 id
  • productName:应用名 "dog-clock"
  • directories:输入输出目录相关的配置项
    • buildResources: 指定打包需要的静态资源,默认是build
    • output: 打包生成的目录,默认是dist。用来放置的是打包生成的各种文件
  • files:用于指定哪些文件和文件夹应该被打包到最终的应用程序中
  • mac:macOS 系统下的专属配置
    • target: 安装包的格式,默认是"dmg"和"zip"
  • dmg:macOS 系统下 dmg 安装包配置项
    • background:安装窗口背景图
    • icon:安装图标
    • iconSize:图标的尺寸
    • window:安装窗口的大小,{"width": 540,"height": 380}
  • win:WindowsOS 系统下的专属配置
    • target: 安装包的格式,默认是"nsis"
    • icon:安装图标
  • nsis:windowsOS 系统下 nsis 安装包配置项
    • oneClick:是否一键安装
    • language:安装语言,2052对应中文
    • allowToChangeInstallationDirectory: 允许用户选择安装目录,默认为false
json 复制代码
"build": {
    "appId": "com.dog-clock",
    "productName": "dog-clock",
    "directories": {
        "output": "dist",
        "buildResources": "build"
    },
    "mac": {
        "target": ["dmg","zip"]
    },
    "dmg": {
        "background": "public/setup_background.jpg",
        "icon": "build/icons/icon.icns",
        "iconSize": 180,
        "window": {
            "width": 540,
            "height": 380,
        }
    },
    "win": {
        "target": ["msi","nsis"],
        "icon": "build/icons/icon.icns"
    },
    "nsis": {
        "oneClick": false,
        "language": "2052",
        "perMachine": true,
        "allowToChangeInstallationDirectory": true
    }
}

生成应用图标

准备一张png图片,借助 electron-icon-builder生成不同操作系统需要的不同尺寸的图标。Mac对应的格式为icns,Windows对应的格式为ico

  1. 准备一张正方形的图片icon.png放在项目根目录的public文件夹中。
  2. 安装依赖 npm i electron-icon-builder --D
  3. 添加命令 "build-icon": "electron-icon-builder --input=./public/icon.png --output=build --flatten"

打包工具分为主要有两个

  • 官方提供的 Electron PackagerElectron Forge:这两个经常是配合在一起使用,因为 Electron Packager 只是将应用打包成可执行程序,而 Electron Forge 会继续将其打包成安装程序。

  • 社区提供的 Electron Builder:这是目前最流行的打包工具,配置简单,开箱即用,使用也更广泛。覆盖 Windows、macOS 和 Linux 平台,并支持多种打包格式,集成了自动更新和代码签名的功能。下面就使用electron-builder来进行打包。

查看哪些文件被打包进app中

在打包后的文件夹中,有一个app.asar压缩包,用来存放 Electron 应用程序的主业务文件,可以使用asar工具将app.asar解压查看都有哪些文件被打包了。

  1. 切换app.asar所在目录 cd ./dist/mac-arm64/dog-clock.app/Contents/Resources/
  2. 解压文件 npx asar extract app.asar ./app-folder
  3. 哪些内容进行打包可以通过配置 package.json 中的 files 字段来指定

第四章 发布与更新

Electron应用实践------发布与更新

相关推荐
Domain-zhuo4 分钟前
Git和SVN有什么区别?
前端·javascript·vue.js·git·svn·webpack·node.js
雪球不会消失了9 分钟前
SpringMVC中的拦截器
java·开发语言·前端
李云龙I19 分钟前
解锁高效布局:Tab组件最佳实践指南
前端
m0_7482370524 分钟前
Monorepo pnpm 模式管理多个 web 项目
大数据·前端·elasticsearch
JinSoooo26 分钟前
pnpm monorepo 联调方案
前端·pnpm·monorepo
m0_7482449635 分钟前
【AI系统】LLVM 前端和优化层
前端·状态模式
明弟有理想35 分钟前
Chrome RCE 漏洞复现
前端·chrome·漏洞·复现
平行线也会相交36 分钟前
云图库平台(二)前端项目初始化
前端·vue.js·云图库平台
shimmer00837 分钟前
抖音小程序登录(前端通过tt.login获取code换取openId)
前端·小程序·状态模式
嘤嘤怪呆呆狗1 小时前
【开发问题记录】使用 Docker+Jenkins+Jenkins + gitee 实现自动化部署前端项目 CI/CD(centos7为例)
前端·vue.js·ci/cd·docker·gitee·自动化·jenkins