关于前端:从零开始的electron开发托盘与消息通知

3次阅读

共计 6591 个字符,预计需要花费 17 分钟才能阅读完成。

托盘与音讯告诉

上一期简略介绍了托盘的退出,这一期呢说一下托盘的音讯解决。
先说说本期的指标:实现微信对话的音讯告诉及解决。

  • windows:当有新音讯推送过去时,托盘闪动,任务栏闪动,左下角推送音讯(简化解决)。
  • mac:当有新音讯时,程序坞有未读红点音讯数,托盘未读数,右上角推送音讯。


音讯的解决

首先说说音讯的解决吧,当有一个新音讯推送过去时,win 下任务栏会闪动,当页面聚焦或过一段时间,闪动勾销,托盘闪动,当音讯全副读取,托盘还原。在 mac 下则是新音讯推送过去程序坞跳动,当音讯全副读取,程序坞红点清空,托盘数字清空。
简略来说就是未读音讯数与 win 的托盘闪动绑定,mac 则是未读音讯数的显示绑定。任务栏或程序坞的闪动则是新音讯的推送这一行为的触发。
不过呢具体到页面对话还有三种状况,以 win 为例(mac 同理):

  1. 软件没聚焦点:任务栏闪动,托盘闪动。
  2. 软件聚焦,但此音讯的推送是正在对话的人收回的(对话人 list 的 active):任务栏闪动,托盘不变。
  3. 软件聚焦,但此音讯的推送不是正在对话的人:任务栏不变,托盘闪动。

任务栏解决

这个比较简单,通过传入布尔值,就能够设置了。

win.flashFrame(flag)

托盘解决

win

托盘的闪动实际上就是一个定时器,把托盘的图片和通明图片来回替换,没有通明图片的话能够通过 nativeImage.createFromPath(null))来设置。

this.flickerTimer = setInterval(() => {global.tray.setImage(this.count++ % 2 === 0 ? this.image : nativeImage.createFromPath(null))
}, 500)

当音讯读取结束之后咱们敞开这个定时器,这里须要留神一点是敞开定时器时咱们并不知道托盘的图片是失常的还是通明的,故需再设置一下失常图片。

this.count = 0
if (this.flickerTimer) {clearInterval(this.flickerTimer)
  this.flickerTimer = null
}
global.tray.setImage(this.image)

mac

mac 呢其实也能够这样做,然而呢,一般来说设置红点数就能够。

global.tray.setTitle(messageConfig.news === 0 ? '': messageConfig.news+'') // 右上角托盘的音讯数
app.dock.setBadge(messageConfig.news === 0 ? '': messageConfig.news+'') // 程序坞红点

须要留神的是这两者接管的都是 string 类型,故当音讯数为 0 时,设置为 ”,且这两个办法时 mac 独有的。

Notification 告诉

这个主过程渲染过程都能够调用,基本上算是面向文档开发了,参考官网文档,通常状况下,mac 的音讯推送和 Notification 一样,win 下这是移入托盘显示一个音讯列表,这里简化解决都用 Notification 推送音讯了(懒),当然你也能够用多页本人建设一个相似的音讯列表,前面讲通信的时候看有机会演示一下不。
如果是用主过程的 Notification 咱们会发现 win10 音讯顶端是咱们的 appid,而不是咱们的 productName,这里须要这样解决一下:

const config = {
  .....
  VUE_APP_APPID: env.VUE_APP_APPID
}
主过程
app.setAppUserModelId(config.VUE_APP_APPID)

实现思路

渲染过程通过轮询或者长链接收音讯,在渲染过程依据咱们的对话状态进行音讯的逻辑解决,把是否任务栏闪动,托盘闪动的后果推送到主过程,主过程承受后展现。
总的来说主过程方面是一个被动的状态,负责展现,逻辑解决次要是在渲染过程,实际上咱们在开发的时候过程也不应有过于简单的判断,最好是渲染过程解决好之后,再发送给主过程

代码逻辑

主过程

其余的变动不大,不过这里把 Tray 批改成立 class,主过程 index.js 调用改为 setTray.init(win)
flash 就是咱们的托盘与闪动解决了,它的参数时渲染过程传递的,flashFrame是任务栏闪动管制,messageConfig是推送音讯的信息。当咱们点击推送音讯的 Notification 时,win-message-read告诉渲染过程定位到点击人的对话框。

import {Tray, nativeImage, Menu, app, Notification} from 'electron'
import global from '../config/global'
const isMac = process.platform === 'darwin'
const path = require('path')

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)
  }
}
class createTray {constructor() {
    const iconType = isMac ? '16x16.png' : 'icon.ico'
    const icon = path.join(__static, `./icons/${iconType}`)
    this.image = nativeImage.createFromPath(icon)
    this.count = 0
    this.flickerTimer = null
    if (isMac) {this.image.setTemplateImage(true)
    }
  }
  init(win) {global.tray = new Tray(this.image)
    let contextMenu = Menu.buildFromTemplate([
      {
        label: '显示 vue-cli-electron',
        click: () => {winShow(win)
        }
      }, {
        label: '退出',
        click: () => {app.quit()
        }
      }
    ])
    if (!isMac) {global.tray.on('click', () => {if (this.count !== 0) {win.webContents.send('win-message-read') // 点击闪动托盘时告诉渲染过程
        }
        winShow(win)
      })
    }
    global.tray.setToolTip('vue-cli-electron')
    global.tray.setContextMenu(contextMenu)
  }
  flash({flashFrame, messageConfig}) {global.sharedObject.win.flashFrame(flashFrame)
    if (isMac && messageConfig) { // mac 设置未读音讯数
      global.tray.setTitle(messageConfig.news === 0 ? '': messageConfig.news+'')
      app.dock.setBadge(messageConfig.news === 0 ? '': messageConfig.news+'')
    }
    if (messageConfig.news !== 0) { // 总音讯数
      if (!this.flickerTimer && !isMac) { // win 托盘闪动
        this.flickerTimer = setInterval(() => {global.tray.setImage(this.count++ % 2 === 0 ? this.image : nativeImage.createFromPath(null))
        }, 500)
      }
      if (messageConfig.body) { // 音讯 Notification 推送
        const n = new Notification(messageConfig)
        n.show()
        n.once('click', () => {winShow(global.sharedObject.win)
          global.sharedObject.win.webContents.send('win-message-read', messageConfig.id)
          n.close()})
      }
    } else { // 勾销托盘闪动,还原托盘
      this.count = 0
      if (this.flickerTimer) {clearInterval(this.flickerTimer)
        this.flickerTimer = null
      }
      global.tray.setImage(this.image)
    }
  }
}

