前端学习

Electron | 简易音乐播放器入门🎵

2019-07-14  本文已影响11人  格致匠心

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

一、 Electron 基本文档:

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

二、主进程和渲染进程

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


主进程和渲染进程

1. 主进程 Main Process

2. 渲染进程 Renderer Process

3. Demo结构对应

Demo中有三个文件:

4. 进程间通信

// 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
  })
})
// 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)
ipcMain.on('open-music-file', () => {
      dialog.showOpenDialog(
        {
          properties: ['openFile', 'multiSelections'], // 启用打开文件和多选
          filters: [{ name: 'Music', extensions: ['mp3'] }] // 格式:mp3
        },
        files => {
          console.log(files)
        }
      )
    })
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')
console.log(app.getPath('userData'))
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
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. 播放音乐

// 在原先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. 打包方式

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": "squirrel",
      "icon": "build/icon.ico"
    }
  },
  "keywords": [
    "Electron",
    "quick",
    "start",
    "tutorial",
    "demo"
  ],

3. 精简方法

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

上一篇 下一篇

猜你喜欢

热点阅读