Electron开发进阶(一)

时间:2021-1-8 作者:admin

作者: 知鸟 何德兴

electron的简介与demo编写我们就不多说了,官方有相关的说明和文档。今天我们讲的是一些官方文档上没有的,希望对大家有所帮助。

electron项目流程

electron项目打包本质上分为三个相对独立的流程,没有说必须的依赖关系,可以独立操作。

  • web业务代码打包

业务代码打包就是我们的web代码打包, 比如打包vue项目,react项目等等,打包到指定文件夹下,比如dist。

  • electron业务代码打包

对于electron来说,我们的自定义配置,各种监听,自定义菜单,等等一系列我们自定义业务,也是需要打包的。

  • electron打包

当上面俩个打包流程完毕,就可以开始electron打包。

自定义配置及打包

现有的electron的打包插件有electron-packagerelectron-builder两类,这里我们选择的是electron-builder

话不多说,直接上配置项:

// package.json 中增加build项
"build": {
    "productName": "老鸟会",
    "appId": "com.example.yourapp",
    "directories": {
      "output": "build"
    },
    "publish": [
      {
        "provider": "generic",
        "url": "http://www.你的网址.com/download/"
      }
    ],
    // file项配置的是你打包到本地的文件。 如果你发现配置文件无效,请尝试在后面加上**/*
    "files": [
      "dist/electron/**/*",
      "!node_modules/asar/**/*"
    ],
    "dmg": {
      "contents": [
        {
          "x": 410,
          "y": 150,
          "type": "link",
          "path": "/Applications"
        },
        {
          "x": 130,
          "y": 150,
          "type": "file"
        }
      ]
    },
    "mac": {
      "icon": "icons/icon.icns",
      "artifactName": "${productName}_setup_${version}.${ext}",
      "target": "dmg"
    },
    "win": {
      "icon": "icons/icon.ico",
      "artifactName": "${productName}_setup_${version}.${ext}",
      // 这个配置是在你需要额外迁移某些文件的时候,可以直接配置此项。则在打包时会额外迁移配置的文件【配置在这里,是指定此平台配置使用。如果需要通用,需要配置在build下级。具体后面有写】
      "extraResources": {
        "from": "./config.json",
        "to": "../config.json"
      }
    },
    "linux": {
      "icon": "icons",
      "artifactName": "${productName}_setup_${version}.${ext}"
    },
    // 安装配置
    "nsis": {
      "oneClick": false,
      "perMachine": true,
      "allowElevation": true,
      "allowToChangeInstallationDirectory": true,
      "createDesktopShortcut": true,
      "runAfterFinish": true,
      "installerIcon": "./build/.icon-ico/icon.ico",
      "uninstallerIcon": "./build/.icon-ico/icon.ico"
    }
  },

