从0到1构建跨平台Electron应用,这篇文章就够了

简介: Electron是一个可以直接开发构建跨平台应用的库,简单、快捷。《Electron从0到1构建跨平台应用》这篇文章,我摘录了我自己在真实项目中,从开发到生成安装包的要点。

上一篇介绍了《Electron快速入手,拥有自己的第一个桌面应用 》,刚开始学习Electron的小伙伴可以先看下上一篇文章,方便快速入手。

但是,Electron的原理及一些其他功能开发,是如何实现的呢?

简介

Electron 基于 Chromium 和 Node.js, 让你可以使用 HTML, CSS 和 JavaScript 构建桌面端应用。

这篇文章摘录了我自己在真实项目中的要点问题

可能有的小伙伴会问,为什么不说一下 Vue、React 或者其他的库,如何与 Electron 结合开发?

这是因为 Vue 或者 React 等其他库都是 UI 页面,可以单独独立开发,只需要在代码打包压缩后,使用 BrowserWindow 中的 loadURL 或者 loadFile 加载即可。

Electron核心

Electron实现跨平台桌面应用程序的原理

image.png

通过集成浏览器内核,使用前端的技术来实现不同平台下的渲染,并结合了 ChromiumNode.js 和用于调用系统本地功能的 API 3大板块:

  1. Chromium:提供强大的 UI 渲染能力,用于显示网页内容。
  2. Node.js:主要用于本地文件系统和操作系统,提供 GUI 的操作能力,如path、fs、crypto 等模块。
  3. Native API:为 Electron 提供原生系统的 GUI 支持,使用 Electron Api 可以调用原生应用程序接口。

Electron主要核心点

Electron其实很简单,基本都是api,我自己整理了主要核心点有进程间通信appBrowserWindowApplication 4个方面:

  1. 进程间通信:处理主进程与渲染进程的双向通信。

  2. app:应用及应用生命周期。贯穿桌面端应用整个生命周期,虽然用的不多,但是很重要。

  3. BrowserWindow:浏览器窗口。大家可以理解成每一个 BWindow 都是一个独立的浏览器,用于加载渲染我们的前端代码。

  4. Application:应用程序功能点。为应用提供更佳完善的功能点,如程序坞、菜单......

从这4个主要核心点入手,开发人员会更快进入开发状态。

进程间通信

Electron 中有2个类型的进程,一种是主进程,另外一种是渲染进程。

而 ipcMain 代表了从主进程到渲染进程的异步通信; ipcRenderer 则代表了从渲染器进程到主进程的异步通信。

ipcMain与ipcRenderer

Electron 会存在多个进程,那么多进程间如何实现通信呢?

image.png

注意:不建议使用同步通信,这会对应用整体性能产生影响。

注意:进程通信所传参数只能为原始类型数据和可被 JSON.stringify 的引用类型数据。故不建议使用 JSON.stringify。

多进程间的通信,无论有多少个渲染进程,都遵循“主进程与渲染进程通信”。

渲染进程之间不能直接通信。

同时 Electron 提供了异步同步Promise 通信方式。

异步

通过 event.reply(...) 将异步消息发送回发送者。

event.reply 会自动处理从非主 frame 发送的消息,建议使用 event.sender.send 或者 win.webContents.send 把消息发送到主 frame。

主进程Main

const {
   
    ipcMain } = require('electron')
// 接收renderer的数据
ipcMain.on('asynchronous-message', (event, arg) => {
   
    
    console.log(arg) // 传递的数据 ping
    // event.reply('asynchronous-reply', 'pong') // 发送数据到渲染进程
    event.sender.send('asynchronous-reply', 'pong')
})

渲染进程Renderer

const {
   
    ipcRenderer } = require('electron')
// 接收main的数据
ipcRenderer.on('asynchronous-reply', (event, arg) => {
   
    
    console.log(arg) // prints "pong"
})
// 发送数据到主进程
ipcRenderer.send('asynchronous-message', 'ping')

同步

也可以设置同步接收(不建议使用),通过 event.returnValue 设置

主进程main

ipcMain.on('synchronous-message', (event, arg) => {
   
   
    event.returnValue = 'sync'
})

渲染进程renderer

const syncMsg = ipcRenderer.sendSync('synchronous-message', 'ping');
console.log(syncMsg)

ipcMain.once(channel, listener)只监听实现一次。

ipcMain.removeListener(channel, listener)删除某一指定的监听。

ipcMain.removeAllListeners([channel])移除多个指定 channel 的监听器,无参移除所有。

Promise

