项目地址css
点击下载应用。html
macOS用户请下载dmg文件,windows用户请下载exe文件,linux用户请下载AppImage文件。项目当前依赖NeteaseCloudMusicApi,感谢NeteaseCloudMusicApi的做者。vue
当前只为windows作了适配 。linux
拖动对话框的身影在项目中仍是挺常见的,如首页中的栏目调整对话框,收藏歌单等。git
然而Ant Design Vue提供的对话框组件并无提供拖拽的功能,但这一功能在项目中又是不可缺乏的,因此只好本身动手丰衣足食。github
封装一个drop-modal主要分三步:
$attrs,$slots,$listeners
实现前两步的目的在于让书写drop-modal的语法和a-modal保持基本一致,其中第一步较为简单,新建drop-modal,其模板以下:web
<template> <a-modal v-bind="{...$attrs,...$slots}" v-on="$listeners" > <slot></slot> </a-modal> </template>
一般咱们在a-modal上经过v-model绑定一个值,经过修改该值来控制对话框的显示隐藏,就像这样vuex
<a-modal v-model="visible"> <p>contents</p> </a-modal>
因此咱们也应该在drop-modal实现上实现v-model。若是了解自定义组件的v-model是:value和@input的语法糖,实现起来也不难。数据库
value
。currentValue
,在其get方法中返回value,在set方法中触发自定义事件currentValue
绑定在a-modal上便可。核心代码以下:npm
<a-modal ... v-model="currentValue"> ... </a-modal> computed: { currentValue: { get () { return this.value }, set (val) { this.$emit('input', val) } } }
最后一步也是最重要的一步,经过watch
监听 value
,当值为true时实例一个Draggabilly让modal变成可拖动。这一步须要注意4点:
至此封装的drop-modal知足当前项目的全部需求,固然也有不足。
封装drop-modal所涉及的vue核心知识点——$attrs
,$slots
,$listeners
,自定义组件的v-model的还原,计算属性保持数据单向,$nextTick。最终代码 drop-modal**
核心思路在于:动态组件 <component :is="componentName" />,经过操做数组navs的元素位置来控制栏目顺序。
navs中每一个对象的key即componentName,hideMore来控制标题的右侧是否显示更多的连接。
navs: [ { name: '独家放送', key: 'privateContent', hideMore: true }, { name: '最新音乐', key: 'newSong' }, { name: '推荐歌单', key: 'playlist' }, { name: '推荐MV', key: 'mv' }, { name: '主播电台', key: 'dj' } ]
<div v-for="nav in navs"> <component :is="nav.key" /> </div>
接下来就是如何操做数组navs的问题了~ 经过h5的拖拽api改变元素位置并将新位置newNavs持久化保存,在页面初始化时使用newNavs渲染栏目组件便可。
此外还结合了
transition-group
组件,让栏目顺序变化有一个过渡效果,而这一过渡效果也很好的诠释了动画的重要意义--“解释刚刚发生了什么”
核心代码以下:
<div v-for="nav in navs" :key="nav.key" draggable="true" @dragstart="dragstart(nav)" @dragenter="dragenter(nav)" > <span>{{nav.name}}</span> <z-icon type="drag"></z-icon> </div> data () { return { oldNav: 0, newNav: 0, } } methods: { dragstart (nav) { this.oldNav = nav }, dragenter (nav) { this.newNav = nav if (this.oldNav.name !== this.newNav.name) { let oldIndex = this.navs.findIndex(nav => nav.name == this.oldNav.name) let newIndex = this.navs.findIndex(nav => nav.name == this.newNav.name) let newItems = [...this.navs] newItems.splice(oldIndex, 1) newItems.splice(newIndex, 0, this.oldNav) this.navs = [...newItems] window.localStorage.setItem('nav', JSON.stringify(this.navs)) } } }
最终实现的效果以下:
项目中优雅操做dom的地方还不少,原理大同小异,即 数据驱动。好比进度条组件<div class="buffered" ref="buffered" :style = "{width :
${bufferedOffsetWidth
}px}"></div>
经过操做变量bufferedOffsetWidth
来控制缓冲条的width
又好比私人fm的歌曲卡片切换,篇幅有限不作过多介绍,详情请移步 fm源码查看
音频可视化生动点长这样,仍是挺炫酷的!!!
项目结合了二者实现了以下效果:射线和动态粒子,区别在于个人射线较细较短较密集(固然这些都是可控的),以及粒子是向圆内波动
音频的可视化要点在于使用canvas绘制基于
AudioContext
获取到频谱数据。
// 获取API let context = new AudioContext; // 加载audio,能够是dom也能够是一个Audio的实例 let audio = new Audio("1.mp3"); // 建立节点 let source = context.createMediaElementSource(audio); let analyser = context.createAnalyser(); // 链接:source → analyser → destination source.connect(analyser); analyser.connect(context.destination); // 建立数据 let output = new Uint8Array(460); // 获取频域数据 analyser.getByteFrequencyData(output)
打印output
,它长这样:
首先绘制静态的外射线,注意观察每条射线
const { width, height } = document.getElementById('canvas') const du = 3 // 圆心到两条射线距离所成的角度,即射线的间隙 const potInt = { x: width / 2, y: height / 2 } // 起始坐标,即画布中心 const R = 150 // 半径 const W = 4 // 射线的宽度 const L = 32 // 射线的长度
(Math.sin(((i * du) / 180) * Math.PI) * R + potInt.y,-Math.cos(((i * du) / 180) * Math.PI) * R + potInt.x)
(Math.sin(((i * du) / 180) * Math.PI) * (R + L) + potInt.y, -Math.cos(((i * du) / 180) * Math.PI) * (R + L) + potInt.x)
其中i为循环360度的索引。肯定了每条射线的起始点和结束点,也就肯定了渐变的起始点和结束点。经过moveTo,lineTo绘制
紧接着将半径R扩大 let Rv = R + value
,先写死1再绘制一层纯色层叠加在渐变层之上。以后在requestAnimationFrame
的执行函数中根据频谱数据动态改变value便可实现动画效果,但要注意渐变层的射线应该老是大于纯色层射线L的长度。
canvas动画固然是少不了 cxt.clearRect(0, 0, width, height)
和 requestAnimationFrame
啦!动画及粒子向内的波动实现请参考musicView源码
除此以外还实现了另外一种相似熔浆喷发的效果,也很nice。
项目一大重点难点是如何将store中歌词,播放状态等数据实时的在各窗体中共享。一开始想经过主进程来作中转,但主进程微笑而不失礼貌地婉拒了:“渲染进程能处理的事就不要拿来骚扰我啦,我很忙的!”。最后把目光投向了localstorage
。
// 监听storage改变时触发更新state window.addEventListener('storage', () => { initState() }) // 订阅mutation改变storage store.subscribe((mutation, state) => { localStorage.setItem(STOREKEY, JSON.stringify(getState(state))) })
其原理在于订阅mutation改变storage,监听storage触发更新state,经过书写一个vuex插件来实现这一功能,详情请查看 keep-state.js
usage:
在store入口文件引入keep-state,keep-state插件是一个函数,传入须要监听模块mudules执行函数,在初始化stroe时将函数的执行结果赋予plugins。
import persistStatePlugin from './plugins/keep-state' const myPlugin = persistStatePlugin(['User', 'play', 'Localsong', 'Setting', 'Update']) const store = new Vuex.Store({ ... plugins: [myPlugin] })
实现桌面歌词须要注意如下几点:
经过设置transparent:true,alwaysOnTop: true可分别实现窗体透明和窗体置顶,其中透明窗体要注意html,body,#app等不能设置非透明的背景色。
经过 setignoremouseeventsignore api可切换锁定窗体。
至于窗体初始时的位置,默认是屏幕中央。我想让他水平居中,垂直在任务栏偏上一点,这就须要获取屏幕的高来作点文章了 const { height } = electron.screen.getPrimaryDisplay().workAreaSize
。
最终窗体初始化的核心代码以下:
const options = { frame: false, x: 0, y: height - 150, fullscreenable: false, minimizable: false, maximizable: false, transparent: true, alwaysOnTop: true, skipTaskbar: true, // 任务栏中不显示窗口面板 closable: false } const winURL = process.env.NODE_ENV === 'development' ? `http://localhost:9080/#desktop-lyric` : `file://${__dirname}/index.html#desktop-lyric` let lyricWindow = new BrowserWindow(options) lyricWindow.loadURL(winURL)
mini模式主要分为两部分:
其中主面板又分三个面板:
实现要点在于隐藏主窗体,显示mini窗体(320*50)。经过win.setBounds()在切换下拉列表时动态改变窗体大小
经过electron Tray模块的实例的setContextMenu
方法建立的菜单是真的丑不忍睹..
如何自定义一个托盘菜单呢?就像这样:
答案之一就是经过一个窗体来模拟。经过监听托盘的右键点击事件切换菜单的显示隐藏便可,其中须要实时计算出每次菜单出现的位置及边界状况。
任务栏工具栏?长这样,包含标题缩略图,及歌曲的相关操做。
幸运的,electron提供相关API实现这一功能 缩略图工具栏
拖拽播放分三种:
在实现以前请先看看默认将文件拖动到客户端会发生什么?
是的,默认和将文件拖动到Chrome浏览器是同样的,就像这样...
就猜到会是这样了...!
因此咱们第一步就是要禁用掉这些默认行为:
window.ondragenter = (event) => { event.preventDefault() } window.ondragover = (event) => { event.preventDefault() }
经过监听window的drop事件来实现咱们的打开文件操做。这只是实现了拖拽播放中的第一种状况。
window.ondrop = openFilesOndrop
其余两种状况在windows平台上须要在process.argv上动动手脚。
先说说第二种状况,在主进程的appready
的事件回调中将process.argv赋予全局变量global global.argv = process.argv
,在渲染进程中经过electron的remote模块的getGlobal方法获取到argv
。process.argv初始化长这样:["E:\electron-vue-cloud-music\网易云音乐.exe"]
即客户端的可执行文件的路径。因此在执行handleWillOpenFiles
方法前判断一下数组长度。在handleWillOpenFiles
方法过滤出.mp3文件进行相关解析播放等操做。详情移步 createdInit
import { remote } from 'electron' const startArgv = remote.getGlobal('argv') if (startArgv.length > 1) { handleWillOpenFiles(startArgv) }
至于第三种状况和第二种大同小异,区别在于argv的参数的获取以及渲染进程如何拿到argv。对于argv的获取,在主进程的app的second-instance
监听回调中获取,经过自定义事件分发,渲染进程监听该自定义事件来接受。
// 主进程 app.on('second-instance', (event, argv, workingDirectory) => { if (mainWindow) { mainWindow.webContents.send('open-files', {argv}) } }) // 渲染进程 import { ipcRenderer} from 'electron' ipcRenderer.on('open-files', async (event, args) => { let { argv } = args handleWillOpenFiles(argv) })
当前自动更新已移除,简单说说如何实现手动检查更新,具体流程是这样的:
下载完成后关闭窗体并打开下载文件进行安装
Nedb数据库 主要用来存储下载的歌曲列表及歌词。盗用官网介绍就是:
Embedded persistent or in memory database for Node.js, nw.js, Electron and browsers, 100% JavaScript, no binary dependency. API is a subset of MongoDB's and it's plenty fast.
本人4级水平简短白话翻译是 为Electron而生,无依赖,快,使用和mongoDb差很少
自定义安装路径较为简单在package.json中找到build字段加入如下代码便可
"nsis": { "oneClick": false, // 是否一键安装 "allowToChangeInstallationDirectory": true // 是否容许修改安装路径 }
自定义安装界面可经过一些开源工具来快捷实现如 NSIS-UI 简单实现了一下,效果还能够:
经过app.setAsDefaultProtocolClient
可实现自定义协议在浏览器中唤起客户端,若是安装过了可尝试 打开electron云音乐
经过window的online
和offline
可监听网络状态。
经过navigator.onLine
可判断当前网络状态.
经过h5的Notification
可实现桌面通知,在window平台中使用请确保设置appId
分享一篇阮一峰的一篇文章便可 持续集成
当前项目只对window平台进行测试。
至此electron云音乐实战分享基本结束,项目中有趣的地方还有不少,但篇幅有限,不能面面俱到。原本还想说说那些使人敬礼的css但再不去打lol的衰减局就要掉峡谷宗师了!不排除有下集..第一次写文章,感谢各位看客老爷看到这里,谢谢。
最后唠叨一句:“以为不错给我一个赞~”