乐趣区

关于前端:从零开始的electron开发主进程窗口关闭与托盘处理

窗口敞开与托盘解决

本期次要波及窗口的敞开解决以及托盘的简略解决。
先说说本期的一个指标性能实现:以网易云音乐为例,在 Windows 环境下,咱们点击右上角的敞开,这个时候会呈现一个弹窗(以前没有勾选不在揭示的话)询问是间接退出还是放大到系统托盘,抉择后确定才会进行理论敞开解决,当勾选不再揭示后点击确认后下一次敞开不再提醒间接解决,如果是放大到托盘,对托盘点击退出才会真正敞开。在 Mac 环境下,点击右上角敞开间接放大到程序坞,对程序坞右键退出或右上角托盘退出或左上角菜单退出,软件才会真正敞开。
当然,每个软件都有不同的退出逻辑,这里介绍如何实现下面性能的同时会对 electron 退出的各种事件进行阐明,心愿能帮忙你找到想要的退出形式。
这里降级了一下版本,本版本为 electron:12.0.0

敞开的概念

咱们在应用官网例子,打包装置后会发现 mac 和 win 在敞开上有所不同,mac 是间接放大到程序坞,对程序坞右键退出能力敞开,win 则是间接敞开软件,这是为什么呢?
这里我先简略说一下敞开的概念,很多人把软件的敞开和窗口的敞开混同在一起了,我这里把窗口和软件辨别开说一下:

窗口的敞开:

win:BrowserWindow 实例
win.destroy():强制敞开这个窗口,会触发 win 的 closed 事件,不会触发 close 事件
win.close():敞开窗口,触发 win 的 close,closed 事件

留神:窗口的敞开不肯定会触发软件的敞开,然而通常状况下咱们只有一个窗口,如果这个窗口敞开了,会触发 app 的 window-all-closed(当所有的窗口都被敞开时触发)这个事件,在这个事件里咱们能够调用软件的敞开 app.quit(),故大多数状况下,咱们把窗口敞开了,软件也就退出了。
那么造成这个差别的起因也就浮出水面了:

app.on('window-all-closed', () => {if (!isMac) {app.quit()
  }
})

软件的敞开:

app.quit():调用会先触发 app 的 before-quit 事件,而后再触发所有窗口的敞开事件,窗口全副敞开了(调用 app.quit()敞开窗口是不会触发 window-all-closed 的,会触发 will-quit),触发 app 的 quit 事件。然而如果在 quit 事件前应用 event.preventDefault()阻止了默认行为(win 的 close 事件,app 的 before-quit 和 will-quit),软件还是不会敞开。app.exit():很好了解,最粗犷的强制敞开所有窗口,触发 app 的 quit 事件,故 win 的 close 事件,app 的 before-quit 和 will-quit 不会被触发

总结一下简略来说软件的敞开要满足两个条件:

  • 所有窗口都敞开了
  • 调用了 app.quit()

所以软件的敞开个别就是上面几种状况了

  1. 所有窗口敞开触发 window-all-closed,在 window-all-closed 里调用 app.quit()
  2. 调用 app.quit(),触发所有窗口的 close 事件
  3. app.exit()

那么要达成咱们的指标只有应用办法 2 了。

过程通信配置

过程通信的话,放到前面再说,这里只是介绍过程通信的配置
如果我在渲染过程想应用 electron 的一些办法的话,应用如下

const {ipcRenderer} = require('electron')
ipcRenderer.send('asynchronous-message', 'ping') // 向主过程发送音讯

这样应用没问题,然而如果咱们有多个页面都要应用那么咱们每个页面都要 require,比拟麻烦,而且如果咱们想既打包 electron,又想打包 web 同样应用(能够通过 process.env.IS_ELECTRON 解决不同场景),那么引入的 electron 就无用了。electron 的窗口的 webPreferences 提供了 preload 能够注入 js,咱们能够在这里把 ipcRenderer 挂载到 window 上面。

vue.config.js:electronBuilder: {
  nodeIntegration: true, // 这里设置实际上是设置 process.env.ELECTRON_NODE_INTEGRATION 的值
  preload: 'src/renderer/preload/ipcRenderer.js',
  ......
}