进程发送消息,并异步等待结果

ipcMain 提供 handle,ipcRenderer 提供了 invoke。

// 渲染进程
ipcRenderer.invoke('some-name', someArgument).then((result) => {
   
   
    // ...
})

// 主进程
ipcMain.handle('some-name', async (event, someArgument) => {
   
   
    const result = await doSomeWork(someArgument)
    return result
})

app

Electron 中 app 模块,是用来控制应用程序的事件生命周期,同时也提供了管理应用程序的事件方法。

注意: 某些方法仅在特定的操作系统上可用,如 app.hide() 只在 macos 可用。

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

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

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

app.on('window-all-closed', function () {
   
   
  if (process.platform !== 'darwin') app.quit()
})

app的生命周期

可以让我们在每个事件生命周期做出相应的处理,如监听初始化完成后创建窗口、监听应用被激活后聚焦窗口等。

1、ready

应用完成初始化时触发,这个是重点。因为Electron中很多都是需要初始化完成后,才可以执行,如 menu 、窗口等。

为避免出现错误,建议所有的操作都在ready之后

const {
   
    app } = require('electron')
app.on('ready', () => {
   
   
    // create menu
    // create browserWindow
})

可以通过 app.isReady() 来检查该事件是否已被触发。

若希望通过Promise实现,使用 app.whenReady()

2、第二个实例创建

开发中需要特别注意第二个实例(即第二个应用)创建时的处理。

因为 Electron 在一些情况下会触发第二个实例的创建,如程序坞对激活的默认操作等。

所以在 Electron 的应用即将要运行第二个实例时,处理方案一般是聚焦到主窗口。

const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
   
   
    app.quit();
} else {
   
   
    app.on('second-instance', (event, commandLine, workingDirectory) => {
   
   
        let win = mainWindow; // 获取到主窗口mainWindow
        if (win) {
   
   
            if (win.isMinimized()) {
   
    // 窗口最小化
                win.restore(); // 窗口从最小化状态恢复
            }
            if (!win.isVisible()) {
   
    // 窗口不可见
                win.show(); // 窗口显示
            }
            win.focus(); // 窗口聚焦
        }
    });
}

第二个实例执行时,会在第二个实例中触发 requestSingleInstanceLock 事件。

requestSingleInstanceLock 是用来判断当前应用程序实例是否成功取得了锁。如果它取得锁失败,代表此时另一个应用实例已经取得了锁并且仍旧在运行,当前的应用程序实例就需要立即退出。

而此时 second-instance 这个事件将在你的应用程序的首个实例中触发。

3、当应用被激活时(macos)

app.on('activate', (event, webContents, details) => {
   
   
    // 聚焦到最近使用的窗口
});

触发此事件的情况很多:
首次启动应用程序、尝试在应用程序已运行时或单击应用程序的坞站任务栏图标时重新激活它。

4、当所有窗口关闭,退出应用

const {
   
    app } = require('electron')
app.on('window-all-closed', () => {
   
   
    app.quit()
})

5、渲染进程进程奔溃

app.on('render-process-gone', (event, webContents, details) => {
   
   
    // 定位原因,可通过 log 记录;也可以做重启 retry 操作,切记限制count
    // webContents.reloadIgnoringCache(); // 忽略缓存强制刷新页面
});

details Object

  • reason string - 渲染进程消失的原因。 可选值:

    • clean-exit - 以零为退出代码退出的进程
    • abnormal-exit - 以非零退出代码退出的进程
    • killed - 进程发送一个 SIGTERM,否则是被外部杀死的。
    • crashed - 进程崩溃
    • oom - 进程内存不足
    • launch-failed - 进程从未成功启动
    • integrity-failure - 窗口代码完整性检查失败
  • exitCode Integer - 进程的退出代码,除非在 reasonlaunch-failed 的情况下, exitCode 将是一个平台特定的启动失败错误代码。

6、应用退出

app.on('will-quit', (event, webContents, details) => {
   
   
    // timer 或者 关闭第三方的进程
});

app常用事件方法

通过操作 app 应用程序的事件方法,来控制应用程序,如重启、关闭等。

const {
   
    app } = require('electron')
app.relaunch({
   
    args: process.argv.slice(1).concat(['--relaunch']) }) 
app.exit(0)

1、重启应用

app.relaunch([options])

relaunch 被多次调用时,多个实例将会在当前实例退出后启动。

2、所有窗口关闭

app.exit(0)

所有窗口都将立即被关闭,而不询问用户

3、当前应用程序目录

app.getAppPath()

可以理解成获取应用启动(代码)目录

