項目地址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的衰減局就要掉峽谷宗師了!不排除有下集..第一次寫文章,感謝各位看客老爺看到這裏,謝謝。
最後嘮叨一句:「以爲不錯給我一個贊~」