Electron | 简易音乐播放器入门

本文基于慕课网课程:Electron开发仿网易云播放器,通过该播放器的制作来入门Electron框架。
吐槽一下名字:根本就不是仿网易云,不需要这么浮夸的名字。课程内容不错,对于入门Electron来说挺好的。

❗本播放器持续更新中:https://www.jianshu.com/p/9abc0a40d39f 已实现网易云音乐和QQ音乐的在线搜索和播放~

一、 Electron 基本文档:

https://electronjs.org/docs。先阅读后就可以实现一个简易到electron入门helloWorld程序。

二、主进程和渲染进程

Chromium的基本原理,用Chrome来举例,主进程就是浏览器的进程,每个tab都是一个渲染进程。

 

主进程和渲染进程

1. 主进程 Main Process

  • 可以使用与系统对接的Electron API,创建菜单、上传文件。
  • 创建渲染进程Renderer Process
  • 全面支持Node.js
  • 只有一个,作为程序的入口

2. 渲染进程 Renderer Process

  • 可以有多个,每个对应一个窗口
  • 每个都是单独的进程
  • 全面支持Node.jsDOM API
  • 可以使用一部分Electron API

3. Demo结构对应

Demo中有三个文件:

  • main.js对应主进程、
  • renderer.js对应渲染进程、
  • index.html渲染页面。
    main.js中通过new BrowserWindow()新建一个浏览器窗口(可以理解为开辟了一个单独的渲染进程),然后通过此实例调用loadFile()方法加载页面。在渲染页面中,引入renderer.js来在渲染进程中执行js代码。

4. 进程间通信

  • 使用IPC(interprocess communication)通信(与chromium一致)

     

  • ipcRenderer 渲染进程
// renderer.js
const { ipcRenderer } = require('electron')
window.addEventListener('DOMContentLoaded', ()=>{
  ipcRenderer.send('message', 'hello from renderer.js')
  ipcRenderer.on('reply', (event, arg) => {
    document.getElementById('message').innerText = arg
  })
})
  • ipcMain 主进程
// main.js
const { app, BrowserWindow, ipcMain } = require('electron')
app.on('ready', () => {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true
    }
  })
  mainWindow.loadFile('index.html')
  ipcMain.on('message', (event, arg) => {
    console.log(arg)
    // event.sender.send('reply', 'hello from main') // 可以用event对象的sender属性获取发送者
    mainWindow.send('reply', 'hello from main') // 也可以用原来的窗口对象来发送
  })
})

三、功能流程图与目录结构

👏从这里开始写我们的简易本地音乐播放器。

 

音乐播放器的功能流程图

 

文件目录结构

四、添加音乐窗口

1. 基本逻辑

就是在点击按钮到添加音乐按钮到时候开辟一个新窗口。
只需要使用我们之前的IPC通信send发送和on监听,结合新建BrowserWindow实例即可,但是那么写BrowserWindow会多出一些没必要的代码,所以咱们用一个窗口类来减少重复代码。

2. 窗口类

const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')

class AppWindow extends BrowserWindow {
  constructor(config, fileLocation) {
    const basicConfig = {
      width: 800,
      height: 600,
      webPreferences: {
        nodeIntegration: true
      }
    }
    // const mergeConfig = Object.assign(basicConfig, config)
    const mergeConfig = { ...basicConfig, ...config } // es6
    super(mergeConfig)
    this.loadFile(fileLocation)
  }
}

app.on('ready', () => {
  const mainWindow = new AppWindow({}, './renderer/index.html')
  ipcMain.on('add-music-window', (event, arg) => {
    const addWindow = new AppWindow(
      {
        width: 600,
        height: 400,
        parent: mainWindow
      },
      './renderer/add.html'
    )
  })
})

3. Dialog模块选择本地文件

我们需要在点击选择音乐的时候打开本地的文件选择器,这里需要Electron的对话框API。

Dialog: 显示用于打开和保存文件、警报等的本机系统对话框。

  • $函数(减少代码重复)
exports.$ = (id) => document.getElementById(id)
  • dialog
    由于dialog是属于主进程能调用的API,因此我们需要在渲染进程点击提交申请的时候用ipcRenderer.send发送给主进程,主进程ipcMain.on监听到事件后,调用即可。