4、设置 "关于" 面板选项

image.png

app.setAboutPanelOptions({
   
   
    applicationName: 'demo',
    applicationVersion: '0.0.1',
    version: '0.0.1'
});

5、注意,部分事件存在着系统的区别

// 只适用于 macOS
app.hide() // 隐藏所有的应用窗口,不是最小化.

BrowserWindow

const {
   
    BrowserWindow } = require('electron')

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

// 加载地址
win.loadURL('https://github.com')

// 加载文件
win.loadFile('index.html')

自定义窗口

可以通过自定窗口 options,实现自定义样式,如桌面窗口页面、桌面通知、模态框。。。

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

父子窗口

拖动父窗口,子窗口跟随父窗口而动。

const top = new BrowserWindow()
const child = new BrowserWindow({
   
    parent: top })

注意:child 窗口将总是显示在 top 窗口的顶部。

win.setParentWindow(parent)给窗口设置父窗口,null取消父窗口
win.getParentWindow()获取窗口的父窗口
win.getChildWindows()获取所有子窗口

显示与隐藏

窗口可以直接展示也可以延后展示。但每次执行 show,都会将当前桌面的焦点聚焦到执行show的窗口上。

const win = new BrowserWindow({
   
   
    show: true, 
})

在加载页面时,渲染进程第一次完成绘制时,如果窗口还没有被显示,渲染进程会发出 ready-to-show 事件。在此事件后显示窗口将没有视觉闪烁:

const win = new BrowserWindow({
   
    show: false })
win.once('ready-to-show', () => {
   
   
    win.show()
})

win.hide()窗口隐藏,焦点消失

win.show()窗口显示,获取焦点

win.showInactive()窗口显示,但不聚焦于窗口(多用于当其他窗口需要显示时,但是不想中断当前窗口的操作)

win.isVisible()判断窗口是否显示

Bounds

1、设置窗口的size、position

setBounds(bounds[, animate]):同时设置 size、position,但是同时也会重置窗口。

setSize(width, height[, animate]): 调整窗口的宽度和高度。

setPosition(x, y[, animate]):将窗口移动到x 和 y。

注意:animate只在macOS上才会生效。

// 设置 bounds 边界属性
win.setBounds({
   
    x: 440, y: 225, width: 800, height: 600 })
// 设置单一 bounds 边界属性
win.setBounds({
   
    width: 100 })
win.setSize(800, 600)
win.setPosition(440, 225)

2、获取窗口size、position

getBounds()获取窗口的边界信息。

getSize()获取窗口的宽度和高度。

getPosition()返回一个包含当前窗口位置的数组

center()将窗口移动到屏幕中央(常用)。

3、常见问题

win.setSize如果 width 或 height 低于任何设定的最小尺寸约束,窗口将对齐到约束的最小尺寸。

win.setPosition有的电脑机型存在兼容问题,执行一次win.setPosition(x,y)不会生效,需要执行两次。

Application

应用包含了很多程序工具,如 Menu、Tray...

Menu

创建原生应用菜单和上下文菜单。

image.png

在mac中,菜单展示在应用内;而 windows 与 linux,菜单则会展示在各个窗口的顶部。

可以对某一窗口单独设置或者删除Menu,但是这只是针对 windows、linux 生效。

win.setMenu(menu)设置为窗口的菜单栏 menu(只对 windows、linux 生效)。

win.removeMenu()删除窗口的菜单栏(只对 windows、linux 生效)。

如何全局设置Menu

const {
   
    Menu } = require('electron')

const template = [
{
   
   
  label: 'Electron',
  submenu: [
    {
   
    role: 'about', label: '关于' },
    {
   
    type: 'separator' },
    {
   
    role: 'services', label: '偏好设置' },
    {
   
    type: 'separator' },
    {
   
    role: 'hide', label: '隐藏' },
    {
   
    role: 'hideOthers', label: '隐藏其他' },
    {
   
    type: 'separator' },
    {
   
    role: 'quit', label: '退出' }
  ]
},
{
   
   
  label: '编辑',
  submenu: [
    {
   
    role: 'undo', label: '撤销' },
    {
   
    type: 'separator' },
    {
   
    role: 'menu_copy', label: '复制' },
    {
   
    role: 'menu_paste', label: '粘贴' }
  ]
},
{
   
   
  label: '窗口',
  submenu: [
    {
   
    
      role: 'minimize',
      label: '最小化',
      click: function (event, focusedWindow, focusedWebContents) {
   
   } 
    },
    {
   
    role: 'close', label: '关闭' },
    {
   
    role: 'togglefullscreen', label: '全屏', accelerator: 'Cmd+,OrCtrl+,'}
  ]
}];

let menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

role:可以理解为官方命名好的指令,详见官方menuitemrole

label:我们对指令自定义的展示文字。

click:触发该指令的接受的函数

Tray

添加图标和上下文菜单到系统通知区

image.png

const {
   
   ipcMain, app, Menu, Tray} = require('electron')

const iconPath = path.join(__dirname, './iconTemplate.png')
const tray = new Tray(iconPath)
const contextMenu = Menu.buildFromTemplate([{
   
   
    label: 'tray 1',
    click: () => {
   
   }
}, {
   
   
    label: 'tray 2',
    click: () => {
   
   }
}])
tray.setToolTip('Electron Demo in the tray.')
tray.setContextMenu(contextMenu)

setToolTip设置鼠标指针在托盘图标上悬停时显示的文本

setContextMenu设置图标的内容菜单(支持动态添加多个内容)

image.png

dialog

系统对话框

image.png

1.支持多选、默认路径

const {
   
    dialog } = require('electron')

const options = {
   
   
    title: '标题',
    defaultPath: '默认地址',
    properties: [  
        openFile, // 容许选择文件
        openDirectory, // 容许选择目录
        multiSelections, // 容许多选
        showHiddenFiles, // 显示隐藏文件
        createDirectory, // 创建新的文件,只在mac生效
        promptToCreate,// 文件目录不存在,生成新文件夹,只在windows生效
    ]
}

dialog.showOpenDialog(win, options); // win是窗口

2.支持过滤文件

过滤文件后缀名 gif 的文件,显示所有文件用 * 代替

options.filters = [
    {
   
    name: 'Images', extensions: ['gif'] }
]

globalShortcut

键盘事件

需要先注册globalShortcut.register(accelerator, callback)

const {
   
    globalShortcut } = require('electron')

globalShortcut.register('CommandOrControl+F', () => {
   
   
    // 注册键盘事件是全局性质的,各个窗口都可以触发
})

globalShortcut.register(accelerator, callback)注册全局快捷键
globalShortcut.isRegistered(accelerator)判断是否注册
globalShortcut.unregister(accelerator)取消注册

注意:应用程序退出时,注销键盘事件

app.on('will-quit', () => {
   
   
    globalShortcut.unregisterAll()
})

Notification

系统通知

这个受限于当前系统是否支持桌面通知,在 mac 或 windows 电脑的设置中,需特别注意是否容许通知。

const {
   
    Notification } = require('electron');

const isAllowed = Notification.isSupported();
if (isAllowed) {
   
   
    const options = {
   
   
        title: '标题',
        body: '正文文本,显示在标题下方',
        silent: true, // 系统默认的通知声音
        icon: '', // 通知图标
    }
    const notification = new Notification(argConig);
    notification.on('click', () => {
   
     });
    notification.on('show', () => {
   
     });
    notification.on('close', () => {
   
     });
    notification.show();
}

notification.close()关闭通知

session

管理浏览器会话、cookie、缓存、代理设置等。

1、全局

const {
   
    session, BrowserWindow } = require('electron')

// 拦截下载
session.defaultSession.on('will-download', (event, item, webContents) => {
   
   
    event.preventDefault() // 阻止默认行为下载或做指定目录下载
})

2、单独窗口

const win = new BrowserWindow({
   
    width: 800, height: 600 })
win.loadURL('http://github.com')
const ses = win.webContents.session

ses.setProxy(config):设置代理

ses.cookies:设置cookies或获取cookies

3、浏览器UserAgent

设置 UserAgent 可以通过 app.userAgentFallback 全局设置,也可以通过ses.setUserAgent 设置。

screen

检索有关屏幕大小、显示器、光标位置等的信息。

const primaryDisplay = screen.getPrimaryDisplay() // 获取光标所在屏幕的屏幕信息
const {
   
    width, height } = primaryDisplay.workAreaSize // 获取光标下的屏幕尺寸
const allDisplay = screen.getAllDisplays() // 返回数组,所有的屏幕

screen.getPrimaryDisplay()返回主窗口 Display

screen.getAllDisplays()返回所有的窗口 Display[] 数组

screen.getDisplayNearestPoint离光标最近的窗口

Node

项目中会用到 Node.js,下面是我整理的常用方法。

fs

本地文件读写

读取目录的内容

fs.readdirSync(path[, options])

fs.readdir(path[, options])

读取文件的内容

fs.readFileSync(path[, options])

