2019Electron+Vue+Ant Design Vue仿網易雲音樂windows客戶端實戰分享

特色

  • 拖拽播放
  • 桌面歌詞
  • mini模式
  • 自定義托盤右鍵菜單
  • 任務欄縮略圖,歌曲操做
  • 音頻可視化
  • 自動/手動檢查更新
  • Nedb數據庫持久化
  • 自定義安裝路徑,安裝界面美化
  • 瀏覽器中啓動客戶端
  • Travis CL,AppVeyor自動構建
  • 換膚,下載,本地歌曲匹配,網絡變化桌面通知,分享歌曲/歌單/MV/視頻等到QQ空間
  • 登陸,私人Fm,歌單,專輯,歌手,排行榜,MV,視頻,評論,搜索,用戶,動態,粉絲,關注,雲盤,收藏...
  • 心動模式,歌詞微調,下一首播放,追加播放,單曲循環,隨機播放,列表循環
  • 路由導向,局部刷新,首頁欄目調整並持久化...
  • ...

下載 && 運行

項目地址css

點擊下載應用html

macOS用戶請下載dmg文件,windows用戶請下載exe文件,linux用戶請下載AppImage文件。

項目當前依賴NeteaseCloudMusicApi,感謝NeteaseCloudMusicApi的做者。vue

當前只爲windows作了適配 。linux

基於draggabilly封裝一個可拖動的對話框

拖動對話框的身影在項目中仍是挺常見的,如首頁中的欄目調整對話框,收藏歌單等。git

然而Ant Design Vue提供的對話框組件並無提供拖拽的功能,但這一功能在項目中又是不可缺乏的,因此只好本身動手豐衣足食。github

封裝一個drop-modal主要分三步:
  • 讓drop-modal擁有擁有a-modal的API
  • 在drop-modal上實現v-model
  • modal首次顯示後實例化Draggabilly

$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>

實現v-model

一般咱們在a-modal上經過v-model綁定一個值,經過修改該值來控制對話框的顯示隱藏,就像這樣vuex

<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上便可。

核心代碼以下:npm

<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之調整欄目順序

動態組件

核心思路在於:動態組件 <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>

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))
      }
    }
}

最終實現的效果以下:

其餘

項目中優雅操做dom的地方還不少,原理大同小異,即 數據驅動。好比進度條組件 <div class="buffered" ref="buffered" :style = "{width : ${ bufferedOffsetWidth}px }"></div> 經過操做變量 bufferedOffsetWidth來控制緩衝條的width
又好比私人fm的歌曲卡片切換,篇幅有限不作過多介紹,詳情請移步 fm源碼查看

音頻可視化

AudioContext

音頻可視化生動點長這樣,仍是挺炫酷的!!!

項目結合了二者實現了以下效果:射線和動態粒子,區別在於個人射線較細較短較密集(固然這些都是可控的),以及粒子是向圓內波動

音頻的可視化要點在於使用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,它長這樣:

使用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繪製

緊接着將半徑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]
})

electron實戰之桌面歌詞

實現桌面歌詞須要注意如下幾點:

  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模式

mini模式主要分爲兩部分:

  1. 主面板
  2. 當前播放列表面板

其中主面板又分三個面板:

  1. 歌曲縮略圖,按住可拖動
  2. 歌曲信息及工具欄
  3. 相關操做面板

實現要點在於隱藏主窗體,顯示mini窗體(320*50)。經過win.setBounds()在切換下拉列表時動態改變窗體大小

electron實戰之自定義托盤菜單

經過electron Tray模塊的實例的setContextMenu方法建立的菜單是真的醜不忍睹..

如何自定義一個托盤菜單呢?就像這樣:

答案之一就是經過一個窗體來模擬。經過監聽托盤的右鍵點擊事件切換菜單的顯示隱藏便可,其中須要實時計算出每次菜單出現的位置及邊界狀況。

electron實戰之自定義任務欄的縮略圖工具欄

任務欄工具欄?長這樣,包含標題縮略圖,及歌曲的相關操做。

幸運的,electron提供相關API實現這一功能 縮略圖工具欄

electron實戰之拖拽播放

介紹

拖拽播放分三種:

  1. 將文件拖到主窗體內實現播放
  2. 將文件拖動到桌面上的快捷方式圖標打開客戶端並播放
  3. 客戶端已經打開,將文件拖動到桌面上的快捷方式圖標實現播放(不會打開第二個實例)

禁用默認行爲

在實現以前請先看看默認將文件拖動到客戶端會發生什麼?
是的,默認和將文件拖動到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)
})

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. 如若須要更新顯示更新窗體引導下載安裝


下載完成後關閉窗體並打開下載文件進行安裝

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 簡單實現了一下,效果還能夠:

electron實戰之自定義協議實現瀏覽器中啓動客戶端

經過app.setAsDefaultProtocolClient可實現自定義協議在瀏覽器中喚起客戶端,若是安裝過了可嘗試 打開electron雲音樂

electron實戰之離線/在線偵測與桌面通知

經過window的onlineoffline可監聽網絡狀態。

經過navigator.onLine可判斷當前網絡狀態.

經過h5的Notification可實現桌面通知,在window平臺中使用請確保設置appId

Travis CL,AppVeyor自動構建

分享一篇阮一峯的一篇文章便可 持續集成


結語

當前項目只對window平臺進行測試。

至此electron雲音樂實戰分享基本結束,項目中有趣的地方還有不少,但篇幅有限,不能面面俱到。原本還想說說那些使人敬禮的css但再不去打lol的衰減局就要掉峽谷宗師了!不排除有下集..第一次寫文章,感謝各位看客老爺看到這裏,謝謝。

最後嘮叨一句:「以爲不錯給我一個贊~」

其餘界面預覽(多圖預警)











相關文章
相關標籤/搜索