ipcMain.on('open-music-file', () => {
      dialog.showOpenDialog(
        {
          properties: ['openFile', 'multiSelections'], // 启用打开文件和多选
          filters: [{ name: 'Music', extensions: ['mp3'] }] // 格式:mp3
        },
        files => {
          console.log(files)
        }
      )
    })
  • 文件列表
    收到文件列表后用dom操作显示出来
const renderListHTML = pathes => {
  const musicList = $('musicList')
  const musicItemHTML = pathes.reduce((html, music) => {
    html += `<li class="list-group-item">${path.basename(music)}</li>`
    return html
  }, '')
  musicList.innerHTML = `<ul class="list-group">${musicItemHTML}</ul>`
}

ipcRenderer.on('selected-file', (event, path) => {
  if (Array.isArray(path)) {
    renderListHTML(path)
  }
})

4. 持久化数据存储 - electron store

我们还需要把歌单缓存到本地,所以需要持久化数据。

  • 方法
const Store = require('electron-store') // 引入
const store = new Store() // 实例化
store.set('key','value')
store.get('key')
store.delete('key')
  • 存储json的地址
console.log(app.getPath('userData'))
  • 定制化存储类
    对store进行封装,可以更加方便地使用,减少冗余代码。
    这里面的一些细节和逻辑对我很有帮助!
const Store = require('electron-store')
const uuidv4 = require('uuid/v4')
const path = require('path')
class DataStore extends Store {
  constructor(settings) {
    super(settings)
    this.tracks = this.getTracks()
  }
  saveTracks() {
    this.set('tracks', this.tracks)
    return this // 方便链式调用
  }
  getTracks() {
    return this.get('tracks') || []
  }
  addTracks(tracks) {
    const tracksWithProps = tracks
      .map(track => {
        return {
          id: uuidv4(), // 用uuid来做id
          path: track,
          filename: path.basename(track)
        }
      })
      .filter(track => {
        const currentTracksPath = this.getTracks().map(track => track.path)
        return currentTracksPath.indexOf(track.path) < 0
      }) // 挺不错的去重逻辑
    this.tracks = [...this.tracks, ...tracksWithProps]
    return this.saveTracks()
  }
}
module.exports = DataStore
  • 存储方法
    add页面到渲染进程监听提交按钮事件,点击后使用ipcRenderer.send发送到主进程,主进程收到之后调用DataStore的实例store的方法addTracks把数据保存到本地。
ipcMain.on('add-tracks', (event, tracks) => {
  const updatedTracks = store.addTracks(tracks).getTracks()
  mainWindow.send('get-tracks', updatedTracks)
})

5. 阶段成果

五、主窗口

1. 渲染列表

和添加音乐窗口列表一样,接收到主进程到信息后,渲染进程操作DOM渲染列表。

const renderListHTML = tracks => {
  const tracksList = $('tracksList')
  const tracksListHTML = tracks.reduce((html, track) => {
    html += `<li class="row music-track list-group-item d-flex justify-content-between align-items-center">
      <div class="col-10">
        <i class="fa fa-music mr-2 text-secondary"></i>
        <b>${track.filename}</b>
      </div>
      <div class="col-2">
        <i class="fa fa-play mr-3 "></i>
        <i class="fa fa-trash"></i>
      </div>
    </li>`
    return html
  }, '')
  const emptyTrackHTML = `<div class="alert alert-primary">还没有添加任何歌曲</div>`
  tracksList.innerHTML = tracks.length
    ? `<ul class="list-group">${tracksListHTML}</ul>`
    : emptyTrackHTML
}

ipcRenderer.on('get-tracks', (event, tracks) => {
  renderListHTML(tracks)
})

2. 播放音乐

  • 音乐操作API
    • HTML <audio> 标签
    • JS HTMLAudioElement 对象
  • DOM存储信息
    • HTML标签data-* 属性存储
    • JS HTMLElement dataset属性读取
  • 播放按钮事件冒泡代理
    • 由于播放按钮数量多,一个一个添加EventListener很浪费性能,所以用了事件冒泡机制来节省性能。
// 在原先HTML的播放按钮图标那里加上data-id="${track.id}" 
// 下面是播放的核心操作
$('tracksList').addEventListener('click', event => {
  event.preventDefault()
  const { dataset, classList } = event.target
  const id = dataset && dataset.id
  console.log(id, classList)
  if (id && classList.contains('fa-play')) {
    // 播放音乐
    currentTrack = allTracks.find(track => track.id === id)
    musicAudio.src = currentTrack.path
    musicAudio.play()
    classList.replace('fa-play','fa-pause')
  }
})

