本项目构建环境:
node => v12.15.0
electron => 10.1.3
搭建环境:
# 克隆示例项目的仓库
$ git clone https://github.com/electron/electron-quick-start
# 进入这个仓库
$ cd electron-quick-start
# 安装依赖并运行
$ npm install && npm start
本项目所需依赖:
"devDependencies": {
"electron": "^10.1.3",
"electron-builder": "^22.8.1"
},
"dependencies": {
"bootstrap": "^4.5.2",
"electron-store": "^6.0.1",
"nodemon": "^2.0.4",
"uuid": "^8.3.1"
}
本项目无使用任何前端框架,纯原生。(体验一把原生开发带来的快落吧)
修改 main.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const DataStore = require('./renderer/MusicDataStore')
const myStore = new DataStore({ name: 'Music Data' })
// 封装一个 BrowserWindow,避免每一次重复的编写
class AppWindow extends BrowserWindow {
constructor(config, fileLocation) {
const baseConfig = {
width: 960,
height: 660,
webPreferences: {
nodeIntegration: true
}
}
// 两个对象合并
const finalConfig = { ...baseConfig, ...config }
super(finalConfig)
this.loadFile(fileLocation)
this.once('ready-to-show', () => {
this.show()
})
}
}
app.on('ready', () => {
// 加载主页面
const mainWindow = new AppWindow({}, './renderer/index.html')
mainWindow.webContents.on('did-finish-load', () => {
mainWindow.send('getTracks', myStore.getTracks())
})
ipcMain.on('add-music-window', () => {
const addWindow = new AppWindow({
width: 800,
height: 600,
// 指定主进程
parent: mainWindow
}, './renderer/add.html')
})
// 添加数据
ipcMain.on('add-tracks', (event, tracks) => {
const updatedTracks = myStore.addTracks(tracks).getTracks()
mainWindow.send('getTracks', updatedTracks)
})
// 删除数据
ipcMain.on('delete-track', (event, id) => {
const updatedTracks = myStore.deleteTrack(id).getTracks()
mainWindow.send('getTracks', updatedTracks)
})
ipcMain.on('open-music-file', (event) => {
dialog.showOpenDialog({
properties: ['openFile', 'multiSelections'],
filters: [{ name: 'Music', extensions: ['mp3'] }]
}).then((files) => {
if (files) {
event.sender.send('selected-file', files)
}
})
})
})
index.html
(这是我们的主页面)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>本地播放器</title>
<link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="./index.css">
<link rel="stylesheet" href="../font/iconfont.css">
</head>
<body>
<div class="music-content">
<div class="container">
<h1>我的播放器</h1>
<button
type="button"
class="btn btn-primary btn-lg btn-block mt-4"
id="add-music-button"
>
添加歌曲到曲库
</button>
<div id="tracksList" class="mt-4">
</div>
<div class="container fixed-bottom pb-4 ">
<hr/>
<div class="row my-2" id="player-status"></div>
<div class="progress">
<div class="progress-bar" id="player-progress" role="progressbar" style="width: 0%;">
0%
</div>
</div>
</div>
</div>
</div>
<script>
require('./index.js');
</script>
</body>
</html>
index.js
const { ipcRenderer } = require('electron')
const { $, convertDuration, str } = require('./helper')
let musicAudio = new Audio()
let allTracks
let currentTrack
$('add-music-button').addEventListener('click', () => {
ipcRenderer.send('add-music-window')
})
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="mr-2 text-secondary iconfont icon-yinle"></i>
<b>${str(track.fileName)}</b>
</div>
<div class="col-2">
<i class="fas fa-play mr-3 icon-ziyuan iconfont" data-id="${track.id}"></i>
<i class="fas fa-trash-alt icon-del iconfont" data-id="${track.id}"></i>
</div>
</li>`
return html
}, '')
const emptyTrackHTML = '<div class="alert alert-primary">还没有添加任何音乐</div>'
tracksList.innerHTML = tracks.length ? `<ul class="list-group">${tracksListHTML}</ul>` : emptyTrackHTML
}
const renderPlayerHTML = (name, duration) => {
const player = $('player-status')
const html = `<div class="col font-weight-bold"">
正在播放:${str(name)}
</div>
<div class="col">
<span id="current-seeker">00:00</span> / ${convertDuration(duration)}
</div>`
player.innerHTML = html
}
const updateProgressHTML = (currentTime, duration) => {
// 计算 progress 的数值
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)
}
ipcRenderer.on('getTracks', (event, tracks) => {
allTracks = tracks
renderListHTML(tracks)
})
musicAudio.addEventListener('loadedmetadata', () => {
// 开始渲染播放器状态
renderPlayerHTML(currentTrack.fileName, musicAudio.duration)
})
musicAudio.addEventListener('timeupdate', () => {
// 更新播放器状态
updateProgressHTML(musicAudio.currentTime, musicAudio.duration)
})
$('tracksList').addEventListener('click', (event) => {
event.preventDefault()
const { dataset, classList } = event.target
const id = dataset && dataset.id
if (id && classList.contains('icon-ziyuan')) {
// 播放音乐
if (currentTrack && currentTrack.id === id) {
// 继续播放音乐
musicAudio.play()
} else {
// 播放新的歌曲
currentTrack = allTracks.find(track => track.id === id)
musicAudio.src = currentTrack.path
musicAudio.play()
const resetIconEle = document.querySelector('.icon-zanting')
if (resetIconEle) {
// 更改播放图标
resetIconEle.classList.replace('icon-zanting', 'icon-ziyuan')
}
}
classList.replace('icon-ziyuan', 'icon-zanting')
} else if (id && classList.contains('icon-zanting')) {
// 暂停
musicAudio.pause()
classList.replace('icon-zanting', 'icon-ziyuan')
} else if (id && classList.contains('icon-del')) {
// 删除
ipcRenderer.send('delete-track', id)
}
})
MusicDataStore.js
(这是存储音乐数据的地方)
const Store = require('electron-store')
const {path} = require('path')
// 保证唯一的ID
const { v4: uuidv4 } = require('uuid');
class DataStore extends Store {
constructor(settings) {
super(settings);
this.tracks = this.get('tracks') || []
}
// 保存数据
saveTracks() {
this.set('tracks', this.tracks)
return this
}
// 获取数据
getTracks() {
return this.get('tracks') || []
}
// 添加数据
addTracks(tracks) {
const tracksWithProps = tracks.map(track => {
return {
id: uuidv4(),
path: track,
fileName: track
}
}).filter(track => {
const currentTrackPath = this.getTracks().map(track => track.path)
return currentTrackPath.indexOf(track.path) < 0
})
this.tracks = [...this.tracks, ...tracksWithProps]
return this.saveTracks()
}
// 删除数据
deleteTrack(deleteId) {
this.tracks = this.tracks.filter(item => item.id !== deleteId)
return this.saveTracks()
}
}
module.exports = DataStore
大概的主代码就差不多了。剩下的一些辅助代码我就不写出来了。
(这个 demo 其实是存在 bug,就是显示音乐的名字有一点问题)
我在 electron 引入 path 模块是 undefined,这个我不知道是为什么,所以代码的名字我只是做了很简单的处理。就很丑了。
代码地址奉上
https://gitee.com/suiboyu/electron-music
下载之后的代码中 electron-music/outName/appName-win32-x64,中有一个 appName.exe 就是打包出来的 exe(这个打包好像也行有点问题的,不过 demo 嘛,刚刚开始学习,凑合凑合用吧)