electron-builder常见相关报错

  1. 打包报错:Application entry file "build\electron.js" in the "app.asar" does not exist. Seems like a wrong configuration.

    解决方案

    (1)请确认你的package.json中的main配置,是否有配置:

      "main": "./dist/electron/main.js",
    

    (2)如果已经配置了main,并确认了配置无误,却依然报此错误。请确认下,你的项目 是不是react项目!如果electron-builderdependencies发现了 含react的依赖,那main配置就无效,解决方法如下:

    a. 在你的build配置中加上:package.json/build/

      "directories": {
        buildResources: "assets"
      },
      "extraMetadata": {
        main: "build/electron.js" // 喏,把这个改成你的main路径
      }
    

    b. 在build里面加上一个配置:

      "build": {
        "extends": null
      },
    
  2. 如果react项目打包成功了,但是在打开之后,页面空白。

    解决方案

    打开控制台,如果发现是加载路径不对,导致加载资源404的话,需要将react打包的时候的绝对路径改成相对路径,直接在package.json加上配置项【跟react/cli版本有关系,最好测试下自己的版本兼容】:

      "homepage": "./"
    
  3. 如果打包出来的客户端体积过大,怎么处理?

    解决方案

    electron打包,默认会按照我们package.json/build/file的配置进行移植文件:

      "files": [
        "dist/electron/**/*", // 配置需要打包的文件
        "!node_modules/asar/**/*" // 配置不需要打包的文件
      ],
    

    因为默认会打包node_modules(node环境需要), 所以如果我们的依赖包特别大,很有可能导致我们打包出来的客户端也会特别大,所以可以通过配置哪些依赖不需要的方式,减少打包体积。

    (1)可以安装asar,解压客户端中的app.asar,查看打包的资源有哪些。

    (2)千万不要尝试说不配置files的方式,electron没有配置files的情况下,会把所有的文件全部打包过去(包括源码)。

    (3)在打包的时候,如果我们有额外的移植文件的需求,比如我要移动一个文件,放到我们的打包资源里。可以通过配置package.json/build中的extraResources配置,来增加在额外资源。extraResources也分为在build中配置和在具体打包端配置/extraResources俩种配置方式。

    • 在build中配置,extraResources的值是个数组,对应的是所有的打包配置。 package.json/build/extraResources

        "build": {
          "productName": "老鸟会",
          "appId": "com.example.yourapp",
          "directories": {
            "output": "build"
          },
          "files": [
            "dist/electron/**/*",
            "!node_modules/asar/**/*"
          ],
          "extraResources": [{
            "from": "./config.json",
            "to": "../config.json"
          },{
            "from": "./extraExe/test.exe",
            "to": "../extraExe/test.exe"
          }]
          "dmg": {
            "contents": [
              {
                "x": 410,
                "y": 150,
                "type": "link",
                "path": "/Applications"
              },
              {
                "x": 130,
                "y": 150,
                "type": "file"
              }
            ]
          }
        }
      
    • 另外一种是在具体打包配置内,这种方式只针对当前打包配置有效,而且值类型为对象。package.json/build/具体打包端配置/extraResources。在这里顺便提一下,配置是可以在打包命令里临时配置替换的【后面会讲】。

        "win": {
          "icon": "icons/icon.ico",
          "artifactName": "${productName}_setup_${version}.${ext}",
          "extraResources": { // 配置此项,即为额外的资源
            "from": "./config.json",
            "to": "../config.json"
          }
        }
      

自定义窗体

mac和windows系统的窗口配置不一样,比如无边框自定义窗体,在mac中,控制钮是系统带的,通过配置来实现展示,而在windows中则需要在业务代码中自定义控制钮。