ipcRenderer.js:import {ipcRenderer} from 'electron'
window.ipcRenderer = ipcRenderer
主过程:win = createWindow({
    ....
    webPreferences: {
      contextIsolation: false,
      nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
      preload: path.join(__dirname, 'preload.js'),
      scrollBounce: isMac
    }
  }, '','index.html')

渲染过程:
if (process.env.IS_ELECTRON) {window.ipcRenderer.send('asynchronous-message', 'ping')
}

这里阐明一下 contextIsolation 这个值,在 12.0.0 以前默认值为 false,本例子是 12.0.0 版本,默认值为 true,区别在于为 true 的话,注入的 preload.js 可视为一个独立运行的环境,对于渲染过程是不可见的,简略来说就是咱们把 ipcRenderer 挂载到 window 上,对应的渲染过程是获取不到的,故这里设置为 false。

性能实现

如何实现呢?理一下思路,win 的 close 事件有两种触发形式:

  1. 一个是咱们点击敞开触发,此时咱们并不想敞开窗口,那么应该应用 e.preventDefault() 阻止窗口的敞开。
  2. 另一个是咱们被动应用 app.quit() 触发敞开,这时 close 事件里就不做解决。

那么通过一个变量 flag 的切换来实现,申明一个全局变量 willQuitApp,在onAppReady 里增加窗口的 close 事件,当咱们点击敞开触发 close 事件,此时 e.preventDefault() 禁止了窗口的敞开,咱们再通过主过程向渲染过程收回一个敞开的告诉。
咱们的流程为:
主过程检测敞开─> 判断是否是 app.quit() 触发
──> 否,告诉渲染过程敞开音讯,渲染过程接管后依据用户操作或本地存储告诉主过程将软件敞开或放大到托盘
──> 是,敞开软件

主过程:

let willQuitApp = false

onAppReady:win.on('close', (e) => {console.log('close', willQuitApp)
  if (!willQuitApp) {win.webContents.send('win-close-tips', { isMac})
    e.preventDefault()}
})

咱们被动应用 `app.quit()` 触发敞开时把 willQuitApp 设置为 true,而后会触发 win 的 close 事件,让窗口敞开掉,达成办法 2。app.on('activate', () => win.show()) // mac 点击程序坞显示窗口
app.on('before-quit', () => {console.log('before-quit')
  willQuitApp = true
})

渲染过程:

<a-modal
    v-model:visible="visible"
    :destroyOnClose="true"
    title="敞开提醒"
    ok-text="确认"
    cancel-text="勾销"
    @ok="hideModal"
  >
    <a-radio-group v-model:value="closeValue">
      <a-radio :style="radioStyle" :value="1"> 最小化到托盘 </a-radio>
      <a-radio :style="radioStyle" :value="2"> 退出 vue-cli-electron</a-radio>
      <a-checkbox v-model:checked="closeChecked"> 不再揭示 </a-checkbox>
    </a-radio-group>
  </a-modal>

import {defineComponent, reactive, ref, onMounted, onUnmounted} from 'vue'
import {LgetItem, LsetItem} from '@/utils/storage'

export default defineComponent({setup() {const closeChecked = ref(false)
    const closeValue = ref(1)
    const visible = ref(false)
    const radioStyle = reactive({
      display: 'block',
      height: '30px',
      lineHeight: '30px',
    })
    onMounted(() => {window.ipcRenderer.on('win-close-tips', (event, data) => { // 承受主过程的敞开告诉
        const closeChecked = LgetItem('closeChecked')
        const isMac = data.isMac
        if (closeChecked || isMac) { // mac 和 win 的辨别解决
          event.sender.invoke('win-close', LgetItem('closeValue')) // 当是 mac 或者勾选了不再提醒时向主过程发送音讯
        } else {
          visible.value = true
          event.sender.invoke('win-focus', closeValue.value) // 显示敞开弹窗并聚焦
        }
      })
    })
    onUnmounted(() => {window.ipcRenderer.removeListener('win-close-tips')
    })
    async function hideModal() {if (closeChecked.value) {LsetItem('closeChecked', true)
        LsetItem('closeValue', closeValue.value)
      }
      await window.ipcRenderer.invoke('win-close', closeValue.value) // 向主过程推送咱们抉择的后果
      visible.value = false
    }
    return {
      closeChecked,
      closeValue,
      radioStyle,
      visible,
      hideModal
    }
  }
})

