Electron+Vue+Ant Design Vue仿網(wǎng)易云音樂windows客戶端實(shí)戰(zhàn)分享

轉(zhuǎn)自:https://blog.csdn.net/weixin_45013937/article/details/100715845
github: https://github.com/xiaozhu188/electron-vue-cloud-music

在這里插入圖片描述

特點(diǎn)

  • 拖拽播放
  • 桌面歌詞
  • mini模式
  • 自定義托盤右鍵菜單
  • 任務(wù)欄縮略圖,歌曲操作
  • 音頻可視化
  • 自動(dòng)/手動(dòng)檢查更新
  • Nedb數(shù)據(jù)庫(kù)持久化
  • 自定義安裝路徑,安裝界面美化
  • 瀏覽器中啟動(dòng)客戶端
  • Travis CL,AppVeyor自動(dòng)構(gòu)建
  • 換膚,下載,本地歌曲匹配,網(wǎng)絡(luò)變化桌面通知,分享歌曲/歌單/MV/視頻等到QQ空間
  • 登錄,私人Fm,歌單,專輯,歌手,排行榜,MV,視頻,評(píng)論,搜索,用戶,動(dòng)態(tài),粉絲,關(guān)注,云盤,收藏…
  • 心動(dòng)模式,歌詞微調(diào),下一首播放,追加播放,單曲循環(huán),隨機(jī)播放,列表循環(huán)
  • 路由導(dǎo)向,局部刷新,首頁(yè)欄目調(diào)整并持久化…

下載 && 運(yùn)行

項(xiàng)目地址

點(diǎn)擊下載應(yīng)用。

macOS用戶請(qǐng)下載dmg文件,windows用戶請(qǐng)下載exe文件,linux用戶請(qǐng)下載AppImage文件。
項(xiàng)目當(dāng)前依賴NeteaseCloudMusicApi,需本地啟動(dòng)該服務(wù)并為接口地址添加/api后綴

基于draggabilly封裝一個(gè)可拖動(dòng)的對(duì)話框

拖動(dòng)對(duì)話框的身影在項(xiàng)目中還是挺常見的,如首頁(yè)中的欄目調(diào)整對(duì)話框,收藏歌單等。

[圖片上傳失敗...(image-c0284d-1602812833312)]

[圖片上傳失敗...(image-c02b39-1602812833312)]

然而Ant Design Vue提供的對(duì)話框組件并沒有提供拖拽的功能,但這一功能在項(xiàng)目中又是不可缺少的,所以只好自己動(dòng)手豐衣足食。

封裝一個(gè)drop-modal主要分三步:

  • 讓drop-modal擁有擁有a-modal的API
  • 在drop-modal上實(shí)現(xiàn)v-model
  • modal首次顯示后實(shí)例化Draggabilly

$attrs,$slots,$listeners

實(shí)現(xiàn)前兩步的目的在于讓書寫drop-modal的語(yǔ)法和a-modal保持基本一致,其中第一步較為簡(jiǎn)單,新建drop-modal,其模板如下:

<template>
  <a-modal
    v-bind="{...$attrs,...$slots}"
    v-on="$listeners"
  >
    <slot></slot>
  </a-modal>
</template>

實(shí)現(xiàn)v-model

通常我們?cè)赼-modal上通過v-model綁定一個(gè)值,通過修改該值來(lái)控制對(duì)話框的顯示隱藏,就像這樣

<a-modal v-model="visible">
  <p>contents</p>
</a-modal>

所以我們也應(yīng)該在drop-modal實(shí)現(xiàn)上實(shí)現(xiàn)v-model。如果了解自定義組件的v-model是:value和@input的語(yǔ)法糖,實(shí)現(xiàn)起來(lái)也不難。

  • 首先定義一個(gè)props value。為了保持單向數(shù)據(jù)流.
  • 再定義一個(gè)計(jì)算屬性 currentValue,在其get方法中返回value,在set方法中觸發(fā)自定義事件
  • 最后將currentValue綁定在a-modal上即可。核心代碼如下:
<a-modal ... v-model="currentValue">
   ...
</a-modal>

computed: {
    currentValue: {
      get () {
        return this.value
      },
      set (val) {
        this.$emit('input', val)
      }
    }
}

實(shí)例化Draggabilly