自定义窗体主要注意几点:

  • mac中的窗体配置,主要为左上角的控制按钮的显示方式titleBarStyle:

    • hidden: 隐藏标题栏的全尺寸内容窗口,在左上角仍然有标准的窗口控制按钮(俗称“红绿灯”)

    • hiddenInset: 隐藏了标题栏的窗口,其中控制按钮到窗口边框的距离更大。

    • customButtonsOnHover: 使用自定义的关闭、缩小和全屏按钮,这些按钮会在划过窗口的左上角时显示。

    mac: {
      height: 480,
      // 设置的宽高是否是内容的宽高
      useContentSize: false,
      width: 865,
      // 窗口是否边框
      frame: false,
      // 是否允许改变窗口大小
      resizable: false,
      // mac用,控制按钮的放置方式
      titleBarStyle: 'hidden',
      webPreferences: {
        // node集成
        nodeIntegration: true,
        plugins: true,
        webviewTag: true
      }
    }
  • 如果需要做自定义无边框的窗口,则需要设置

    • frame: 窗口是否有边框。当无边框时,则直接显示的是我们的页面。

    • transparent: 可以使无框窗口透明。

    自定义无边框窗口的局限性:(以下为官网原文)

    • 你不能点击穿透透明区域。 我们将引入一个 API 来设置窗口形状以解决此问题, 请参阅 our issue 以了解详细信息。

    • 透明窗口不能调整大小。将resizable设置为true可能会使透明窗口在某些平台上停止工作。

    • blur 筛选器仅适用于网页, 因此无法对位于透明窗口下方的内容应用模糊效果 (例如在用户系统上打开的其他应用程序) 。

    • 在windows 操作系统上, 当 DWM 被禁用时, 透明窗口将无法工作。

    • 在linux 上, 用户必须在命令行中设置--enable-transparent-visuals --disable-gpu 来禁用GPU, 启用 ARGB,用以实现窗体透明。 这是由一个上游的 bug 导致的, 即 在Linux机上,透明度通道(alpha channel )在一些英伟达的驱动(NVidia drivers)中无法运行。

    • 在 Mac 上, 透明窗口无法显示原生窗口的阴影。

  • 在业务页面中,定义可拖动区域(拖动窗口)

    • 默认情况下, 无边框窗口是不可拖拽的。 我们可以在样式中增加属性来让用户可以点击某元素可以拖拽窗口:

        -webkit-app-region: drag
      
    • 与之相对的,我们也需要在某些元素中增加属性来将某些元素排除。比如配置了可拖拽元素中包含有input输入框,会发现输入框无法输入了。这时候就需要给input增加:

      -webkit-app-region: no-drag
      
    • 当前好像只支持矩形形状,不知道后续会不会升级。

  • 父子模态窗

    父子模态窗口包含了父子窗口和模态窗口俩个概念,但因为一般使用都是俩个一起,所以这里直接使用父子模态窗的概念。

    • 父子窗口是指子窗口创建时,设置属性parent为父窗口,则此时子窗口会显示在父窗口上。

    • 模态窗口是在父子窗口的基础上,增加model属性,此时子窗口不仅仅在父窗口之上,子窗口没关闭,父窗口还无法操作。(想想我们玩网游时,打开的注册窗口)

      import { BrowserWindow } from 'electron'
      
      let child = new BrowserWindow({ parent: top, modal: true, show: false })
      child.loadURL('https://www.baidu.com')
      child.once('ready-to-show', () => {
        child.show()
      })
      

    注意: 打开父子模态窗口需谨慎,因为子窗口没有关闭按钮(mac),如果你业务代码没有关闭,那会很尴尬(子窗口关不了,父窗口更关不了)

  • 窗口白屏时间

    electron这里,白屏时间的问题也是一个很大的问题,处理办法有:

    • 将部分首屏资源放到客户端本地(详见打包配置)

    • 优化业务代码(不做撰述,自行百度)

    • 设置backgroundColor,让白屏变有色的屏

      let win = new BrowserWindow({ backgroundColor: '#cdcdcd' })
      
    • 单独载入动画也是个不错的方案,就是需要费点事

客户端版本检测和自动更新

  • 自动更新应该是客户端程序的一个非常核心的功能,在electron中也内置有autoUpdater模块,包含了版本打包,版本检测,安装包下载,安装包安装等。

  • 我们使用的是electron-updater,是一个对electron中的autoUpdater进行封装优化的依赖包。

  • 我们可以将autoUpdater模块独立成一个方法,直接引入调用,参数即为我们的主窗口:

      import { updateHandle } from './auto-updater'
    
      let mainWindow
      function createWindow () {
        // userAppConfig 为你的窗口配置 (去看前面的文里有)
        mainWindow = new BrowserWindow(userAppConfig)
    
        mainWindow.loadURL(winURL)
    
        mainWindow.on('closed', () => {
          mainWindow = null
        })
    
        updateHandle(mainWindow)
      }
    
      // auto-updater.js
    
      import fs from 'fs'
      import { ipcMain } from 'electron'
      import { autoUpdater } from 'electron-updater'
      import { updateURL } from './config/urlConfig' // 这个是我们的服务器放安装包的地址
      // 绑定检测更新事件
      /**
       * 接收渲染进程 checkForUpdate 事件触发自动更新查询
       */
    
      const updateUrl = process.platform === 'darwin' ? updateURL.mac : updateURL.windows
    
      function updateHandle (mainWindow) {
        if (process.env.NODE_ENV === 'development') {
          // 如果需要在开发环境调试自动更新功能,必须主动设置当前版本
          // electron-updater在开发环境,版本号将为electron版本,而不是app版本号
          autoUpdater.currentVersion = '0.1.0'
        }
        function sendUpdateMessage(obj) {
          mainWindow.webContents.send('updateMessage', obj)
        }
    
        // 设置升级包所在的地址
        autoUpdater.setFeedURL({
          provider: 'generic', // 这里还可以是 github, s3, bintray
          url: updateUrl
        })
    
        // 当更新出现错误时触发
        autoUpdater.on('error', (err) => {
          fs.writeFile('update_error', err, function() {
            // callback function
          })
          sendUpdateMessage({action: 'error', errorInfo: err})
        })
    
        // 当开始检查更新的时候触发
        autoUpdater.on('checking-for-update', () => {
          sendUpdateMessage({action: 'checking'})
        })
    
        // 自动下载配置,若为true, 则当发现一个可用更新的时候,会自动开始下载升级包
        autoUpdater.autoDownload = false
        // 发现了有新版本
        autoUpdater.on('update-available', (info) => {
          sendUpdateMessage({action: 'updateAva', updateInfo: info})
        })
    
        // 当没有可用更新的时候触发
        autoUpdater.on('update-not-available', (info) => {
          sendUpdateMessage({action: 'updateNotAva', updateInfo: info})
        })
    
        // 更新下载进度事件
        autoUpdater.on('download-progress', (progressObj) => {
          mainWindow.webContents.send('downloadProgress', progressObj)
        })
    
        // 如果发现新版本已经下载,则发送版本已下载消息,并退出app进行安装
        autoUpdater.on('update-downloaded', (info) => {
          sendUpdateMessage({action: 'updateDownloaded', updateInfo: info})
          ipcMain.on('isUpdateNow', () => {
            console.log('isUptaeNow')
            autoUpdater.quitAndInstall()
          })
          fs.writeFile('update-downloaded', JSON.stringify(info), function() {
            // callback function
          })
        })
    
        // 执行自动更新检查
        ipcMain.on('checkForUpdate', () => {
          console.log('主进程接收到命令, 开始查询版本...')
          autoUpdater.checkForUpdates()
        })
    
        // 下载
        ipcMain.on('downloadUpdate', () => {
          autoUpdater.downloadUpdate()
        })
      }
    
      export {
        updateHandle
      }
    
    