fs.readFile(path[, options])

文件的信息

const stats = fs.statSync(path);

stats.isDirectory() // 是否为系统目录
stats.isFile() // 是否为文件
stats.size // 文件大小
。。。

路径是否存在

fs.existsSync(path)

写入文件

file 是文件名时,将数据写入文件,如果文件已存在则替换该文件。

fs.writeFile(file, data[, options], callback)

fs.writeFileSync(file, data[, options], callback)

移除文件

fs.rmSync(path[, options])

fs.rmdirSync(path[, options])

更改文件的权限

fs.chmod(path, mode, callback)

fs.chmodSync(path, mode, callback)

注意:mode是8进制,可通过parseInt(mode, 8)转化

修改文件夹名

fs.renameSync

拷贝文件到指定地址

fs.copySync

path

拼接绝对路径

path.resolve([...paths])

拼接路径

path.join([...paths])

路径文件夹名字

path.dirname(path)

获取文件名

path.basename(path)

都是属于 Nodejs,整理到最后懒得整理了~~,小伙伴们想研究的话,具体看Node.js

生成安装包

Electron打包生成跨平台软件,见我的另一篇文章
使用Electron生成安装包并打包为不同平台的应用格式

存在的问题

Electron还是会存在部分坑~~

我整理了一些,已经添加到我个人的Electron专题集,小伙伴们可以借鉴一下。

我也会持续更新。。。

有些问题我可能遇到没写,或者我写的是错误的,欢迎大家积极指正沟通,共同学习,共同进步~~。

结语

感谢您的阅读!希望本文带给您有价值的信息。

如果对您有帮助,请「点赞」支持,并「关注」我的主页获取更多后续相关文章。同时,也欢迎「收藏」本文,方便以后查阅。

写作不易,我会继续努力,提供有意义的内容。感谢您的支持和关注!

后期分享

《Electron开发自定义通知 & 多并发下接收消息处理》

《Electron仿浏览器多窗口多进程管理》

《Electron性能优化》

目录
相关文章
|
7月前
|
开发框架 移动开发 小程序
强烈推荐:绝对是好用的小程序开源框架
强烈推荐:绝对是好用的小程序开源框架
138 0
|
8月前
|
Dart 开发工具 iOS开发
带你读《深入浅出Dart》二十三、Flutter环境的安装与配置(1)
带你读《深入浅出Dart》二十三、Flutter环境的安装与配置(1)
|
8月前
|
Dart 开发工具 Android开发
带你读《深入浅出Dart》二十三、Flutter环境的安装与配置(2)
带你读《深入浅出Dart》二十三、Flutter环境的安装与配置(2)
|
11月前
|
存储 缓存 移动开发
构建跨平台应用的利器——UniApp入门指南
构建跨平台应用的利器——UniApp入门指南
|
11月前
|
Web App开发 开发框架 前端开发
Electron快速入手,拥有自己的第一个桌面应用
Electron 是一个跨平台的、基于 Web 前端技术的桌面 GUI 应用程序开发框架。 支持 Web 技术开发桌面应用,其本身是基于 C++ 开发的,GUI 核心来自于 Chrome,而 JavaScript 引擎使用 v8。 这使得我们使用前端技术就可以开发出属于自己的第一个桌面应用。
247 0
|
Rust 前端开发 安全
通过跨端程序tauri,来简单入门一下rust
Tauri 是一个跨平台 GUI 框架,与 Electron 的思想基本类似。Tauri 的前端实现也是基于 Web 系列语言,Tauri 的后端使用 Rust。Tauri 可以创建体积更小、运行更快、更加安全的跨平台桌面应用。
1207 0
|
存储 Web App开发 Dart
安卓开发学Flutter——安装与构建环境的一些注意点(第一篇)
Flutter是多平台开发的利器,我在半年前和技术交流群就接触到了,但一直没有去主动学习,但为了未来的长足发展,还是去认真学习一下
198 0
|
JavaScript 前端开发 Linux
Electron入门教程1 —— 编写第一个桌面应用程序
Electron入门教程1 —— 编写第一个桌面应用程序
770 0
Electron入门教程1 —— 编写第一个桌面应用程序
|
XML 存储 开发框架
怎么使用Java开发HarmonyOS
怎么使用Java开发HarmonyOS
236 0
怎么使用Java开发HarmonyOS
|
前端开发 JavaScript
开源项目推荐:Electron,使用网页编程的方式构建跨平台的桌面应用
开源项目推荐:Electron,使用网页编程的方式构建跨平台的桌面应用
661 0