轉(zhuǎn)自:https://blog.csdn.net/weixin_45013937/article/details/100715845
github: https://github.com/xiaozhu188/electron-vue-cloud-music
特點(diǎn)
- 拖拽播放
- 桌面歌詞
- mini模式
- 自定義托盤右鍵菜單
- 任務(wù)欄縮略圖,歌曲操作
- 音頻可視化
- 自動(dòng)/手動(dòng)檢查更新
- Nedb數(shù)據(jù)庫(kù)持久化
- 自定義安裝路徑,安裝界面美化
- 瀏覽器中啟動(dòng)客戶端
- Travis CL,AppVeyor自動(dòng)構(gòu)建
- 換膚,下載,本地歌曲匹配,網(wǎng)絡(luò)變化桌面通知,分享歌曲/歌單/MV/視頻等到QQ空間
- 登錄,私人Fm,歌單,專輯,歌手,排行榜,MV,視頻,評(píng)論,搜索,用戶,動(dòng)態(tài),粉絲,關(guān)注,云盤,收藏…
- 心動(dòng)模式,歌詞微調(diào),下一首播放,追加播放,單曲循環(huán),隨機(jī)播放,列表循環(huán)
- 路由導(dǎo)向,局部刷新,首頁(yè)欄目調(diào)整并持久化…
- …
下載 && 運(yùn)行
點(diǎn)擊下載應(yīng)用。
macOS用戶請(qǐng)下載dmg文件,windows用戶請(qǐng)下載exe文件,linux用戶請(qǐng)下載AppImage文件。
項(xiàng)目當(dāng)前依賴NeteaseCloudMusicApi,需本地啟動(dòng)該服務(wù)并為接口地址添加/api后綴
基于draggabilly封裝一個(gè)可拖動(dòng)的對(duì)話框
拖動(dòng)對(duì)話框的身影在項(xiàng)目中還是挺常見的,如首頁(yè)中的欄目調(diào)整對(duì)話框,收藏歌單等。
[圖片上傳失敗...(image-c0284d-1602812833312)]
[圖片上傳失敗...(image-c02b39-1602812833312)]
然而Ant Design Vue提供的對(duì)話框組件并沒有提供拖拽的功能,但這一功能在項(xiàng)目中又是不可缺少的,所以只好自己動(dòng)手豐衣足食。
封裝一個(gè)drop-modal主要分三步:
- 讓drop-modal擁有擁有a-modal的API
- 在drop-modal上實(shí)現(xiàn)v-model
- modal首次顯示后實(shí)例化Draggabilly
$attrs,$slots,$listeners
實(shí)現(xiàn)前兩步的目的在于讓書寫drop-modal的語(yǔ)法和a-modal保持基本一致,其中第一步較為簡(jiǎn)單,新建drop-modal,其模板如下:
<template>
<a-modal
v-bind="{...$attrs,...$slots}"
v-on="$listeners"
>
<slot></slot>
</a-modal>
</template>
實(shí)現(xiàn)v-model
通常我們?cè)赼-modal上通過v-model綁定一個(gè)值,通過修改該值來(lái)控制對(duì)話框的顯示隱藏,就像這樣
<a-modal v-model="visible">
<p>contents</p>
</a-modal>
所以我們也應(yīng)該在drop-modal實(shí)現(xiàn)上實(shí)現(xiàn)v-model。如果了解自定義組件的v-model是:value和@input的語(yǔ)法糖,實(shí)現(xiàn)起來(lái)也不難。
- 首先定義一個(gè)props
value。為了保持單向數(shù)據(jù)流. - 再定義一個(gè)計(jì)算屬性
currentValue,在其get方法中返回value,在set方法中觸發(fā)自定義事件 - 最后將
currentValue綁定在a-modal上即可。核心代碼如下:
<a-modal ... v-model="currentValue">
...
</a-modal>
computed: {
currentValue: {
get () {
return this.value
},
set (val) {
this.$emit('input', val)
}
}
}
實(shí)例化Draggabilly
最后一步也是最重要的一步,通過watch監(jiān)聽 value ,當(dāng)值為true時(shí)實(shí)例一個(gè)Draggabilly讓modal變成可拖動(dòng)。這一步需要注意4點(diǎn):
- 確保在nextTick中實(shí)例化Draggabilly
- 僅在首次顯示時(shí)實(shí)例化Draggabilly
- 確定可拖動(dòng)的dom
- modal的嵌套情況
至此封裝的drop-modal滿足當(dāng)前項(xiàng)目的所有需求,當(dāng)然也有不足。
總結(jié)
封裝drop-modal所涉及的vue核心知識(shí)點(diǎn)——$attrs,$slots,$listeners,自定義組件的v-model的還原,計(jì)算屬性保持?jǐn)?shù)據(jù)單向,$nextTick。最終代碼 drop-modal**
Vue中優(yōu)雅“操作”dom之調(diào)整欄目順序
動(dòng)態(tài)組件
核心思路在于:動(dòng)態(tài)組件 ,通過操作數(shù)組navs的元素位置來(lái)控制欄目順序。
navs中每個(gè)對(duì)象的key即componentName,hideMore來(lái)控制標(biāo)題的右側(cè)是否顯示更多的鏈接。
navs: [
{
name: '獨(dú)家放送',
key: 'privateContent',
hideMore: true
},
{
name: '最新音樂',
key: 'newSong'
},
{
name: '推薦歌單',
key: 'playlist'
},
{
name: '推薦MV',
key: 'mv'
},
{
name: '主播電臺(tái)',
key: 'dj'
}
]
<div v-for="nav in navs">
<component :is="nav.key" />
</div>
h5的拖拽api
接下來(lái)就是如何操作數(shù)組navs的問題了~ 通過h5的拖拽api改變?cè)匚恢貌⑿挛恢胣ewNavs持久化保存,在頁(yè)面初始化時(shí)使用newNavs渲染欄目組件即可。
此外還結(jié)合了
transition-group組件,讓欄目順序變化有一個(gè)過渡效果,而這一過渡效果也很好的詮釋了動(dòng)畫的重要意義–“解釋剛剛發(fā)生了什么”
核心代碼如下:
<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))
}
}
}
最終實(shí)現(xiàn)的效果如下:
[圖片上傳失敗...(image-d7c6c8-1602812833311)]
其他
項(xiàng)目中優(yōu)雅操作dom的地方還很多,原理大同小異,即數(shù)據(jù)驅(qū)動(dòng)。比如進(jìn)度條組件
<div class="buffered" ref="buffered" :style = "{width :${bufferedOffsetWidth}px}"></div>通過操作變量bufferedOffsetWidth來(lái)控制緩沖條的width
又比如私人fm的歌曲卡片切換,篇幅有限不做過多介紹,詳情請(qǐng)移步fm源碼查看
[圖片上傳失敗...(image-b6ce0d-1602812833311)]
音頻可視化
AudioContext
音頻可視化生動(dòng)點(diǎn)長(zhǎng)這樣,還是挺炫酷的?。。?/p>
[圖片上傳失敗...(image-6cd913-1602812833311)]
[圖片上傳失敗...(image-30cb41-1602812833311)]
項(xiàng)目結(jié)合了兩者實(shí)現(xiàn)了如下效果:射線和動(dòng)態(tài)粒子,區(qū)別在于我的射線較細(xì)較短較密集(當(dāng)然這些都是可控的),以及粒子是向圓內(nèi)波動(dòng)
[圖片上傳失敗...(image-d8f4d3-1602812833311)]
音頻的可視化要點(diǎn)在于使用canvas繪制基于
AudioContext獲取到頻譜數(shù)據(jù)。
首先獲取頻譜數(shù)據(jù)
// 獲取API
let context = new AudioContext;
// 加載audio,可以是dom也可以是一個(gè)Audio的實(shí)例
let audio = new Audio("1.mp3");
// 創(chuàng)建節(jié)點(diǎn)
let source = context.createMediaElementSource(audio);
let analyser = context.createAnalyser();
// 連接:source → analyser → destination
source.connect(analyser);
analyser.connect(context.destination);
// 創(chuàng)建數(shù)據(jù)
let output = new Uint8Array(460);
// 獲取頻域數(shù)據(jù)
analyser.getByteFrequencyData(output)
打印output,它長(zhǎng)這樣:
[圖片上傳失敗...(image-12adea-1602812833311)]
使用canvas繪制
首先繪制靜態(tài)的外射線,注意觀察每條射線
const { width, height } = document.getElementById('canvas')
const du = 3 // 圓心到兩條射線距離所成的角度,即射線的間隙
const potInt = { x: width / 2, y: height / 2 } // 起始坐標(biāo),即畫布中心
const R = 150 // 半徑
const W = 4 // 射線的寬度
const L = 32 // 射線的長(zhǎng)度
圓角:cxt.lineCap = ‘round’
漸變:cxt.createLinearGradient(x1,y1,x2,y2)
起始點(diǎn):
(Math.sin(((i * du) / 180) * Math.PI) * R + potInt.y,-Math.cos(((i * du) / 180) * Math.PI) * R + potInt.x)結(jié)束點(diǎn):
(Math.sin(((i * du) / 180) * Math.PI) * (R + L) + potInt.y, -Math.cos(((i * du) / 180) * Math.PI) * (R + L) + potInt.x)
其中i為循環(huán)360度的索引。確定了每條射線的起始點(diǎn)和結(jié)束點(diǎn),也就確定了漸變的起始點(diǎn)和結(jié)束點(diǎn)。通過moveTo,lineTo繪制
[圖片上傳失敗...(image-423aec-1602812833311)]
緊接著將半徑R擴(kuò)大 let Rv = R + value ,先寫死1再繪制一層純色層疊加在漸變層之上。之后動(dòng)態(tài)改變value即可實(shí)現(xiàn)動(dòng)畫效果,但要注意漸變層的射線應(yīng)該總是大于純色層射線L的長(zhǎng)度。
[圖片上傳失敗...(image-21d4b7-1602812833311)]
canvas動(dòng)畫當(dāng)然是少不了 cxt.clearRect(0, 0, width, height) 和 requestAnimationFrame 啦!動(dòng)畫及粒子向內(nèi)的波動(dòng)實(shí)現(xiàn)請(qǐng)參考musicView源碼
渲染進(jìn)程的即時(shí)通訊
項(xiàng)目一大重點(diǎn)難點(diǎn)是如何將store中歌詞,播放狀態(tài)等數(shù)據(jù)實(shí)時(shí)的在各窗體中共享。一開始想通過主進(jìn)程來(lái)做中轉(zhuǎn),但主進(jìn)程微笑而不失禮貌地婉拒了:“渲染進(jìn)程能處理的事就不要拿來(lái)騷擾我啦,我很忙的!”。最后把目光投向了localstorage。原理在于訂閱mutation改變storage,監(jiān)聽storage觸發(fā)更新state,通過書寫一個(gè)vuex插件來(lái)實(shí)現(xiàn)這一功能,詳情請(qǐng)查看 keep-state.js
usage:
在store入口文件引入keep-state,keep-state插件是一個(gè)函數(shù),傳入需要監(jiān)聽模塊mudules執(zhí)行函數(shù),在初始化stroe時(shí)將函數(shù)的執(zhí)行結(jié)果賦予plugins。
import persistStatePlugin from './plugins/keep-state'
const myPlugin = persistStatePlugin(['User', 'play', 'Localsong', 'Setting', 'Update'])
const store = new Vuex.Store({
...
plugins: [myPlugin]
})
electron實(shí)戰(zhàn)之桌面歌詞
[圖片上傳失敗...(image-174311-1602812833311)]
實(shí)現(xiàn)桌面歌詞需要注意以下幾點(diǎn):
透明窗體
窗口在別的窗口上面
可鎖定(鎖定后忽略窗口內(nèi)的所有鼠標(biāo)事件)
出現(xiàn)在屏幕的位置
通過設(shè)置transparent:true,alwaysOnTop: true可分別實(shí)現(xiàn)窗體透明和窗體置頂,其中透明窗體要注意html,body,#app等不能設(shè)置非透明的背景色。
通過 setignoremouseeventsignore api可切換鎖定窗體。
至于窗體初始時(shí)的位置,默認(rèn)是屏幕中央。我想讓他水平居中,垂直在任務(wù)欄偏上一點(diǎn),這就需要獲取屏幕的高來(lái)做點(diǎn)文章了 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, // 任務(wù)欄中不顯示窗口面板
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實(shí)戰(zhàn)之mini模式
[圖片上傳失敗...(image-cf3739-1602812833311)]
[圖片上傳失敗...(image-633bfb-1602812833311)]
mini模式主要分為兩部分:
- 主面板
- 當(dāng)前播放列表面板
其中主面板又分三個(gè)面板:
- 歌曲縮略圖,按住可拖動(dòng)
- 歌曲信息及工具欄
- 相關(guān)操作面板
實(shí)現(xiàn)要點(diǎn)在于隱藏主窗體,顯示mini窗體(320*50)。通過win.setBounds()在切換下拉列表時(shí)動(dòng)態(tài)改變窗體大小
electron實(shí)戰(zhàn)之自定義托盤菜單
通過electron Tray模塊的實(shí)例的setContextMenu方法創(chuàng)建的菜單是真的丑不忍睹…
[圖片上傳失敗...(image-139b63-1602812833311)]
如何自定義一個(gè)托盤菜單呢?就像這樣:
[圖片上傳失敗...(image-aa3d00-1602812833311)]
答案之一就是通過一個(gè)窗體來(lái)模擬。通過監(jiān)聽托盤的右鍵點(diǎn)擊事件切換菜單的顯示隱藏即可,其中需要實(shí)時(shí)計(jì)算出每次菜單出現(xiàn)的位置及邊界情況。
electron實(shí)戰(zhàn)之自定義任務(wù)欄的縮略圖工具欄
任務(wù)欄工具欄?長(zhǎng)這樣,包含標(biāo)題縮略圖,及歌曲的相關(guān)操作。
[圖片上傳失敗...(image-1d1b7-1602812833311)]
幸運(yùn)的,electron提供相關(guān)API實(shí)現(xiàn)這一功能 縮略圖工具欄
electron實(shí)戰(zhàn)之拖拽播放
介紹
拖拽播放分三種:
- 將文件拖到主窗體內(nèi)實(shí)現(xiàn)播放
- 將文件拖動(dòng)到桌面上的快捷方式圖標(biāo)打開客戶端并播放
- 客戶端已經(jīng)打開,將文件拖動(dòng)到桌面上的快捷方式圖標(biāo)實(shí)現(xiàn)播放(不會(huì)打開第二個(gè)實(shí)例)
禁用默認(rèn)行為
在實(shí)現(xiàn)之前請(qǐng)先看看默認(rèn)將文件拖動(dòng)到客戶端會(huì)發(fā)生什么?
是的,默認(rèn)和將文件拖動(dòng)到Chrome瀏覽器是一樣的,就像這樣…
[圖片上傳失敗...(image-fadd22-1602812833311)]
就猜到會(huì)是這樣了…!
[圖片上傳失敗...(image-4f35c3-1602812833311)]
所以我們第一步就是要禁用掉這些默認(rèn)行為:
window.ondragenter = (event) => {
event.preventDefault()
}
window.ondragover = (event) => {
event.preventDefault()
}
window.ondrop = openFilesOndrop
將文件拖到主窗體內(nèi)實(shí)現(xiàn)播放
監(jiān)聽window的drop事件來(lái)實(shí)現(xiàn)我們的打開文件操作。這只是實(shí)現(xiàn)了拖拽播放中的第一種情況。
其他兩種情況在windows平臺(tái)上需要在process.argv上動(dòng)動(dòng)手腳。
將文件拖動(dòng)到桌面上的快捷方式圖標(biāo)打開客戶端并播放
先說(shuō)說(shuō)第二種情況,在主進(jìn)程的appready的事件回調(diào)中將process.argv賦予全局變量global global.argv = process.argv,在渲染進(jìn)程中通過electron的remote模塊的getGlobal方法獲取到argv。process.argv初始化長(zhǎng)這樣:["E:\electron-vue-cloud-music\網(wǎng)易云音樂.exe"] 即客戶端的可執(zhí)行文件的路徑。所以在執(zhí)行handleWillOpenFiles方法前判斷一下數(shù)組長(zhǎng)度。在handleWillOpenFiles方法過濾出.mp3文件進(jìn)行相關(guān)解析播放等操作。詳情移步 createdInit
import { remote } from 'electron'
const startArgv = remote.getGlobal('argv')
if (startArgv.length > 1) {
handleWillOpenFiles(startArgv)
}
客戶端已經(jīng)打開,將文件拖動(dòng)到桌面上的快捷方式圖標(biāo)實(shí)現(xiàn)播放
至于第三種情況和第二種大同小異,區(qū)別在于argv的參數(shù)的獲取以及渲染進(jìn)程如何拿到argv。對(duì)于argv的獲取,在主進(jìn)程的app的second-instance監(jiān)聽回調(diào)中獲取,通過自定義事件分發(fā),渲染進(jìn)程監(jiān)聽該自定義事件來(lái)接受。
// 主進(jìn)程
app.on('second-instance', (event, argv, workingDirectory) => {
if (mainWindow) {
mainWindow.webContents.send('open-files', {argv})
}
})
// 渲染進(jìn)程
import { ipcRenderer} from 'electron'
ipcRenderer.on('open-files', async (event, args) => {
let { argv } = args
handleWillOpenFiles(argv)
})
electron實(shí)戰(zhàn)之自動(dòng)/手動(dòng)檢查更新
當(dāng)前自動(dòng)更新已移除,簡(jiǎn)單說(shuō)說(shuō)如何實(shí)現(xiàn)手動(dòng)檢查更新,具體流程是這樣的:
- 開發(fā),commit
- npm version patch && git push origin master && git push origin --tags
- Travis CL,AppVeyor監(jiān)測(cè)到master變化自動(dòng)構(gòu)建
- github上編輯發(fā)布遠(yuǎn)程版本
- 用戶/客戶端觸發(fā)檢查更新
- 客戶端調(diào)用github API獲取最新的遠(yuǎn)程版本號(hào)與本地版本號(hào)對(duì)比
- 如若需要更新顯示更新窗體引導(dǎo)下載安裝
[圖片上傳失敗...(image-e178f5-1602812833311)]
[圖片上傳失敗...(image-3ddfe2-1602812833311)]
下載完成后關(guān)閉窗體并打開下載文件進(jìn)行安裝
electron實(shí)戰(zhàn)之Nedb數(shù)據(jù)庫(kù)持久化
Nedb數(shù)據(jù)庫(kù) 主要用來(lái)存儲(chǔ)下載的歌曲列表及歌詞。盜用官網(wǎng)介紹就是:
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級(jí)水平簡(jiǎn)短白話翻譯是為Electron而生,無(wú)依賴,快,使用和mongoDb差不多
electron實(shí)戰(zhàn)之打包自定義安裝路徑,安裝界面美化
自定義安裝路徑較為簡(jiǎn)單在package.json中找到build字段加入以下代碼即可
"nsis": {
"oneClick": false, // 是否一鍵安裝
"allowToChangeInstallationDirectory": true // 是否允許修改安裝路徑
}
自定義可通過一些開源工具來(lái)快捷實(shí)現(xiàn) NSIS-UI 簡(jiǎn)單實(shí)現(xiàn)了一下,效果還可以:
[圖片上傳失敗...(image-791022-1602812833310)]
electron實(shí)戰(zhàn)之自定義協(xié)議實(shí)現(xiàn)瀏覽器中啟動(dòng)客戶端
通過app.setAsDefaultProtocolClient可實(shí)現(xiàn)自定義協(xié)議在瀏覽器中喚起客戶端,如果安裝過了可嘗試 打開electron云音樂
electron實(shí)戰(zhàn)之離線/在線偵測(cè)與桌面通知
通過window的online和offline可監(jiān)聽網(wǎng)絡(luò)狀態(tài)。
通過navigator.onLine可判斷當(dāng)前網(wǎng)絡(luò)狀態(tài).
通過h5的Notification可實(shí)現(xiàn)桌面通知,在window平臺(tái)中使用請(qǐng)確保設(shè)置appId
[圖片上傳失敗...(image-1e2fa5-1602812833310)]
Travis CL,AppVeyor自動(dòng)構(gòu)建
分享一篇阮一峰的一篇文章即可 持續(xù)集成
結(jié)語(yǔ)
當(dāng)前項(xiàng)目只對(duì)window平臺(tái)進(jìn)行測(cè)試。
至此electron云音樂實(shí)戰(zhàn)分享基本結(jié)束,項(xiàng)目中有趣的地方還有很多,但篇幅有限,不能面面俱到。本來(lái)還想說(shuō)說(shuō)那些令人敬禮的css但再不去打lol的衰減局就要掉峽谷宗師了!不排除有下集…第一次寫文章,感謝各位看客老爺看到這里,謝謝。
最后嘮叨一句:“覺得不錯(cuò)給我一個(gè)贊~”