export default new createTray()

因为音讯呢是由渲染过程推送过去的,services/ipcMain.js增加对应的监听及 flash 的调用

ipcMain.handle('win-message', (_, data) => {setTray.flash(data)
})

渲染过程(Vue3)

<div class="tary">
  <div class="btn"><a-button type="primary" @click="pushNews()"> 推送音讯 </a-button></div>
  <section class="box">
    <a-list class="list" :data-source="list">
      <template #renderItem="{item, index}">
        <a-list-item
          class="item"
          :class="{active: item.id === activeId}"
          @click="openList(index)"
        >
          <a-badge :count="item.news">
            <a-list-item-meta
              :description="item.newsList[item.newsList.length - 1]"
            >
              <template #title>
                <span>{{item.name}}</span>
              </template>
              <template #avatar>
                <a-avatar
                  src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
                />
              </template>
            </a-list-item-meta>
          </a-badge>
        </a-list-item>
      </template>
    </a-list>
    <div class="messageList">
      <ul class="messageBox" v-if="activeId">
        <li v-for="(item, index) in messageList" :key="index">
          {{item}}
        </li>
      </ul>
    </div>
  </section>
</div>

import {defineComponent, reactive, toRefs, onMounted, onUnmounted, computed} from 'vue'
import Mock from 'mockjs'


export default defineComponent({setup() {
    申明一个对话列表 list,`activeId` 是以后正在对话的人的 id,`lastId` 为最初音讯推送人的 id。const state = reactive({
      activeId: '',
      list: [{name: Mock.mock('@cname'),
        id: Mock.mock('@id'),
        news: 0,
        newsList: []}, {name: Mock.mock('@cname'),
        id: Mock.mock('@id'),
        news: 0,
        newsList: []}, {name: Mock.mock('@cname'),
        id: Mock.mock('@id'),
        news: 0,
        newsList: []}]
    })
    onMounted(() => {
    承受主过程传递的音讯读取,如果有对应 id,定位到对应 id,无则定位到第一个未读音讯
    window.ipcRenderer.on('win-message-read', (_, data) => {const index = data ? state.list.findIndex(s => s.id === data) : state.list.findIndex(s => s.news !== 0)
      ~index && openList(index)
    })
    })
    onUnmounted(() => {window.ipcRenderer.removeListener('win-message-read')
    })
    news 是总音讯数。const news = computed(() => state.list.reduce((pre, cur) => pre + cur.news, 0))
    const messageList = computed(() => state.list.find(s => s.id === state.activeId)['newsList'])

    function setMessage(obj) { // 向主过程推送音讯
      window.ipcRenderer.invoke('win-message', obj)
    }
    function openList(index) { // 点击对话框
      state.activeId = state.list[index].id
      state.list[index].news = 0
      setMessage({
        flashFrame: false,
        messageConfig: {news: news.value}
      })
    }
    function pushNews(index) { // 模仿音讯的推送,index 为固定人的音讯发送
      let flashFrame = true
      const hasFocus = document.hasFocus()
      const puahIndex = index != null ? index : getRandomIntInclusive(0, 2)
      const item = state.list[puahIndex]
      if (state.activeId !== item.id) { // 页面对话的状况解决
        item.news += 1
        if (hasFocus) {flashFrame = false}
      } else {if (hasFocus) {flashFrame = false}
      }
      item.newsList.push(Mock.mock('@csentence(20)'))
      setMessage({
        flashFrame,
        messageConfig: {
          title: item.name,
          id: item.id,
          body: item.newsList[item.newsList.length - 1],
          news: news.value
        }
      })
    }
    function getRandomIntInclusive(min, max) {min = Math.ceil(min)
      max = Math.floor(max)
      return Math.floor(Math.random() * (max - min + 1)) + min
    }
    return {...toRefs(state),
      messageList,
      openList,
      pushNews
    }
  }
})

测验

  1. 软件没聚焦点:任务栏闪动,托盘闪动。
咱们加个定时器,而后把软件敞开到托盘。setTimeout(() => {pushNews()
}, 5000)
  1. 软件聚焦,但此音讯的推送是正在对话的人收回的(对话人 list 的 active):任务栏闪动,托盘不变。
pushNews 传入 0,固定音讯为第一人收回的,那么咱们在定时器产生前点击第一个人使其处于 active。setTimeout(() => {pushNews(0)
}, 5000)
  1. 软件聚焦,但此音讯的推送不是正在对话的人:任务栏不变,托盘闪动。
同上,咱们在定时器产生前点击除第一个人以外的其他人使其处于 active。

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

正文完
 0