Electron | 简易音乐播放器入门🎵
2019-07-14 本文已影响11人
格致匠心
本文基于慕课网课程:Electron开发仿网易云播放器,通过该播放器的制作来入门Electron框架。
吐槽一下名字:根本就不是仿网易云,不需要这么浮夸的名字。课程内容不错,对于入门Electron来说挺好的。
一、 Electron 基本文档:
https://electronjs.org/docs。先阅读后就可以实现一个简易到electron入门helloWorld程序。
二、主进程和渲染进程
Chromium的基本原理,用Chrome来举例,主进程就是浏览器的进程,每个tab都是一个渲染进程。
![](https://img.haomeiwen.com/i12775720/58998008a322e6a6.png)
1. 主进程 Main Process
- 可以使用与系统对接的
Electron API
,创建菜单、上传文件。 - 创建渲染进程
Renderer Process
- 全面支持
Node.js
- 只有一个,作为程序的入口
2. 渲染进程 Renderer Process
- 可以有多个,每个对应一个窗口
- 每个都是单独的进程
- 全面支持
Node.js
和DOM 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') // 也可以用原来的窗口对象来发送
})
})
三、功能流程图与目录结构
👏从这里开始写我们的简易本地音乐播放器。
![](https://img.haomeiwen.com/i12775720/efe2033c97d0579f.png)
![](https://img.haomeiwen.com/i12775720/3f4b2b7eb9886410.png)
四、添加音乐窗口
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. 阶段成果
![](https://img.haomeiwen.com/i12775720/76de88ba9ea4ea24.png)
五、主窗口
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
对象
- HTML
- 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. 阶段成果
![](https://img.haomeiwen.com/i12775720/5012ca6697927f63.png)
六、音乐器播放状态
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. 此阶段成果
![](https://img.haomeiwen.com/i12775720/62d5d5ee5cbbe1ca.png)
七、打包
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": "squirrel",
"icon": "build/icon.ico"
}
},
"keywords": [
"Electron",
"quick",
"start",
"tutorial",
"demo"
],