轉自: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點:
- 確保在nextTick中實例化Draggabilly
- 僅在首次顯示時實例化Draggabilly
- 確定可拖動的dom
- 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)]
實現桌面歌詞需要注意以下幾點:
透明窗體
窗口在別的窗口上面
可鎖定(鎖定后忽略窗口內的所有鼠標事件)
出現在屏幕的位置
通過設置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模式主要分為兩部分:
- 主面板
- 當前播放列表面板
其中主面板又分三個面板:
- 歌曲縮略圖,按住可拖動
- 歌曲信息及工具欄
- 相關操作面板
實現要點在于隱藏主窗體,顯示mini窗體(320*50)。通過win.setBounds()在切換下拉列表時動態改變窗體大小
electron實戰之自定義托盤菜單
通過electron Tray模塊的實例的setContextMenu
方法創建的菜單是真的丑不忍睹…
[圖片上傳失敗...(image-139b63-1602812833311)]
如何自定義一個托盤菜單呢?就像這樣:
[圖片上傳失敗...(image-aa3d00-1602812833311)]
答案之一就是通過一個窗體來模擬。通過監聽托盤的右鍵點擊事件切換菜單的顯示隱藏即可,其中需要實時計算出每次菜單出現的位置及邊界情況。
electron實戰之自定義任務欄的縮略圖工具欄
任務欄工具欄?長這樣,包含標題縮略圖,及歌曲的相關操作。
[圖片上傳失敗...(image-1d1b7-1602812833311)]
幸運的,electron提供相關API實現這一功能 縮略圖工具欄
electron實戰之拖拽播放
介紹
拖拽播放分三種:
- 將文件拖到主窗體內實現播放
- 將文件拖動到桌面上的快捷方式圖標打開客戶端并播放
- 客戶端已經打開,將文件拖動到桌面上的快捷方式圖標實現播放(不會打開第二個實例)
禁用默認行為
在實現之前請先看看默認將文件拖動到客戶端會發生什么?
是的,默認和將文件拖動到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實戰之自動/手動檢查更新
當前自動更新已移除,簡單說說如何實現手動檢查更新,具體流程是這樣的:
- 開發,commit
- npm version patch && git push origin master && git push origin --tags
- Travis CL,AppVeyor監測到master變化自動構建
- github上編輯發布遠程版本
- 用戶/客戶端觸發檢查更新
- 客戶端調用github API獲取最新的遠程版本號與本地版本號對比
- 如若需要更新顯示更新窗體引導下載安裝
[圖片上傳失敗...(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的online
和offline
可監聽網絡狀態。
通過navigator.onLine
可判斷當前網絡狀態.
通過h5的Notification
可實現桌面通知,在window平臺中使用請確保設置appId
[圖片上傳失敗...(image-1e2fa5-1602812833310)]
Travis CL,AppVeyor自動構建
分享一篇阮一峰的一篇文章即可 持續集成
結語
當前項目只對window平臺進行測試。
至此electron云音樂實戰分享基本結束,項目中有趣的地方還有很多,但篇幅有限,不能面面俱到。本來還想說說那些令人敬禮的css但再不去打lol的衰減局就要掉峽谷宗師了!不排除有下集…第一次寫文章,感謝各位看客老爺看到這里,謝謝。
最后嘮叨一句:“覺得不錯給我一個贊~”