Electron+Vue+Ant Design Vue仿网易云音乐windows客户端实战分享

转自:https://blog.csdn.net/weixin_45013937/article/details/100715845
github: https://github.com/xiaozhu188/electron-vue-cloud-music

在这里插入图片描述

特点

  • 拖拽播放
  • 桌面歌词
  • mini模式
  • 自定义托盘右键菜单
  • 任务栏缩略图,歌曲操作
  • 音频可视化
  • 自动/手动检查更新
  • Nedb数据库持久化
  • 自定义安装路径,安装界面美化
  • 浏览器中启动客户端
  • Travis CL,AppVeyor自动构建
  • 换肤,下载,本地歌曲匹配,网络变化桌面通知,分享歌曲/歌单/MV/视频等到QQ空间
  • 登录,私人Fm,歌单,专辑,歌手,排行榜,MV,视频,评论,搜索,用户,动态,粉丝,关注,云盘,收藏…
  • 心动模式,歌词微调,下一首播放,追加播放,单曲循环,随机播放,列表循环
  • 路由导向,局部刷新,首页栏目调整并持久化…

下载 && 运行

项目地址

点击下载应用

macOS用户请下载dmg文件,windows用户请下载exe文件,linux用户请下载AppImage文件。
项目当前依赖NeteaseCloudMusicApi,需本地启动该服务并为接口地址添加/api后缀

基于draggabilly封装一个可拖动的对话框

拖动对话框的身影在项目中还是挺常见的,如首页中的栏目调整对话框,收藏歌单等。

[图片上传失败...(image-c0284d-1602812833312)]

[图片上传失败...(image-c02b39-1602812833312)]

然而Ant Design Vue提供的对话框组件并没有提供拖拽的功能,但这一功能在项目中又是不可缺少的,所以只好自己动手丰衣足食。

封装一个drop-modal主要分三步:

  • 让drop-modal拥有拥有a-modal的API
  • 在drop-modal上实现v-model
  • modal首次显示后实例化Draggabilly

$attrs,$slots,$listeners

实现前两步的目的在于让书写drop-modal的语法和a-modal保持基本一致,其中第一步较为简单,新建drop-modal,其模板如下:

<template>
  <a-modal
    v-bind="{...$attrs,...$slots}"
    v-on="$listeners"
  >
    <slot></slot>
  </a-modal>
</template>

实现v-model

通常我们在a-modal上通过v-model绑定一个值,通过修改该值来控制对话框的显示隐藏,就像这样

<a-modal v-model="visible">
  <p>contents</p>
</a-modal>

所以我们也应该在drop-modal实现上实现v-model。如果了解自定义组件的v-model是:value和@input的语法糖,实现起来也不难。

  • 首先定义一个props value。为了保持单向数据流.
  • 再定义一个计算属性 currentValue,在其get方法中返回value,在set方法中触发自定义事件
  • 最后将currentValue绑定在a-modal上即可。核心代码如下:
<a-modal ... v-model="currentValue">
   ...
</a-modal>

computed: {
    currentValue: {
      get () {
        return this.value
      },
      set (val) {
        this.$emit('input', val)
      }
    }
}

实例化Draggabilly

最后一步也是最重要的一步,通过watch监听 value ,当值为true时实例一个Draggabilly让modal变成可拖动。这一步需要注意4点:

  1. 确保在nextTick中实例化Draggabilly
  2. 仅在首次显示时实例化Draggabilly
  3. 确定可拖动的dom
  4. modal的嵌套情况

至此封装的drop-modal满足当前项目的所有需求,当然也有不足。

总结

封装drop-modal所涉及的vue核心知识点——$attrs$slots$listeners,自定义组件的v-model的还原,计算属性保持数据单向,$nextTick。最终代码 drop-modal**


Vue中优雅“操作”dom之调整栏目顺序

动态组件

核心思路在于:动态组件 ,通过操作数组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>

h5的拖拽api

接下来就是如何操作数组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))
      }
    }
}

最终实现的效果如下:

[图片上传失败...(image-d7c6c8-1602812833311)]

其他

项目中优雅操作dom的地方还很多,原理大同小异,即数据驱动。比如进度条组件 <div class="buffered" ref="buffered" :style = "{width :${bufferedOffsetWidth}px}"></div> 通过操作变量bufferedOffsetWidth来控制缓冲条的width

又比如私人fm的歌曲卡片切换,篇幅有限不做过多介绍,详情请移步fm源码查看

[图片上传失败...(image-b6ce0d-1602812833311)]

音频可视化

AudioContext

音频可视化生动点长这样,还是挺炫酷的!!!

[图片上传失败...(image-6cd913-1602812833311)]

[图片上传失败...(image-30cb41-1602812833311)]

项目结合了两者实现了如下效果:射线和动态粒子,区别在于我的射线较细较短较密集(当然这些都是可控的),以及粒子是向圆内波动

[图片上传失败...(image-d8f4d3-1602812833311)]

音频的可视化要点在于使用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,它长这样:

[图片上传失败...(image-12adea-1602812833311)]

使用canvas绘制