3.完善播放

  • 按钮事件汇总

     

    按钮事件汇总

  • 代码:
    得益于事件代理,一切都变得很简单
$('tracksList').addEventListener('click', event => {
  event.preventDefault()
  const { dataset, classList } = event.target
  const id = dataset && dataset.id
  if (id && classList.contains('fa-play')) {
    // 播放音乐
    if (currentTrack && currentTrack.id === id) {
      // 继续播放
      musicAudio.play()
    } else {
      // 播放新歌曲,注意还原之前的图标
      currentTrack = allTracks.find(track => track.id === id)
      musicAudio.src = currentTrack.path
      musicAudio.play()
      const prevPlayElement = document.querySelector('.fa-pause')
      prevPlayElement &&
        prevPlayElement.classList.replace('fa-pause', 'fa-play')
    }
    classList.replace('fa-play', 'fa-pause')
  } else if (id && classList.contains('fa-pause')) {
    // 暂停音乐
    musicAudio.pause()
    classList.replace('fa-pause', 'fa-play')
  } else if (id && classList.contains('fa-trash')) {
    // 删除音乐
    ipcRenderer.send('delete-track', id)
  }
})

4. 阶段成果

六、音乐器播放状态

1. 显示时间

主要是两个事件,loadedmetadata:当指定当音频视频数据已经加载完毕当时候;timeupdate:当音乐的播放时间更新的时候。
以及两个属性,audio.duration():获取音乐的时间长度;audio.currentTime:获取当前播放时间。

const renderPlayerHTML = (name, duration) => {
  const player = $('player-status')
  const html = `<div class="col font-weight-bold">
                  正在播放: ${name}
                </div>
                <div class="col">
                  <span id="current-seeker"> 00:00 </span> / ${duration}
                </div>`
  player.innerHTML = html
}

const updatedProgressHTML = currentTime => {
  const seeker = $('current-seeker')
  seeker.innerHTML = currentTime
}

ipcRenderer.on('get-tracks', (event, tracks) => {
  allTracks = tracks
  renderListHTML(tracks)
})

musicAudio.addEventListener('loadedmetadata', () => {
  // 开始渲染播放器状态
  renderPlayerHTML(currentTrack.filename, musicAudio.duration)
})

musicAudio.addEventListener('timeupdate', () => {
  // 更新播放器状态
  updatedProgressHTML(musicAudio.currentTime)
})

2. 友好化时间显示

exports.convertDuration = time => {
  // 计算分钟
  const minutes = Math.floor(time / 60)
    .toString()
    .padStart(2, '0')
  const seconds = Math.floor(time - minutes * 60)
    .toString()
    .padStart(2, '0')
  return `${minutes}:${seconds}`
}

3. 进度条

用bootstrap的进度条,只需要更改style的width和innerHTML就行了。

<div
  class="progress-bar progress-bar-success"
  id="player-progress"
  role="progressbar"
  style="width: 0%;"
>
  0%
</div>
const updatedProgressHTML = (currentTime, duration) => {
  const progress = Math.floor((currentTime / duration) * 100)
  const bar = $('player-progress')
  bar.innerHTML = progress + '%'
  bar.style.width = progress + '%'
  const seeker = $('current-seeker')
  seeker.innerHTML = convertDuration(currentTime)
}

4. 此阶段成果

七、打包

1. 打包方式

  • 手动打包
  • electron builder
  • electron packager
    这里选择 electron builder

2. 配置文件

"build": {
    "appId": "simpleMusicPlayer",
    "mac": {
      "category": "public.app-category.productivity"
    },
    "dmg": {
      "background": "build/appdmg.png",
      "icon": "build/icon.icns",
      "iconSize": 100,
      "contents": [
        {
          "x": 380,
          "y": 280,
          "type": "link",
          "path": "/Applications"
        },
        {
          "x": 110,
          "y": 280,
          "type": "file"
        }
      ],
      "window": {
        "width": 500,
        "height": 500
      }
    },
    "linux": {
      "target": [
        "AppImage",
        "deb"
      ]
    },
    "win": {
      "target": "nsis",
      "icon": "build/icon.ico"
    }
  }

3. 精简方法

https://imweb.io/topic/5b6817b5f6734fdf12b4b09c

 

3人点赞

 

前端相关

 



作者:格致匠心
链接:https://www.jianshu.com/p/e2cbf5edb520
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值