Magren

Magren

Idealist & Garbage maker 🛸
twitter
jike

關於CloudMusic

前言#

這個項目是我某天在宿舍聽歌想到的,從 16 年註冊網易雲到現在都有五年的時間了,雖然這個平台現在越來越讓我失望,但是聽了五年了都聽出感情來了,就在這個假期裡面花了將近一個月磨磨蹭蹭寫出來這個項目。至於為什麼寫的是移動端頁面嘛,是因為那時候我聽歌是用著我的手機聽的…… 所以模仿的 UI 也是安卓版網易雲音樂的 UI。雖然功能沒有全部實現,但是比較核心的播放頁面還是做出來了,這篇博客也是記錄一下這個項目的至今的實現過程還有一些坑。

項目基於 Vue + Typescript + Vuetify UI 實現。
項目地址:CloudMusic
已經實現的功能:

  • 登錄
  • 獲取歌單
  • 創建歌單
  • 刪除 / 取消收藏歌單
  • 播放歌曲
  • 排行榜
  • 每日推薦
  • 推薦歌單

項目截圖:
1.png
2.png
3.png
4.png
5.png

播放#

這個可以說是核心功能,畢竟一個聽歌的平台不能聽歌就說不過去了。
首先歌曲的選擇可以來自歌單,可以來自發現頁的輪播圖,還可以來自播放頁面的選擇,然後控制歌曲的播放暫停既可以從其他頁面下方的播放 tab 控制,又可以在播放的頁面進行控制,如果使用 Prop 和 Emit 實在是太多太亂了,所以我這裡使用了 Vuex。

Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。

其實就是把我們需要的組件共享狀態抽取出來,以一個全局單例模式管理,然後我們可以在任何一個組件下通過 Vuex 來修改其狀態或者值。
於是我將所選的歌曲 ID,以及當前播放的歌單信息,ID 傳進了 Vuex,在播放頁面只需要監聽 Vuex 中歌曲 id 的變化,即可播放新的歌曲;監聽 Vuex 中是否暫停的狀態,來控制 audio 組件暫停或者播放。

播放進度#

進度條我用的是 Vuetify UI 中的 slider 組件,通過 v-model 來設定其值。
在 audio 中通過 @timeupdate 來監聽 audio 當前的播放時間戳,接著與播放總時長相除並乘以 100 得到當前播放進度的百分比,接著賦值給 slider 組件即可,進度條左邊的時間也是通過當前播放的時間戳轉換成的時間。

但是這裡會有個問題,就是需要通過移動滑塊來修改播放的進度,在剛剛 @timeupdate 綁定的方法中只是單向的將進度賦值給滑塊,並且在音樂播放的時候這個方法是一直在運行的,這樣無論我怎樣移動滑塊,滑塊都會立馬就瞬移到了當前的播放位置,所以這裡還得加一個監聽是否正在移動滑塊的方法,當移動滑塊的時候不將當前播放的進度賦值給滑塊,然後當我松手的時候將滑塊當前的進度轉換成時間戳賦值回給 audio 的播放時間。

我的解決辦法是給滑塊組件引入了 @mousedown@mouseup,當鼠標按下的時候將一個變量賦值為 true,抬起的時候為 false,修改 @timeupdate 中的方法,設定其只有在該變量為 false 的時候才會對滑塊組件賦值,但是滑動滑塊的時候也得修改當前的播放時間,所以給滑塊組件引入了 @change ,當手動更改了滑塊的值的時候會觸發該方法,在這個方法裡面對 audio 的播放進度進行修改。

6.png

歌詞實現#

從後台獲取的歌詞是一個字符串,但是每行歌詞都用了 \n 標識,所以將字符串以 \n 分割可以得到每行的歌詞,每行歌詞裡中括號裡面的時間,還得將每句歌詞以中括號進行分割,最後將時間轉換成時間戳,與歌詞放在一塊做為一個類放入數組中。

設置一個索引,用於記錄是第幾行歌詞,通過 offsetHeight 獲取到歌詞組件的高度來確認歌詞中線所在的位置,通過索引乘以每行歌詞的高度來判斷是否滾動。

