沒朋友(mpvue),還有音樂陪你

我服啦發表於2018-07-22

炎炎夏日,劇烈的高溫也抵擋不住對學習的堅持。對於學習,很多時候都會有鬆懈和逃避的心理,但看到身邊近乎瘋狂的考研黨們,我還是選擇了堅持。距離上次發文章已經有一個月之久了,期間學習了很多知識,越學習越發覺得自己的路還有很長,但既然選擇了遠方,便只顧風雨兼程了。

mpvue仿網易雲音樂小程式


關於mpvue

mpvue 是一個使用 Vue.js 開發小程式的前端框架。框架基於 Vue.js 核心,mpvue 修改了 Vue.js 的 runtime 和 compiler 實現,使其可以執行在小程式環境中,從而為小程式開發引入了整套 Vue.js 開發體驗。

專案使用的技術棧

  • 資料請求:Fly.js 一個基於Promise的、強大的、支援多種JavaScript執行時的http請求庫. 有了它,您可以使用一份http請求程式碼在瀏覽器、微信小程式、Weex、Node中都能正常執行。同時可以方便配合 Vue家族的框架,最大可能的實現 Write Once Run Everywhere。
  • 模擬資料:easy-mock 可以快速生成模擬資料的持久化服務
  • css預編譯器:stylus-基於Node.js的CSS的預處理框架
  • vuex:集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化
  • 框架:mpvue

關於模擬資料這裡提一個網易雲音樂api,真的很強但由於小程式不支援本地域名的request請求,需要部署到伺服器上變為小程式合法域名,由於精力有限這裡只是擷取了部分資料進行模擬。有興趣的小夥伴可以自己玩玩啊。

上圖


沒朋友(mpvue),還有音樂陪你
沒朋友(mpvue),還有音樂陪你


分享(採坑)記

寫這個專案的時候,碰到了很多問題,BUG更是不少,在mpvue與原生小程式中瘋狂糾結,由於時間精力有限,目前只完成了三個頁面,重點把核心播放功能完成了大半。由於網上關於mpvue的資源很少,遇到問題都是在瘋狂百度,嘗試等等方法也算是解決了部分。這裡分享我遇到的一些困難,希望能幫助到有需要的人。

1.關於資料請求封裝

  • 安裝flyio

npm install flyio

  • 在util下新建flyio.js
import Vue from 'vue'
var Fly = require('flyio/dist/npm/wx.js') //wx.js為flyio的微信小程式入口檔案
var fly = new Fly();

fly.interceptors.request.use((config,promise)=>{
    config.headers["X-Tag"]="flyio";  //給所有請求新增自定義header
    return config;
})
//配置請求基地址
fly.config.baseURL="https://www.easy-mock.com/mock/5b372361808a747e8d04a1e3/"
Vue.prototype.$http=fly //將fly掛載在vue上供全域性使用
export default fly
複製程式碼
  • 可以在根目錄main.js目錄下封裝一個方法用到請求資料的頁面直接呼叫這個方法即可。提高程式碼複用率,這裡由於使用了vuex來管理資料即更加的方便操作使用資料。

2.vuex管理資料

  • vuex的準備 npm install vuex
  • vue-cli + vuex 在一般的vue-cli + vuex專案中,主函式 main.js 中會將 store 物件提供給 “store” 選項,這樣可以把 store 物件的例項注入所有的子元件中,從而在子元件中可以用this.$store.state.xxx,this.$store.dispatch 等來訪問或操縱資料倉儲中的資料
new Vue({
el: '#app',
store,
router,
template: '<App/>',
components: { App }
})
複製程式碼
  • mpvue + vuex

注意了,在mpvue + vuex專案中,很遺憾不能通過上面那種方式來將store物件例項注入到每個子元件中(至少我嘗試N種配置不行),也就是說,在子元件中不能使用this.$store.xxx,從而導致輔助函式不能正確使用。這個時候我們就需要換個思路去實現,要在每個子元件中能夠訪問this.$store才行。既然我們需要在子元件中用this.$store 訪問store例項,那我們直接在vue的原型上新增$store屬性指向store物件不就行啦,於是解決方案如下:

Vue.prototype.$store = store

src下新建一個store資料夾,目錄結構如下

action.js  //提交mutation以達到委婉地修改state狀態,可非同步操作
getters.js  //獲取store內的狀態 
index.js //引入vuex,設定state狀態資料,引入getter、mutation和action
mutation-type.js  //將方法與方法名分開便於檢視
mutations.js //更改store中狀態用的函式的儲存之地
state.js //存放state
複製程式碼

這裡展示下index.js內容

import Vue from 'vue'
import Vuex from 'vuex'
import * as actions from './actions'
import * as getters from './getters'
import state from './state'
import mutations from './mutations'

Vue.use(Vuex)