最后一步也是最重要的一步,通過watch監(jiān)聽 value ,當(dāng)值為true時(shí)實(shí)例一個(gè)Draggabilly讓modal變成可拖動(dòng)。這一步需要注意4點(diǎn):

  1. 確保在nextTick中實(shí)例化Draggabilly
  2. 僅在首次顯示時(shí)實(shí)例化Draggabilly
  3. 確定可拖動(dòng)的dom
  4. modal的嵌套情況

至此封裝的drop-modal滿足當(dāng)前項(xiàng)目的所有需求,當(dāng)然也有不足。

總結(jié)

封裝drop-modal所涉及的vue核心知識(shí)點(diǎn)——$attrs$slots,$listeners,自定義組件的v-model的還原,計(jì)算屬性保持?jǐn)?shù)據(jù)單向,$nextTick。最終代碼 drop-modal**


Vue中優(yōu)雅“操作”dom之調(diào)整欄目順序

動(dòng)態(tài)組件

核心思路在于:動(dòng)態(tài)組件 ,通過操作數(shù)組navs的元素位置來(lái)控制欄目順序。

navs中每個(gè)對(duì)象的key即componentName,hideMore來(lái)控制標(biāo)題的右側(cè)是否顯示更多的鏈接。

navs: [
    {
      name: '獨(dú)家放送',
      key: 'privateContent',
      hideMore: true
    },
    {
      name: '最新音樂',
      key: 'newSong'
    },
    {
      name: '推薦歌單',
      key: 'playlist'
    },
    {
      name: '推薦MV',
      key: 'mv'
    },
    {
      name: '主播電臺(tái)',
      key: 'dj'
    }
  ]

<div v-for="nav in navs">
    <component :is="nav.key" />
</div>

h5的拖拽api

接下來(lái)就是如何操作數(shù)組navs的問題了~ 通過h5的拖拽api改變?cè)匚恢貌⑿挛恢胣ewNavs持久化保存,在頁(yè)面初始化時(shí)使用newNavs渲染欄目組件即可。

此外還結(jié)合了 transition-group 組件,讓欄目順序變化有一個(gè)過渡效果,而這一過渡效果也很好的詮釋了動(dòng)畫的重要意義–“解釋剛剛發(fā)生了什么”

核心代碼如下:

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

最終實(shí)現(xiàn)的效果如下:

[圖片上傳失敗...(image-d7c6c8-1602812833311)]

其他

項(xiàng)目中優(yōu)雅操作dom的地方還很多,原理大同小異,即數(shù)據(jù)驅(qū)動(dòng)。比如進(jìn)度條組件 <div class="buffered" ref="buffered" :style = "{width :${bufferedOffsetWidth}px}"></div> 通過操作變量bufferedOffsetWidth來(lái)控制緩沖條的width

又比如私人fm的歌曲卡片切換,篇幅有限不做過多介紹,詳情請(qǐng)移步fm源碼查看

[圖片上傳失敗...(image-b6ce0d-1602812833311)]

音頻可視化

AudioContext

音頻可視化生動(dòng)點(diǎn)長(zhǎng)這樣,還是挺炫酷的?。。?/p>

[圖片上傳失敗...(image-6cd913-1602812833311)]

[圖片上傳失敗...(image-30cb41-1602812833311)]

項(xiàng)目結(jié)合了兩者實(shí)現(xiàn)了如下效果:射線和動(dòng)態(tài)粒子,區(qū)別在于我的射線較細(xì)較短較密集(當(dāng)然這些都是可控的),以及粒子是向圓內(nèi)波動(dòng)

[圖片上傳失敗...(image-d8f4d3-1602812833311)]

音頻的可視化要點(diǎn)在于使用canvas繪制基于AudioContext獲取到頻譜數(shù)據(jù)。

首先獲取頻譜數(shù)據(jù)

    // 獲取API
    let context = new AudioContext;
    // 加載audio,可以是dom也可以是一個(gè)Audio的實(shí)例
    let audio = new Audio("1.mp3");
    // 創(chuàng)建節(jié)點(diǎn)
    let source = context.createMediaElementSource(audio);
    let analyser = context.createAnalyser();
    // 連接:source → analyser → destination
    source.connect(analyser);
    analyser.connect(context.destination);
    // 創(chuàng)建數(shù)據(jù)
    let output = new Uint8Array(460);
    // 獲取頻域數(shù)據(jù)
    analyser.getByteFrequencyData(output)

打印output,它長(zhǎng)這樣:

[圖片上傳失敗...(image-12adea-1602812833311)]

