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的衰減局就要掉峽谷宗師了!不排除有下集…第一次寫文章,感謝各位看客老爺看到這里,謝謝。

最后嘮叨一句:“覺得不錯給我一個贊~”

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。