export default new Vuex.Store({
  actions,
  getters,
  state,
  mutations
})
複製程式碼
  • vuex的使用 在fly請求資料後我們可以將其資料儲存到state裡 方便其他頁面或元件使用、修改,在本專案中請求資料可以資料儲存下來,後續其它頁面需要此資料時可以不用再次請求直接引入呼叫即可,大大減少了資料請求次數
 methods: {
    ...mapMutations({ //es6 展開運算子
      saveDetailState: 'SAVE_DETAIL_STATE',
      saveMidimg2: 'SAVE_MIDIMG2',
      saveMidimg3: 'SAVE_MIDIMG3'
    }),
    fly
      .get('music#!method=get')
      .then(res => {
          this.menu = res.data.data.menu;
          this.midimg = res.data.data.midimg;
          this.saveMidimg2(res.data.data.midimg2);  //子元件標題資料
          this.saveDetailState(res.data.data.footimg);//推薦歌單資訊
          this.saveMidimg3(res.data.data.midimg3);//
          this.footimg2 = res.data.data.footimg2;
          this.footimg3 = res.data.data.footimg3;
      })
複製程式碼

3.小程式css字首問題

由於本人開發專案使用的css預編譯器stylus 他會自動補全字首,而小程式又並不支援-moz- -webkit-等字首那怎麼辦呢? 解決方案如下:

<style lang="stylus">
...
</style>
 <style> //不在stylus編譯下
  @keyframes rotate {
  0%{
    transform: rotate(0);}
  100%  {
       transform: rotate(360deg);
  }
    }
 </style> 
複製程式碼

4.建立建立並返回內部 audio 上下文物件

  • api
    這裡我使用的是wx.createInnerAudioContext()api 是wx.createAudioContext 升級版。 由於初次使用audio並不是很熟悉,碰到了一個坑。播放音樂後返回歌單播放另一首歌曲,上一首歌曲不會被替換,兩首歌竟同時播放,建立audio物件後要結束這個例項需要進行銷燬,注意這裡我們需要將這個audio物件進行儲存不然再次跳轉播放頁面找不到銷燬的物件
mounted(){
        wx.showLoading({
            title: '載入中'
        })
        this.midshow = true
        let options = this.$root.$mp.query;
        const songid = options.songid;
        const id = options.id;
        if(this.audioCtx != null) { //第一次不用銷燬
            if(this.song.id != songid){
                this.audioCtx.destroy()} //銷燬例項
        }
        if(this.song.id != songid || this.song.id == '') //同一首歌不用新建
        {   this.audioCtx = wx.createInnerAudioContext() //新建例項
            }
        if(options.name == 'undefined' && this.song.id != songid){ // 同一首歌不用重新更新資料
        this.getSong(songid,id) //獲取當前音樂資訊
        const afterlyric = this.normalizeLyric(this.song.lyric)
        this.currentLyric = new Lyric(afterlyric)
        ...
複製程式碼
  • 小問題
    這裡還碰到一個並不是很理解的地方,就是傳入歌曲連結src 後獲取總時長並沒有獲取 猜測是引入src獲取歌曲資訊需要時間? 這裡用了很粗暴的方法解決了,應該是使用promise進行非同步處理?期待大家能給個指導意見
 this.audioCtx.src = this.song.mp3url //設定src
 this.allmiao = this.audioCtx.duration //讀取歌曲總時長
 
 const a = setInterval(()=>{   
    this.allmiao = this.audioCtx.duration
    },50)
    if(this.allmiao){
    clearInterval('a')
}
複製程式碼

5.音樂播放條

沒朋友(mpvue),還有音樂陪你

  • 小點和線條分別通過translate3d,和wdith進行向右的增加等,
copmputed: {
 width () {
            return 'width:'+(this.nowmiao/this.allmiao)*550+'rpx' //盒子寬度總為550rpx
        },
midwidth () {
            return 'transform:translate3d('+(this.nowmiao/this.allmiao)*529+'rpx,0px,0px);'
        }}
複製程式碼
  • 點選、滑動進度條事件
clClick (e) { //點選進度條事件
            const rect = wx.getSystemInfoSync().windowWidth //螢幕總寬
            this.offsetWidth = e.pageX - (rect-275)/2 //點選進度條上距離左側寬度
            this.nowmiao = this.allmiao*(this.offsetWidth/275) //根據比例計算點選位置對應的播放時間
            const miao = Math.floor(this.nowmiao)
            this.audioCtx.seek(miao) // 跳轉播放
        },
        // 滑動進度條 分為start、move、end事件 這個大家應該不陌生,思路都是根據距離計算比例 跳轉時間點播放
         clstart (e) {
            this.touch.initiated = true
            const rect = wx.getSystemInfoSync().windowWidth
            this.touch.setWidth = (rect-275)/2
            this.touch.startX = e.touches[0].pageX 
            this.touch.time = this.nowmiao
        },
        clmove (e) { //這裡要注意進度條臨界點 滑動距離超出的問題
            if(!this.touch.initiated) return;
            const movex = e.touches[0].pageX - this.touch.startX
            if(e.touches[0].pageX>=(this.touch.setWidth+275)) {this.nowmiao =this.allmiao }
            else if(e.touches[0].pageX<=this.touch.setWidth) {this.nowmiao = 0;}
            else {this.nowmiao = this.touch.time+this.allmiao*movex/275}
        },
        clend (e) {
            this.touch.initiated = false
            const miao = Math.floor(this.nowmiao)
            this.audioCtx.seek(miao)
        },
複製程式碼

6.歌詞滾屏

沒朋友(mpvue),還有音樂陪你
這裡使用了黃軼老師寫的一個lyric-parser使用他對歌詞資料進行處理等,通過sroll-view-into進行跟隨播放進度的滾屏

  <scroll-view class="lyric-wrapper" :scroll-into-view="'line'+toLineNum" scroll-y scroll-with-animation>
    <view v-if="currentLyric">
        <view  :id="'line'+i" class="text" :class="[currentLineNum == i ? 'current': '' ]" v-for="(item,i) of currentLyric.lines" :key="i">
            {{item.txt}}
        </view>
    </view>
    <view v-if="!currentLyric">
        <view class="text current">暫無歌詞</view>
    </view>
</scroll-view>
複製程式碼

將歌詞進行處理後v-for輸出 通過id、class分別進行滾屏與當前播放歌詞行的高亮

normalizeLyric: function (lyric) {//將歌詞資料進行拆分
    return lyric.replace(/&#58;/g, ':').replace(/&#10;/g, '\n').replace(/&#46;/g, '.').replace(/&#32;/g, ' ').replace(/&#45;/g, '-').replace(/&#40;/g, '(').replace(/&#41;/g, ')')
},
const afterlyric = this.normalizeLyric(this.song.lyric) 
this.currentLyric = new Lyric(afterlyric) //建立Lyric例項
this.audioCtx.onTimeUpdate(()=>{ //音訊播放進度更新事件 
            this.nowmiao = this.audioCtx.currentTime //當前播放時間
            if (this.currentLyric) {
            this.handleLyric(this.nowmiao * 1000) //歌詞行處理函式
        }
        })
 handleLyric (currentTime) { //控制歌詞滾屏 隨播放進度不斷觸發
            let lines = [{time: 0, txt: ''}], lyric = this.currentLyric, lineNum
            lines = lines.concat(lyric.lines) //進行歌詞對應時間、內容
            //判斷當前播放時間位置 進行歌詞行的調整 使其當前歌詞部分處於中間,若開頭則從頭部往下,尾部反之
            for (let i = 0; i < lines.length; i++) {
                if (i < lines.length - 1) {
                    let time1 = lines[i].time, time2 = lines[i + 1].time
                if (currentTime > time1 && currentTime < time2) { 
                    lineNum = i - 1
                    break;
                    }
                } else {
                lineNum = lines.length - 2
                }
            }
            this.currentLineNum = lineNum,
            this.currentText = lines[lineNum + 1] && lines[lineNum + 1].txt
            let toLineNum = lineNum - 5
            if (lineNum > 5 && toLineNum != this.toLineNum) {
                this.toLineNum = toLineNum
            }
        },        
複製程式碼

7.元件複用

在專案中有很多部分是非常相似的,將這部分進行元件封裝通過v-if、class、style等進行新增修改元件部分,元件封裝要易於維護,高效能,低耦合。這裡展示下首頁中部元件

<scroll-view scroll-y="true" enable-back-to-top="true" class="mid-top">
            <swiper-t :menu="menu"></swiper-t>
            <mid-t :midimg="midimg"></mid-t>
            <foot-t :footimg="footimg" :footname="footname1"></foot-t>
            <foot-t :footimg="footimg2" :footname="footname2"></foot-t>
            <foot-t :footimg="footimg3" :footname="footname3"></foot-t>
</scroll-view>
複製程式碼

寫在最後

雖然只是寫了三個頁面,但也確實讓我體會到了mpvue框架好用的地方,對於那些不太熟悉小程式對vue比較熟悉的人mpvue是一個好用的框架了,但還有很多地方需要完善。文章寫到這裡其實還有東西可寫,比如切歌 播放模式等等,這裡就不一一闡述了。寫文章,志在分享,志在認識更多志同道合的盆友,也算是自己的一個總結梳理吧。這裡附上我的專案地址,有興趣的朋友可以看看玩玩,幫忙點個star~作為一個即將出去闖蕩的大三學生,時間是真的很寶貴,對待學習也不敢懈怠,如果你有什麼好的建議或者問題歡迎加我qq:1404827307,寫文章不易,且贊且珍惜。前端路漫漫,穩步向前行!

沒朋友(mpvue),還有音樂陪你

相關文章