使用canvas繪制

首先繪制靜態(tài)的外射線,注意觀察每條射線

  const { width, height } = document.getElementById('canvas')
  const du = 3 // 圓心到兩條射線距離所成的角度,即射線的間隙
  const potInt = { x: width / 2, y: height / 2 } // 起始坐標(biāo),即畫布中心
  const R = 150 // 半徑
  const W = 4 // 射線的寬度
  const L = 32 // 射線的長(zhǎng)度

  • 圓角:cxt.lineCap = ‘round’

  • 漸變:cxt.createLinearGradient(x1,y1,x2,y2)

  • 起始點(diǎn):(Math.sin(((i * du) / 180) * Math.PI) * R + potInt.y,-Math.cos(((i * du) / 180) * Math.PI) * R + potInt.x)

  • 結(jié)束點(diǎn):(Math.sin(((i * du) / 180) * Math.PI) * (R + L) + potInt.y, -Math.cos(((i * du) / 180) * Math.PI) * (R + L) + potInt.x)

其中i為循環(huán)360度的索引。確定了每條射線的起始點(diǎn)和結(jié)束點(diǎn),也就確定了漸變的起始點(diǎn)和結(jié)束點(diǎn)。通過moveTo,lineTo繪制

[圖片上傳失敗...(image-423aec-1602812833311)]

緊接著將半徑R擴(kuò)大 let Rv = R + value ,先寫死1再繪制一層純色層疊加在漸變層之上。之后動(dòng)態(tài)改變value即可實(shí)現(xiàn)動(dòng)畫效果,但要注意漸變層的射線應(yīng)該總是大于純色層射線L的長(zhǎng)度。

[圖片上傳失敗...(image-21d4b7-1602812833311)]

canvas動(dòng)畫當(dāng)然是少不了 cxt.clearRect(0, 0, width, height)requestAnimationFrame 啦!動(dòng)畫及粒子向內(nèi)的波動(dòng)實(shí)現(xiàn)請(qǐng)參考musicView源碼

渲染進(jìn)程的即時(shí)通訊

項(xiàng)目一大重點(diǎn)難點(diǎn)是如何將store中歌詞,播放狀態(tài)等數(shù)據(jù)實(shí)時(shí)的在各窗體中共享。一開始想通過主進(jìn)程來(lái)做中轉(zhuǎn),但主進(jìn)程微笑而不失禮貌地婉拒了:“渲染進(jìn)程能處理的事就不要拿來(lái)騷擾我啦,我很忙的!”。最后把目光投向了localstorage。原理在于訂閱mutation改變storage,監(jiān)聽storage觸發(fā)更新state,通過書寫一個(gè)vuex插件來(lái)實(shí)現(xiàn)這一功能,詳情請(qǐng)查看 keep-state.js

usage:

在store入口文件引入keep-state,keep-state插件是一個(gè)函數(shù),傳入需要監(jiān)聽模塊mudules執(zhí)行函數(shù),在初始化stroe時(shí)將函數(shù)的執(zhí)行結(jié)果賦予plugins。

import persistStatePlugin from './plugins/keep-state'
const myPlugin = persistStatePlugin(['User', 'play', 'Localsong', 'Setting', 'Update'])
const store = new Vuex.Store({
  ...
  plugins: [myPlugin]
})

electron實(shí)戰(zhàn)之桌面歌詞

[圖片上傳失敗...(image-174311-1602812833311)]

實(shí)現(xiàn)桌面歌詞需要注意以下幾點(diǎn):

  1. 透明窗體

  2. 窗口在別的窗口上面

  3. 可鎖定(鎖定后忽略窗口內(nèi)的所有鼠標(biāo)事件)

  4. 出現(xiàn)在屏幕的位置

通過設(shè)置transparent:true,alwaysOnTop: true可分別實(shí)現(xiàn)窗體透明和窗體置頂,其中透明窗體要注意html,body,#app等不能設(shè)置非透明的背景色。

通過 setignoremouseeventsignore api可切換鎖定窗體。

至于窗體初始時(shí)的位置,默認(rèn)是屏幕中央。我想讓他水平居中,垂直在任務(wù)欄偏上一點(diǎn),這就需要獲取屏幕的高來(lái)做點(diǎn)文章了 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, // 任務(wù)欄中不顯示窗口面板
    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實(shí)戰(zhàn)之mini模式

[圖片上傳失敗...(image-cf3739-1602812833311)]

