electron 基于JMessage websdk 扩展实现web端发送语音及自动更新的功能

134 阅读5分钟

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

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

今天就是站在上两篇文章的肩膀上,继续深入,实现基于electron和JMessage web sdk的IM应用,今天的主题是录音的实现和打包编译相关的知识。

目录结构

|-- im
    |-- vue.config.js
    |-- yarn.lock
    |-- dist_electron
    |-- public
    |   |-- index.html
    |   |-- jmessage-sdk-web.2.6.0.min.js
    |   |-- preload.js
    |   |-- static
    |       |-- img
    |       |-- video
    |-- src
        |-- App.vue
        |-- background.js
        |-- main.js
        |-- common
        |   |-- ajax.js
        |   |-- api.js
        |   |-- interceptor.js
        |   |-- jimInit.js
        |   |-- service.js
        |-- components
        |   |-- download.vue
        |   |-- install.vue
        |   |-- loading.vue
        |   |-- title-bar
        |       |-- index.vue
        |-- express
        |   |-- index.js
        |-- router
        |   |-- index.js
        |-- store
        |   |-- index.js
        |-- style
        |   |-- index.css
        |   |-- font
        |-- views
            |-- home
            |   |-- index.vue
            |   |-- myInput.vue
            |   |-- quickReply.vue
            |-- login
                |-- index.vue

结构大概就是这样,很简单就实现了一个基础的消息应用,拿来做基础的客服消息是够用了。

调用pc麦克风实现录音的实现

为了降低使用的心智负担,这里跳过原生试错的阶段,直接上采用 js-audio-recorder 的版本

yarn add js-audio-recorder
// template 一个声音图标
<div @click="startRecord" class="iconfont icon-voice"></div>
// script
import Recorder from 'js-audio-recorder'
...
methods: {
...
stopRecord () {
      this.recordStart = false
      const fileBlob = this.recorder.getWAVBlob()
      const file = new File([fileBlob], nanoid() + '.wav', { type: 'audio/x-wav', lastModified: Date.now() })
      const formData = new FormData()
      formData.append(file.name, file)
      this.sendVoice(formData, this.recorder.duration)
      this.recorder.destroy().then(() => {
        this.recorder = null
      })
    },
    startRecord () {
      Recorder.getPermission().then(() => {
        console.log('给权限了')
        this.recorder = new Recorder({
          numChannels: 2
        })
        this.recordStart = true
        this.recorder.start().then(() => {
          // 开始录音
        }, (error) => {
          this.$message.error(error.name + ':' + error.message)
          // 出错了
          console.log(`${error.name} : ${error.message}`)
        })
      }, (error) => {
        console.log(`${error.name} : ${error.message}`)
        this.$message.error('请在系统->隐私->麦克风开启授权')
      })
    },
}

这里采用的实现是,每次点击前,进行recorder的初始化,录音完成进行销毁。

在录音前,进行权限请求,如果是electron的项目 windows下,需要在系统设置内统一处理

image.png

如果是mac,则需要先通过electron提供的方法进行请求授权. 这里有一篇参考文章 www.electron.build/configurati… 需要先进行授权,否则调用录音会造成应用崩溃。

附上 js-audio-recorder 的文档 recorder.api.zhuyuntao.cn/

JMessage的websdk默认不支持发送音频,这里我们需要借助消息体协议的extra扩展字段,扩展我们的消息类型,

 sendVoice (file, duration) {
      this.activeRoom.JIMinstance.sendSingleFile({
        target_username: this.activeUserName,
        appkey: this.activeRoom.appkey,
        target_nickname: this.activeRoom.nickName,
        file: file,
        extras: {
          type: 'wav',
          duration: duration
        },
        need_receipt: false
      }).onSuccess((data, msg) => {
        this.PUSH_MESSAGE_LIST({ msg_id: msg.msg_id, ...msg.content, nanoid: nanoid() })
      })
    },

通过sendFileAPI实现,extras内对文件类型进行标识,拿到会话后判断文件类型进行处理播放即可

自动更新相关。网上文章都是四处copy,没有完整的从头到尾讲明白的,这里特意做出说明

这里需要借助electron-updater

// background.js  主进程内写自动更新相关的代码
import { autoUpdater } from 'electron-updater'

在这里,我们需要实现的功能是:应用打开后,通过定时器轮询是否有新的更新,如果有新的更新,则弹出更新提示,点击确定才下载更新,下载更新需要提示进度条,更新完成后提示安装。

在这里,我们需要分清楚,主进程我们需要做什么,渲染进程我们需要做什么对吧

毫无疑问, 交互提示需要显示在渲染进程,检测更新的实现需要在主进程,而定时器轮询需要写在渲染进程。

按思路我们来一步步实现,首先来看主进程

autoUpdater默认是会自动下载更新的,我们先设置为不自动下载

 autoUpdater.autoDownload = false
 // 这个是自动更新的地址
 autoUpdater.setFeedURL('https://pic.qy566.com/im/')

应用更新依赖的是*.exe及latest.yml,所以是无需后台代码的,我这里是借助了阿里oss加cdn分发进行实现。这里打包完成后,是需要手动上传oss的,也可以通ci自动上传,我们下一章节再讨论如何实现打包构建完成后自动上传oss。

autoUpdater会有一些事件抛出来,我们需要监听对应的事件,处理相应的业务逻辑

// text展示消息内容,通过type处理业务逻辑
const message = {
    error: { text: '检查更新出错', type: 'error' },
    checking: { text: '正在检查更新……', type: 'checking' },
    updateAva: { text: '检测到新版本', type: 'updateAva' },
    updateNotAva: { text: '现在使用的就是最新版本,不用更新', type: 'updateNotAva' }
  }