首先绘制静态的外射线,注意观察每条射线

  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 // 射线的长度

  • 圆角:cxt.lineCap = ‘round’

  • 渐变:cxt.createLinearGradient(x1,y1,x2,y2)

  • 起始点:(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绘制

[图片上传失败...(image-423aec-1602812833311)]

紧接着将半径R扩大 let Rv = R + value ,先写死1再绘制一层纯色层叠加在渐变层之上。之后动态改变value即可实现动画效果,但要注意渐变层的射线应该总是大于纯色层射线L的长度。

[图片上传失败...(image-21d4b7-1602812833311)]

canvas动画当然是少不了 cxt.clearRect(0, 0, width, height)requestAnimationFrame 啦!动画及粒子向内的波动实现请参考musicView源码

渲染进程的即时通讯

项目一大重点难点是如何将store中歌词,播放状态等数据实时的在各窗体中共享。一开始想通过主进程来做中转,但主进程微笑而不失礼貌地婉拒了:“渲染进程能处理的事就不要拿来骚扰我啦,我很忙的!”。最后把目光投向了localstorage。原理在于订阅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]
})

electron实战之桌面歌词

[图片上传失败...(image-174311-1602812833311)]

实现桌面歌词需要注意以下几点:

  1. 透明窗体

  2. 窗口在别的窗口上面

  3. 可锁定(锁定后忽略窗口内的所有鼠标事件)

  4. 出现在屏幕的位置

通过设置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)

electron实战之mini模式

[图片上传失败...(image-cf3739-1602812833311)]

[图片上传失败...(image-633bfb-1602812833311)]

mini模式主要分为两部分:

  1. 主面板
  2. 当前播放列表面板

其中主面板又分三个面板:

  1. 歌曲缩略图,按住可拖动
  2. 歌曲信息及工具栏
  3. 相关操作面板

实现要点在于隐藏主窗体,显示mini窗体(320*50)。通过win.setBounds()在切换下拉列表时动态改变窗体大小

electron实战之自定义托盘菜单

通过electron Tray模块的实例的setContextMenu方法创建的菜单是真的丑不忍睹…
[图片上传失败...(image-139b63-1602812833311)]

如何自定义一个托盘菜单呢?就像这样:

[图片上传失败...(image-aa3d00-1602812833311)]

答案之一就是通过一个窗体来模拟。通过监听托盘的右键点击事件切换菜单的显示隐藏即可,其中需要实时计算出每次菜单出现的位置及边界情况。

electron实战之自定义任务栏的缩略图工具栏

任务栏工具栏?长这样,包含标题缩略图,及歌曲的相关操作。

[图片上传失败...(image-1d1b7-1602812833311)]

幸运的,electron提供相关API实现这一功能 缩略图工具栏

electron实战之拖拽播放

介绍

拖拽播放分三种:

  1. 将文件拖到主窗体内实现播放
  2. 将文件拖动到桌面上的快捷方式图标打开客户端并播放
  3. 客户端已经打开,将文件拖动到桌面上的快捷方式图标实现播放(不会打开第二个实例)

禁用默认行为

在实现之前请先看看默认将文件拖动到客户端会发生什么?
是的,默认和将文件拖动到Chrome浏览器是一样的,就像这样…

[图片上传失败...(image-fadd22-1602812833311)]

就猜到会是这样了…!
[图片上传失败...(image-4f35c3-1602812833311)]

所以我们第一步就是要禁用掉这些默认行为:

  window.ondragenter = (event) => {
      event.preventDefault()
  }
  window.ondragover = (event) => {
      event.preventDefault()
  }
  window.ondrop = openFilesOndrop

将文件拖到主窗体内实现播放

监听window的drop事件来实现我们的打开文件操作。这只是实现了拖拽播放中的第一种情况。

其他两种情况在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)
})

electron实战之自动/手动检查更新

当前自动更新已移除,简单说说如何实现手动检查更新,具体流程是这样的:

  1. 开发,commit
  2. npm version patch && git push origin master && git push origin --tags
  3. Travis CL,AppVeyor监测到master变化自动构建
  4. github上编辑发布远程版本
  5. 用户/客户端触发检查更新
  6. 客户端调用github API获取最新的远程版本号与本地版本号对比
  7. 如若需要更新显示更新窗体引导下载安装

[图片上传失败...(image-e178f5-1602812833311)]

[图片上传失败...(image-3ddfe2-1602812833311)]

下载完成后关闭窗体并打开下载文件进行安装

electron实战之Nedb数据库持久化

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差不多

electron实战之打包自定义安装路径,安装界面美化

自定义安装路径较为简单在package.json中找到build字段加入以下代码即可

"nsis": {
  "oneClick": false, // 是否一键安装
  "allowToChangeInstallationDirectory": true // 是否允许修改安装路径
}

自定义可通过一些开源工具来快捷实现 NSIS-UI 简单实现了一下,效果还可以:

[图片上传失败...(image-791022-1602812833310)]

electron实战之自定义协议实现浏览器中启动客户端

通过app.setAsDefaultProtocolClient可实现自定义协议在浏览器中唤起客户端,如果安装过了可尝试 打开electron云音乐

electron实战之离线/在线侦测与桌面通知

通过window的onlineoffline可监听网络状态。

通过navigator.onLine可判断当前网络状态.

通过h5的Notification可实现桌面通知,在window平台中使用请确保设置appId

[图片上传失败...(image-1e2fa5-1602812833310)]

Travis CL,AppVeyor自动构建

分享一篇阮一峰的一篇文章即可 持续集成


结语

当前项目只对window平台进行测试。

至此electron云音乐实战分享基本结束,项目中有趣的地方还有很多,但篇幅有限,不能面面俱到。本来还想说说那些令人敬礼的css但再不去打lol的衰减局就要掉峡谷宗师了!不排除有下集…第一次写文章,感谢各位看客老爷看到这里,谢谢。

最后唠叨一句:“觉得不错给我一个赞~”

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345