[圖片上傳失敗...(image-633bfb-1602812833311)]

mini模式主要分為兩部分:

  1. 主面板
  2. 當(dāng)前播放列表面板

其中主面板又分三個(gè)面板:

  1. 歌曲縮略圖,按住可拖動(dòng)
  2. 歌曲信息及工具欄
  3. 相關(guān)操作面板

實(shí)現(xiàn)要點(diǎn)在于隱藏主窗體,顯示mini窗體(320*50)。通過win.setBounds()在切換下拉列表時(shí)動(dòng)態(tài)改變窗體大小

electron實(shí)戰(zhàn)之自定義托盤菜單

通過electron Tray模塊的實(shí)例的setContextMenu方法創(chuàng)建的菜單是真的丑不忍睹…
[圖片上傳失敗...(image-139b63-1602812833311)]

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

[圖片上傳失敗...(image-aa3d00-1602812833311)]

答案之一就是通過一個(gè)窗體來(lái)模擬。通過監(jiān)聽托盤的右鍵點(diǎn)擊事件切換菜單的顯示隱藏即可,其中需要實(shí)時(shí)計(jì)算出每次菜單出現(xiàn)的位置及邊界情況。

electron實(shí)戰(zhàn)之自定義任務(wù)欄的縮略圖工具欄

任務(wù)欄工具欄?長(zhǎng)這樣,包含標(biāo)題縮略圖,及歌曲的相關(guān)操作。

[圖片上傳失敗...(image-1d1b7-1602812833311)]

幸運(yùn)的,electron提供相關(guān)API實(shí)現(xiàn)這一功能 縮略圖工具欄

electron實(shí)戰(zhàn)之拖拽播放

介紹

拖拽播放分三種:

  1. 將文件拖到主窗體內(nèi)實(shí)現(xiàn)播放
  2. 將文件拖動(dòng)到桌面上的快捷方式圖標(biāo)打開客戶端并播放
  3. 客戶端已經(jīng)打開,將文件拖動(dòng)到桌面上的快捷方式圖標(biāo)實(shí)現(xiàn)播放(不會(huì)打開第二個(gè)實(shí)例)

禁用默認(rèn)行為

在實(shí)現(xiàn)之前請(qǐng)先看看默認(rèn)將文件拖動(dòng)到客戶端會(huì)發(fā)生什么?
是的,默認(rèn)和將文件拖動(dòng)到Chrome瀏覽器是一樣的,就像這樣…

[圖片上傳失敗...(image-fadd22-1602812833311)]

就猜到會(huì)是這樣了…!
[圖片上傳失敗...(image-4f35c3-1602812833311)]

所以我們第一步就是要禁用掉這些默認(rèn)行為:

  window.ondragenter = (event) => {
      event.preventDefault()
  }
  window.ondragover = (event) => {
      event.preventDefault()
  }
  window.ondrop = openFilesOndrop

將文件拖到主窗體內(nèi)實(shí)現(xiàn)播放

監(jiān)聽window的drop事件來(lái)實(shí)現(xiàn)我們的打開文件操作。這只是實(shí)現(xiàn)了拖拽播放中的第一種情況。

其他兩種情況在windows平臺(tái)上需要在process.argv上動(dòng)動(dòng)手腳。

將文件拖動(dòng)到桌面上的快捷方式圖標(biāo)打開客戶端并播放

先說(shuō)說(shuō)第二種情況,在主進(jìn)程的appready的事件回調(diào)中將process.argv賦予全局變量global global.argv = process.argv,在渲染進(jìn)程中通過electron的remote模塊的getGlobal方法獲取到argv。process.argv初始化長(zhǎng)這樣:["E:\electron-vue-cloud-music\網(wǎng)易云音樂.exe"] 即客戶端的可執(zhí)行文件的路徑。所以在執(zhí)行handleWillOpenFiles方法前判斷一下數(shù)組長(zhǎng)度。在handleWillOpenFiles方法過濾出.mp3文件進(jìn)行相關(guān)解析播放等操作。詳情移步 createdInit

import { remote } from 'electron'
const startArgv = remote.getGlobal('argv')
  if (startArgv.length > 1) {
      handleWillOpenFiles(startArgv)
  }

客戶端已經(jīng)打開,將文件拖動(dòng)到桌面上的快捷方式圖標(biāo)實(shí)現(xiàn)播放

