項目地址html
點擊下載應用。vue
macOS用戶請下載dmg文件,windows用戶請下載exe文件,linux用戶請下載AppImage文件。linux
項目當前依賴NeteaseCloudMusicApi,感謝NeteaseCloudMusicApi的做者。git
當前只爲windows作了適配 。github
拖動對話框的身影在項目中仍是挺常見的,如首頁中的欄目調整對話框,收藏歌單等。web
然而Ant Design Vue提供的對話框組件並無提供拖拽的功能,但這一功能在項目中又是不可缺乏的,因此只好本身動手豐衣足食。vuex
封裝一個drop-modal主要分三步:數據庫
$attrs,$slots,$listeners
實現前兩步的目的在於讓書寫drop-modal的語法和a-modal保持基本一致,其中第一步較爲簡單,新建drop-modal,其模板以下:npm
<template>
<a-modal
v-bind="{...$attrs,...$slots}"
v-on="$listeners"
>
<slot></slot>
</a-modal>
</template>
複製代碼
一般咱們在a-modal上經過v-model綁定一個值,經過修改該值來控制對話框的顯示隱藏,就像這樣json
<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上便可。 核心代碼以下:<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**
核心思路在於:動態組件 ,經過操做數組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 // 射線的長度
複製代碼
圓角: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繪製
緊接着將半徑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雲音樂實戰分享基本結束,項目中有趣的地方還有不少,但篇幅有限,不能面面俱到。第一次寫文章,感謝各位看客老爺看到這裏,謝謝。
最後嘮叨一句:「以爲不錯給我一個贊~」