// 通过main进程发送事件给renderer进程,提示更新信息
function sendUpdateMessage (text) {
  win.webContents.send('downloadMessage', text)
}
// 各类业务事件
autoUpdater.on('error', () => {
    sendUpdateMessage(message.error)
  })
  autoUpdater.on('checking-for-update', function () {
    sendUpdateMessage(message.checking)
  })
  autoUpdater.on('update-available', function (info) {
    sendUpdateMessage(message.updateAva)
  })
  autoUpdater.on('update-not-available', function (info) {
    sendUpdateMessage(message.updateNotAva)
  })

  // 更新下载进度事件
  autoUpdater.on('download-progress', function (progressObj) {
    win.setProgressBar(progressObj.percent / 100)
  })
  autoUpdater.on('update-downloaded', function (event, releaseNotes, releaseName, releaseDate, updateUrl, quitAndUpdate) {
     // 收到更新事件开始更新
    ipcMain.on('isUpdateNow', (e, arg) => {
      console.log(arguments)
      console.log('开始更新')
      // some code here to handle event
      autoUpdater.quitAndInstall()
    })
    // 下载完成后发送是否更新的事件
    win.webContents.send('isUpdateNow')
  })

这里核心事件有几个,一个是update-available 这个是有新的更新的事件 一个是 download-progress 下载进度事件 还有一个 update-downloaded 更新下载完成事件

上面只是监听了autoUpdater的事件,那么如何触发呢? 这就需要借助autoUpdater提供的方法

// 主进程注册下载更新事件
ipcMain.on('downloadUpdate', () => {
    autoUpdater.downloadUpdate()
})
// 主进程注册自动更新事件
ipcMain.on('checkForUpdate', () => {
// 执行自动更新检查
autoUpdater.checkForUpdates()
})

主进程代码就是这样了,完整实现如下:

// 自动更新

function updateHandle () {
  const message = {
    error: { text: '检查更新出错', type: 'error' },
    checking: { text: '正在检查更新……', type: 'checking' },
    updateAva: { text: '检测到新版本', type: 'updateAva' },
    updateNotAva: { text: '现在使用的就是最新版本,不用更新', type: 'updateNotAva' }
  }
  autoUpdater.autoDownload = false
  autoUpdater.setFeedURL('https://pic.qy566.com/im/')
  autoUpdater.on('error', () => {
    sendUpdateMessage(message.error)
  })
  autoUpdater.on('checking-for-update', function () {
    sendUpdateMessage(message.checking)
  })
  autoUpdater.on('update-available', function (info) {
    sendUpdateMessage(message.updateAva)
  })
  autoUpdater.on('update-not-available', function (info) {
    sendUpdateMessage(message.updateNotAva)
  })

  // 更新下载进度事件
  autoUpdater.on('download-progress', function (progressObj) {
    win.setProgressBar(progressObj.percent / 100)
  })
  autoUpdater.on('update-downloaded', function (event, releaseNotes, releaseName, releaseDate, updateUrl, quitAndUpdate) {
    ipcMain.on('isUpdateNow', (e, arg) => {
      console.log(arguments)
      console.log('开始更新')
      // some code here to handle event
      autoUpdater.quitAndInstall()
    })
    win.webContents.send('isUpdateNow')
  })
  ipcMain.on('downloadUpdate', () => {
    autoUpdater.downloadUpdate()
  })
  ipcMain.on('checkForUpdate', () => {
    // 执行自动更新检查
    autoUpdater.checkForUpdates()
  })
}

// 通过main进程发送事件给renderer进程,提示更新信息
function sendUpdateMessage (text) {
  win.webContents.send('downloadMessage', text)
}

渲染进程,在App.vue内进行事件处理和注册

preload的部分不再赘述,默认已经看过我之前的系列文章内的进程间通信和上下文隔离相关内容

首先需要调用一次,然后建立定时器轮询

methods:
...
checkForUpdate () {
  window.mainAPI.downloadMessage((evt, message) => {
    console.log(message)
    if (message.type === 'updateAva') {
      this.$confirm('检测到应用有新的版本,是否更新?', '新版本提示', {
        showClose: false,
        showCancelButton: false,
        closeOnClickModal: false,
        closeOnPressEscape: false
      }).then(() => {
        this.show = true
        window.mainAPI.downloadUpdate()
      })
    }
  })
  window.mainAPI.isUpdateNow(() => {
    this.$confirm('新版本已经下载完成,是否更新?', '更新提示', {
      showClose: false,
      showCancelButton: false,
      closeOnClickModal: false,
      closeOnPressEscape: false
    }).then(() => {
      window.mainAPI.UpdateNow()
    })
  })
  window.mainAPI.checkForUpdate()
}
...

到这里就完成了定时器的更新。这里this.show控制了一个全局的loading,loading完整代码如下

<template>
  <div class="loading-container" v-if="show">
    <div class="text">正在努力下载中。。。</div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
  name: 'install-vue',
  props: {
    show: {
      type: Boolean,
      default: null
    }
  },
  data () {
    return {
    }
  },
  computed: {
    ...mapGetters(['connectState'])
  },
  created () {},
  mounted () {
  },
  watch: {
  },
  methods: {
  },
  components: {}
}
</script>

<style lang="less" scoped>
.loading-container{
  position: absolute;
  top: 0%;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
  background-image: url('http://localhost:56566/img?name=loading.gif');
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  background-color: #000;
  .text{
    font-size: 30px;
    text-align: center;
    line-height: 200px;
  }
}
</style>

图片在这里:

loading.gif

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