关于前端:从零开始的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地址:链接

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理