从零搭建Electron跨平台桌面IM应用【包含托盘,通知,进程间通信,音效, etc...】你想问的,都在这里!

2,365 阅读3分钟

如何初始化项目在这里不再赘述,请看我的掘金处女作 基于Electron+vue的跨平台实践初探

此处假设你已经读完我的初探文章或本身已具备初始化electron项目的能力

托盘如何生成?如何设置托盘标题和通知

先看官方文档

let tray

app.whenReady().then(() => {
const icon = nativeImage.createFromPath('path/to/asset.png')
tray = new Tray(icon)

// 注意: 你的 contextMenu, Tooltip 和 Title 代码需要写在这里!
const contextMenu = Menu.buildFromTemplate([
{ label: 'Item1', type: 'radio' },
{ label: 'Item2', type: 'radio' },
{ label: 'Item3', type: 'radio', checked: true },
{ label: 'Item4', type: 'radio' }
])

tray.setContextMenu(contextMenu)
})
// 鼠标移上去的横幅
tray.setToolTip('This is my application')
// 这个玩意儿不知道是哪块
tray.setTitle('This is my title')

技术要点

  1. 注意看托盘的变量 tray 声明在了顶层作用域内,这样做的原因是 如果声明在函数作用域内,因为electron的垃圾收集机制,会被回收掉,表现效果为 托盘图标出现在任务栏,然后很快就消失了。
  2. 托盘图标是用nativeImage模块创建的。
  3. 托盘图标的图片文件路径: 可以使用内置变量 __static 如果是按我的教程进行的初始化,__static 对应的是 public 文件夹

通知

通知分为两种 一种是electron提供的模块 Notification 一种是HTML5的notificationAPI HTML5的文档很多了,不再赘述,这里主要讲electron提供的 Notification 模块

先看我代码内的实现

const notify = new Notification({
  icon,
  title: `您有来自${options.from_id}的新消息通知`,
  subtitle: '苍穹·IM',
  body: body
})
notify.show()
notify.on('click', () => {
  if (timer) {
    clearInterval(timer)
    timer = null
    const icon = nativeImage.createFromPath(__static + '/app.png')
    tray.setImage(icon)
  }
  win.setAlwaysOnTop(true)
  win.restore()
  win.setAlwaysOnTop(false)
})

有关timer那部分代码可以忽略,我们下一部分再讲。这里重点关注notify实例点击事件内的两行代码

// 使用该方法将窗口置顶

win.setAlwaysOnTop(true) //避免窗口被最小化的情况,恢复至原状 win.restore() //使用该方法将窗口取消总是置顶 win.setAlwaysOnTop(false)

为什么这样设计呢?因为win.show()或者win.focus()均不能完成将窗口置于顶层的重任。也就无法实现点击消息弹出窗口的效果

进程间通信

低版本的electron默认是可以在渲染进程内直接通过es2015模块引入ipcRender的,高版本则默认关闭了该选项,请自行查看官方文档描述

文档1 上下文隔离

文档2 进程间通信

看完文档后也就基本明白了,为什么进程间通信需要设计为通过预加载脚本的方式声明方法了,此处注意的点

  1. preloadjs引入的时机,官方文档是在new BrowserWindow的时候,在传入的option内的 webPreferences 内通过 preload 属性 声明文件路径, 此处依然可以借助__static进行引入,为什么不在index.html内直接引入呢? 因为preload内的contextBridge需要在创建窗口完成后才会存在。

消息音效与任务栏图标闪烁

消息音效很简单,app.vue内写一个display:noneaudio标签

// template
<video :src="src" id="beep" style="display:none"></video>
// script
 src: 'http://localhost:56566/video?name=beep.mp3'
// 此处音效地址在一节进行说明

将audio实例注册在vuex上

