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

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

作者: SailingBytes | 来源:发表于2022-08-08 16:43 被阅读0次

    简介

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

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

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

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

    刚开始学习Electron的小伙伴可以先看 Electron快速入手,拥有自己的第一个桌面应用

    Electron核心

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

    image.png

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

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

    Electron主要核心点

    Electron其实很简单,基本都是api,我自己整理了主要核心点有进程间通信app生命周期BrowserWindowApplication 4个方面。

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

    app生命周期:贯穿桌面端应用整个生命周期(用的不多,但是很重要)。

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

    Application:应用程序功能点(为应用提供更佳完善的功能点)。

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

    进程间通信

    ipcMain与ipcRenderer

    electron存在多个进程,那么多进程间如何实现通信,electron官方提供了方法。

    image.png

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

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

    异步

    通过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

    app控制应用程序的事件生命周期

    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 完成初始化,这个是重点

    Electron中很多都是需要Electron初始化完成后,才可以执行(如menu、窗口......)

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

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

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

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

    2、第二个实例创建

    当运行第二个实例时,聚焦到主窗口

    gotTheLock = app.requestSingleInstanceLock();
    if (!gotTheLock) {
        app.quit();
    } else {
        app.on('second-instance', (event, commandLine, workingDirectory) => {
            let win = mainWindow;
            if (win) {
                if (win.isMinimized()) {
                    win.restore();
                }
                if (!win.isFocused()) {
                    win.show();
                }
                win.focus();
            }
        });
    }
    

    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常用方法

    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还是会存在部分坑~~

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/cmwwwrtx.html