主过程承受渲染过程音讯,initWindow里 win 赋值后调用,这里要留神的是 Mac 的解决,Mac 在全屏状态下如果暗藏的话,那么会呈现软件白屏或黑屏状况,咱们这里要先退出全屏而后再暗藏掉。

import {ipcMain, app} from 'electron'
import global from '../config/global'

export default function () {
  const win = global.sharedObject.win
  const isMac = process.platform === 'darwin'
  ipcMain.handle('win-close', (event, data) => {if (isMac) {if (win.isFullScreen()) { // 全屏状态下非凡解决
        win.once('leave-full-screen', function () {win.setSkipTaskbar(true)
          win.hide()})
        win.setFullScreen(false)
      } else {win.setSkipTaskbar(true)
        win.hide()}
    } else {if (data === 1) {  // win 放大到托盘
        win.setSkipTaskbar(true) // 使窗口不显示在任务栏中
        win.hide() // 暗藏窗口} else {app.quit() // win 退出
      }
    }
  })
  ipcMain.handle('win-focus', () => { // 聚焦窗口
    if (win.isMinimized()) {win.restore()
      win.focus()}
  })
}

托盘设置

这里的托盘设置只是为了实现软件的退出性能,故只是简略介绍,其余的性能前面的篇章会具体介绍的。
托盘的右键点击退出间接退出,所以间接调用 app.quit() 触发退出流程

initWindow 里 win 赋值后调用 setTray(win)

import {Tray, nativeImage, Menu, app} from 'electron'
const isMac = process.platform === 'darwin'
const path = require('path')
let tray = null

export default function (win) {
  const iconType = isMac ? '16x16.png' : 'icon.ico'
  const icon = path.join(__static, `./icons/${iconType}`)
  const image = nativeImage.createFromPath(icon)
  if (isMac) {image.setTemplateImage(true)
  }
  tray = new Tray(image)
  let contextMenu = Menu.buildFromTemplate([
    {
      label: '显示 vue-cli-electron',
      click: () => {winShow(win)
      }
    }, {
      label: '退出',
      click: () => {app.quit()
      }
    }
  ])
  if (!isMac) {tray.on('click', () => {winShow(win)
    })
  }
  tray.setToolTip('vue-cli-electron')
  tray.setContextMenu(contextMenu)
}

function winShow(win) {if (win.isVisible()) {if (win.isMinimized()) {win.restore()
      win.focus()} else {win.focus()
    }
  } else {!isMac && win.minimize()
    win.show()
    win.setSkipTaskbar(false)
  }
}

这里的逻辑还是比较简单的,惟一纳闷的点可能是 win.show() 前为什么要有个 win.minimize(),这里的解决呢是因为 hide 前如果咱们渲染过程有可见的扭转(咱们这里是让敞开提醒的弹窗敞开了),前面再 show 时会呈现一个闪动的问题,有趣味的同学能够把win.minimize() 正文一下再看一下成果。当然你也能够用上面的解决形式:

win.on('show', () => {setTimeout(() => {win.setOpacity(1)
  }, 200)
})
win.on('hide', () => {win.setOpacity(0)
})

补充

Mac 零碎在解决上有一些逻辑和 Windows 是不一样的,尽管并没有一个硬性的规定要这样解决,更多的是看集体爱好与约定俗成。
比方托盘的点击解决 win 上左击间接关上软件,右击关上菜单,而 mac 上左击除了触发 click 外还会关上菜单,如果和 win 上一样解决的话有些不太合适。
这里再补充一个 mac 上的,mac 软件在全屏时,大多数软件都是把放大这个按钮给禁用了的,那么 electron 怎么实现这个呢:

win.on('enter-full-screen', () => {isMac && app.commandLine.appendSwitch('disable-pinch', true)
})
win.on('leave-full-screen', () => {isMac && app.commandLine.appendSwitch('disable-pinch', false)
})

因为咱们的窗口实际上就是 chromium,故咱们能够通过设置 chromium 的参数来实现,更多的参数请参考链接设置。

本文地址:链接
本文 github 地址:链接

退出移动版