state: {
    ...
    beep: null,
    ...
  },
  getters: {
     ...
    beep: state => state.beep,
    ...
  },
  mutations: {
   ...
    PLAY_BEEP (state) {
      if (state.beep) {
        state.beep.play()
      }
    },
    SET_BEEP (state, beep) {
      state.beep = beep
    },
    ...
  }
this.SET_BEEP(document.getElementById('beep'))

在收到新消息时调用PLAY_BEEP()即可

任务栏图标闪烁,本质上是两个托盘图片,通过setInterval不断切换实现的【还记得上面的timer吗?】

// background.js 主进程内
let timer
// 托盘这里需要注册点击事件,点击将窗口还原,此时需要判断窗口是否为闪烁状态,如果是,清除定时器,设置托盘图标
tray.on('click', () => {
  if (timer) {
    clearInterval(timer)
    timer = null
    const icon = nativeImage.createFromPath(__static + '/app.png')
    tray.setImage(icon)
  }
  if (win.isMinimized()) {
    win.restore()
    win.focus()
  } else {
    win.minimize()
  }
})
// 主进程注册消息事件
ipcMain.on('newMsg', (event, options) => {
    win.flashFrame(true)
    // 实现托盘图标闪烁
    const icon = nativeImage.createFromPath(__static + '/app.png')
    const icon2 = nativeImage.createFromPath(__static + '/transparent.png')
    let flag = true
    timer = setInterval(() => {
      if (flag) {
        tray.setImage(icon2)
        flag = false
      } else {
        tray.setImage(icon)
        flag = true
      }
    }, 400)
    let body = options.msg_body.text
    if (options.msg_type === 'image') {
      body = '[图片]'
    } else if (options.msg_type === 'file') {
      body = '[文件]'
    }
    const notify = new Notification({
      icon,
      title: `您有来自${options.from_id}的新消息通知`,
      subtitle: '苍穹·IM',
      body: body
    })
    notify.show()
    notify.on('click', () => {
       // 点击消息打开弹窗,设置托盘图标
      if (timer) {
        clearInterval(timer)
        timer = null
        const icon = nativeImage.createFromPath(__static + '/app.png')
        tray.setImage(icon)
      }
      win.setAlwaysOnTop(true)
      win.restore()
      win.setAlwaysOnTop(false)
    })
  })

托盘图标闪烁与音效是之前章节的有机结合。按部就班即可实现

css内背景图片路径与渲染进程内资源路径开发环境和打包后一致化

这里是为了解决开发环境可以看到图片,打包后图片路径找不到的问题

开发环境是通过dev-server提供的http协议的localhost地址 打包后是自定义的app协议,这个是问题出现的关键原因

关于css内的静态资源,vue-cli官方文档有详细描述,这里不再赘述,上链接自行查看

vue-cli 官方文档 css引用静态资源

vue-cli 官方文档 处理静态资源

此处我查阅了大量文档,包括修改css引用的publicPath 等,均不生效,如果有人有过成功经验,可以评论区留言探讨

我的解决方案: 在主进程通过引入的方式,跑一个简易的文件服务器,来代理项目内资源的访问

// 基于express
// background.js
import './express/index'
// express/index
const express = require('express')
const app = express()
const port = 56566
const path = require('path')
app.get('/img', (req, res) => {
  if (req.query.name) {
    res.sendFile(path.join(__static, 'static/img/' + req.query.name))
  } else {
    res.sendFile(path.join(__static, 'static/img/default.png'))
  }
})
app.get('/video', (req, res) => {
  if (req.query.name) {
    res.sendFile(path.join(__static, 'static/video/' + req.query.name))
  }
})
app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})
// css
background: url('http://localhost:56566/img?name=login_bg.jpg');

这样就实现了开发与打包的一致,唯一的缺点就是服务端口被占用还没做处理,毕竟是小概率事件

多图预警

登录页是时空隧道穿梭特效,gif图就不放了

image.png

image.png

这是我写过的最长的一篇了。。如果有不同意见或者我写的有不清楚的地方,欢迎评论区进行留言探讨。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