自动更新的问题和细节

  • 很多需要注意的地方我已经直接在代码里标注了。

  • 开发环境自动自动更新版本检测,版本一直不对。

    • 如果需要在开发环境调试自动更新功能,必须主动设置当前版本
    • electron-updater在开发环境,版本号将为electron版本,而不是app版本号
  • 在打包的时候,别忘了在package.json/build/publish配置一下你要放升级包的地址

      "publish": [
        {
          "provider": "generic",
          "url": "http://www.snowhe.com/myaide/download/"
        }
      ]
    
  • 我们配置了publish之后,打包之后会生成俩个文件,一个是latest-mac.ymlbuilder-effective-config.yaml((windows是latest.yml和一个.yaml文件)。这俩个文件一定一定要和安装包一起更新到我们在服务器的文件夹内。

  • 我们可以通过配置package.json/build/nsis来配置安装的时候的安装流程(指windows系统的exe安装流程,mac就别想了)

        "nsis": {
          "oneClick": false,
          "perMachine": true,
          "allowElevation": true,
          "allowToChangeInstallationDirectory": true,
          "createDesktopShortcut": true,
          "runAfterFinish": true,
          "installerIcon": "./build/.icon-ico/icon.ico",
          "uninstallerIcon": "./build/.icon-ico/icon.ico"
        }
    
    • 此配置在mac是无效的,别想了,mac安装app什么时候让你自定义了?

    • 如果你对nsh文件有研究,你也可以自己配置一个nsh文件(反正我不熟):

      !macro customHeader
      
      !macroend
      
      !macro preInit
      
      !macroend
      
      !macro customInit
      # guid=7e51495b-3f4d-5235-aadd-5636863064f0
      ReadRegStr $0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{7e51495b-3f4d-5235-aadd-5636863064f0}" "UninstallString"
      ${If} $0 != ""
      MessageBox MB_ICONINFORMATION|MB_TOPMOST  "检测到系统中已安装本程序,将卸载旧版本" IDOK
      # ExecWait $0 $1
      ${EndIf}
      !macroend
      
      !macro customInstall
      
      !macroend
      
      !macro customInstallMode
      # set $isForceMachineInstall or $isForceCurrentInstall
      # to enforce one or the other modes.
      #set $isForceMachineInstall
      !macroend
      
    • 在业务代码中,我们也可以通过渲染进程向主进程发送消息的方式,让主进程进行版本检测(消息通信后面会讲)。

进程之间的通信

进程之间的通信主要指主进程和渲染进程之间的通信,是我们做桌面app的重要功能,也可以说是我们业务代码和窗口之间的通信。

- 理解一,就是父页面和iframe子页面的通信。有兴趣的同学可以去了解一下,都是消息通信这一套。**所以传递的消息也是字符串(划重点)**
- 理解二,我们在具体处理的时候,有点类似于发布订阅者【观察者模式】,可以看下方具体用法。

渲染进程中的通信是通过ipcRender传递的,你可以使用它提供的一些方法从渲染进程 (web 页面) 发送同步或异步的消息到主进程。
而主进程也可以通过监听窗口的webContent消息来接受和发送消息。

- 消息通信是单向的。
- 可以理解为电话留言,并不是实时通话。

如何理解ipcRender?

主进程和渲染进程的关系,很像页面和页面中的iframe的关系,想想页面和iframe的消息通道,是不是很好理解了?(如果对这块不熟悉的同学可以去查一下postMessage)

与之对应的,我们需要在主程序ipcMain这边做接收,即在渲染进程中的ipcRender发送消息的时候,主进程中的ipcMain进行接收处理,并回传结果给ipcRender(再发消息给ipcRender),从而达到一个双向通信的目的。

  • 所以综上所述,其实还是ipcMain和ipcRender之间的消息通信。

  • 我们做通信也是,在这俩个模块相对应的挂载我们需要的消息,以实现我们需要的通信类型。

下面详细讲下如何在ipcRender和ipcMain中挂载消息

  • 在ipcMain中挂载消息监听:

    • 头部引入
    import { app, BrowserWindow, ipcMain } from 'electron'
    import { createChildWindow } from './utils'
    
    
    • 创建窗口
      let mainWindow
    
      function createWindow () {
        /**
        * Initial window options
        */
        mainWindow = new BrowserWindow(userAppConfig)
    
        mainWindow.loadURL(winURL)
    
        mainWindow.on('closed', () => {
          mainWindow = null
        })
    
        // 建立监听
        addWindowListener()
      }
    
    • ipcMain监听 【ipcMain会监听我们订阅的消息类型】
    function addWindowListener () {
      // 关闭窗口
      ipcMain.on('closeWindow', () => {
        mainWindow.close()
      })
      // 隐藏窗口
      ipcMain.on('hideWindow', () => {
        console.log('hide')
        mainWindow.minimize()
      })
      // 最大化窗口
      ipcMain.on('maxWindow', () => {
        console.log('maxWindow')
        mainWindow.maximize()
      })
      // 全屏显示
      ipcMain.on('fullScreen', (e, flag) => {
        console.log(flag)
        mainWindow.setFullScreen(flag)
      })
      // 恢复上一屏幕状态
      ipcMain.on('restoreWindow', () => {
        mainWindow.restore(true)
      })
      // 生成一个子模态窗口(禁用主窗口)
      ipcMain.on('addChildWindow', (e, url, config) => {
        console.log('addChildWindow:', e, url, config)
        createChildWindow({
          parent: mainWindow,
          modal: true,
          url
        })
      })
    }
    
    
    • ipcMain postMessage【ipcMain发送消息】
      function sendMessage(obj) {
        mainWindow.webContents.send('sendMessageToIpcRender', obj)
      }
      sendMessage({
          type: 'call',
        message: 'ipcRender,我是你老大ipcMain,麻烦帮我找下楚楚baby'
      })
    
    • ipcRender中接收 【其实就是我们的具体业务代码里引入】
    import { ipcRenderer, shell } from 'electron'
    ipcRenderer.on('sendMessageToIpcRender', (event, info) => {
      console.log(info)
    })
    
    • ipcRender中发送消息
    import {ipcRenderer} from 'electron'
    const info = {
        type: 'callback',
        message: 'ipcMain老大, 我是ipcRender,没找到楚楚baby呀'
    }
    ipcRenderer.send('sendMessageToIpcMain', JSON.stringify(info))
    

    通信传输的相似和不同,底层的原因是因为选用的协议导致。在学习这些时,我们会看到类似的功能总会有很多相通的共同点【想研究的同学可以去看看这些协议,可以明白很多无法正常解释的问题~】。

    这一期先讲这些,谢谢~

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。