至于第三種情況和第二種大同小異,區(qū)別在于argv的參數(shù)的獲取以及渲染進(jìn)程如何拿到argv。對(duì)于argv的獲取,在主進(jìn)程的app的second-instance監(jiān)聽回調(diào)中獲取,通過自定義事件分發(fā),渲染進(jìn)程監(jiān)聽該自定義事件來(lái)接受。

// 主進(jìn)程
app.on('second-instance', (event, argv, workingDirectory) => {
    if (mainWindow) {
        mainWindow.webContents.send('open-files', {argv})
    }
})
// 渲染進(jìn)程
import { ipcRenderer} from 'electron'
ipcRenderer.on('open-files', async (event, args) => {
    let { argv } = args
    handleWillOpenFiles(argv)
})

electron實(shí)戰(zhàn)之自動(dòng)/手動(dòng)檢查更新

當(dāng)前自動(dòng)更新已移除,簡(jiǎn)單說(shuō)說(shuō)如何實(shí)現(xiàn)手動(dòng)檢查更新,具體流程是這樣的:

  1. 開發(fā),commit
  2. npm version patch && git push origin master && git push origin --tags
  3. Travis CL,AppVeyor監(jiān)測(cè)到master變化自動(dòng)構(gòu)建
  4. github上編輯發(fā)布遠(yuǎn)程版本
  5. 用戶/客戶端觸發(fā)檢查更新
  6. 客戶端調(diào)用github API獲取最新的遠(yuǎn)程版本號(hào)與本地版本號(hào)對(duì)比
  7. 如若需要更新顯示更新窗體引導(dǎo)下載安裝

[圖片上傳失敗...(image-e178f5-1602812833311)]

[圖片上傳失敗...(image-3ddfe2-1602812833311)]

下載完成后關(guān)閉窗體并打開下載文件進(jìn)行安裝

electron實(shí)戰(zhàn)之Nedb數(shù)據(jù)庫(kù)持久化

Nedb數(shù)據(jù)庫(kù) 主要用來(lái)存儲(chǔ)下載的歌曲列表及歌詞。盜用官網(wǎng)介紹就是:

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級(jí)水平簡(jiǎn)短白話翻譯是為Electron而生,無(wú)依賴,快,使用和mongoDb差不多

electron實(shí)戰(zhàn)之打包自定義安裝路徑,安裝界面美化

自定義安裝路徑較為簡(jiǎn)單在package.json中找到build字段加入以下代碼即可

"nsis": {
  "oneClick": false, // 是否一鍵安裝
  "allowToChangeInstallationDirectory": true // 是否允許修改安裝路徑
}

自定義可通過一些開源工具來(lái)快捷實(shí)現(xiàn) NSIS-UI 簡(jiǎn)單實(shí)現(xiàn)了一下,效果還可以:

[圖片上傳失敗...(image-791022-1602812833310)]

electron實(shí)戰(zhàn)之自定義協(xié)議實(shí)現(xiàn)瀏覽器中啟動(dòng)客戶端

通過app.setAsDefaultProtocolClient可實(shí)現(xiàn)自定義協(xié)議在瀏覽器中喚起客戶端,如果安裝過了可嘗試 打開electron云音樂

electron實(shí)戰(zhàn)之離線/在線偵測(cè)與桌面通知

通過window的onlineoffline可監(jiān)聽網(wǎng)絡(luò)狀態(tài)。

通過navigator.onLine可判斷當(dāng)前網(wǎng)絡(luò)狀態(tài).

通過h5的Notification可實(shí)現(xiàn)桌面通知,在window平臺(tái)中使用請(qǐng)確保設(shè)置appId

[圖片上傳失敗...(image-1e2fa5-1602812833310)]

Travis CL,AppVeyor自動(dòng)構(gòu)建

分享一篇阮一峰的一篇文章即可 持續(xù)集成


結(jié)語(yǔ)

當(dāng)前項(xiàng)目只對(duì)window平臺(tái)進(jìn)行測(cè)試。

至此electron云音樂實(shí)戰(zhàn)分享基本結(jié)束,項(xiàng)目中有趣的地方還有很多,但篇幅有限,不能面面俱到。本來(lái)還想說(shuō)說(shuō)那些令人敬禮的css但再不去打lol的衰減局就要掉峽谷宗師了!不排除有下集…第一次寫文章,感謝各位看客老爺看到這里,謝謝。

最后嘮叨一句:“覺得不錯(cuò)給我一個(gè)贊~”

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容