其實就是給外部的 div 設置超出隱藏,然後通過修改 mragin-top 來實現一個滾動的效果,而 margin-top 的值是由中線的高度減去索引乘以每行歌詞的高度來決定。同時可以通過 transition 屬性來設定其改變的時間。

例如:transition: margin-top 1s;
表示的就是說 margin-top 屬性會在一秒內完成。

關於 Vue 的自定義指令#

在製作的過程中我想實現個功能就是點擊某個組件以外的位置的時候隱藏該組件,即點擊的不是該組件的時候會執行一個方法。

我查資料的時候發現 vue 並沒有這麼一個指令,但是我可以通過自己去自定義這麼一個指令來實現這個功能。

首先看看一個自定義的指令對象的鉤子函數:

  • bind:只調用一次,指令第一次綁定到元素時調用。在這裡可以進行一次性的初始化設置。
  • inserted:被綁定元素插入父節點時調用 (僅保證父節點存在,但不一定已被插入文檔中)。
  • update:所在組件的 VNode 更新時調用,但是可能發生在其子 VNode 更新之前。指令的值可能發生了改變,也可能沒有。但是你可以通過比較更新前後的值來忽略不必要的模板更新 (詳細的鉤子函數參數見下)
  • componentUpdated:指令所在組件的 VNode 及其子 VNode 全部更新後調用。
  • unbind:只調用一次,指令與元素解绑時調用。

接著鉤子函數中都有以下幾個參數:

  • el:指令所綁定的元素,可以用來直接操作 DOM。
  • binding:一個對象,包含以下 property:
    • name:指令名,不包括 v- 前綴。
    • value:指令的綁定值,例如:v-my-directive="1 + 1" 中,綁定值為 2。
    • oldValue:指令綁定的前一個值,僅在 update 和 componentUpdated 鉤子中可用。無論值是否改變都可用。
    • expression:字符串形式的指令表達式。例如 v-my-directive="1 + 1" 中,表達式為 "1 + 1"。
    • arg:傳給指令的參數,可選。例如 v-my-directive 中,參數為 "foo"。
    • modifiers:一個包含修飾符的對象。例如:v-my-directive.foo.bar 中,修飾符對象為 {foo: true, bar: true}。
  • vnode:Vue 編譯生成的虛擬節點。移步 VNode API 來了解更多詳情。
  • oldVnode:上一個虛擬節點,僅在 update 和 componentUpdated 鉤子中可用

以上都是來自 Vue 的文檔,雖然有那麼多的鉤子函數和變量但是都是可選的,只選需要用到的即可。在實現這個功能中我也就只用到了 bind 和 unbind,其中的變量也是只用到了 el 和 binding。

import {DirectiveOptions} from "vue";
//自定義指令clickoutside,當點擊的不是當前元素的時候執行綁定的方法
const clickoutside: DirectiveOptions = {
    // 初始化指令
    bind (el: any, binding: any, vnode) {
      function documentHandler (e: any) {
        // 這裡判斷點擊的元素是否是本身,是本身,則返回
        if (el.contains(e.target)) {
          return false
        }
        // 判斷指令中是否綁定了函數
        if (binding.expression) {
          // 如果綁定了函數 則調用那個函數
          binding.value(e)
        }
      }
      // 給當前元素綁定個私有變量,方便在unbind中可以解除事件監聽
      el.vueClickOutside = documentHandler
      document.addEventListener('click', documentHandler)
    },
    unbind (el: any, binding) {
      // 解除事件監聽
      document.removeEventListener('click', el.vueClickOutside)
      delete el.vueClickOutside
    }
  }

  export default clickoutside;

在需要的地方引入:

@Component({
    directives:{
        clickoutside,
    }
})

使用 (當點擊的不是該 div 的時候調用 outside 方法):

<div v-clickoutside="outside"></div>

當我把這個東西實現了不久後我發現 Vuetify 已經集成了這麼一個指令,我可以直接使用,這就是不好好看文檔的下場。

最後⭐️#

🙏 感謝由 網易雲音樂 api 提供的接口以及文檔。
以及這個項目是個人學習而製作,正常使用還請到網易雲音樂👈

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。