基於vue的移動端web音樂播放器

聲明

如下只是學習完慕課網huangyi老師實戰視頻課程的筆記內容,僅供我的參考學習使用。
若是對Vue2.0實戰高級-開發移動端音樂WebApp感興趣的話,請移步這裏:
https://coding.imooc.com/clas...
謝謝。html

項目GitHub地址: https://github.com/bjw1234/vu...vue

項目演示地址: http://music.baijiawei.topwebpack

項目初始化

// 安裝vue腳手架工具
npm install vue-cli -g

// 初始化webpack應用
vue init webpack vue-music

項目中使用到的mixin

// 背景圖片
bg-image($url)
  background-image: url($url + "@2x.png")
  @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3)
    background-image: url($url + "@3x.png")

// 不換行
no-wrap()
  text-overflow: ellipsis
  overflow: hidden
  white-space: nowrap

// 擴展點擊區域
extend-click()
  position: relative
  &:before
    content: ''
    position: absolute
    top: -10px
    left: -10px
    right: -10px
    bottom: -10px

配置路徑別名

resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      '@': resolve('src'),
      'common': resolve('src/common')
    }
}

移動端300毫秒延時和點透問題

fastclick:處理移動端click事件300毫秒延遲和點透問題。git

先執行安裝fastclick的命令。github

npm install fastclick --save

以後,在main.js中引入,並綁定到bodyweb

import FastClick from 'fastclick';

FastClick.attach(document.body);

注意: fastclick和其餘的模塊點擊衝突,致使點擊事件不可用時,能夠給對應的dom添加needsclick類來解決。vuex

對jsonp進一步封裝

下載原始的jsonp模塊:vue-cli

npm install jsonp --save

再次封裝:npm

import originJSONP from 'jsonp';

/**
 * 作一個簡單的jsonp封裝
 * @param url
 * @param data
 * @param option
 * @return {Promise}
 */
export default function jsonp (url, data, option) {
  return new Promise((resolve, reject) => {
    url = `${url}?${_obj2String(data)}`;
    originJSONP(url, option, (err, data) => {
      if (!err) {
        resolve(data);
      } else {
        reject(err);
      }
    });
  });
};

function _obj2String (obj, arr = [], index = 0) {
  for (let item in obj) {
    arr[index++] = [item, obj[item]];
  }
  return new URLSearchParams(arr).toString();
}

vue的生命週期函數

注意: 當使用keep-alive組件時,當切換到其餘路由,會調用前組件的deactivated鉤子函數,當切回來時,會調用activated函數。json

better-scroll組件的使用

注意:

  • 1.better-scroll只處理容器的第一個子元素的滾動。
  • 2.必定得保證子元素超出父元素,這樣才能正確的滾動。

初始化:

import BScroll from 'better-scroll';

let wrapper = document.querySelector('.wrapper');
let scroll = new BScroll(wrapper,{
    // 配置項
});
.wrapper
    position: fixed
    width: 100%
    top: 88px
    bottom: 0
    .scroll
        height: 100%
        overflow: hidden

問題排查(沒法滾動緣由:)

  • 1.內層容器的高度沒有超過外層容器。
  • 2.dom沒有渲染完畢就初始化better-scroll
  • 3.改變了dom的顯隱性,沒有對scroll進行從新計算。
  • 針對3:當dom顯示出來以後,加20毫秒延時,而後調用refresh方法。

開發模式下的請求代理

當在開發模式下,須要使用一些後臺接口,爲了防止跨域問題,vue-cli提供了很是強大的http-proxy-middleware包。能夠對咱們的請求進行代理。
進入 config/index.js 代碼下以下配置便可:

proxyTable: {
  '/getDescList': {
    target: 'http://127.0.0.1:7070/desclist', // 後端接口地址
    changeOrigin: true,
    // secure: false,
    pathRewrite: {
      '^/getDescList': '/'
    }
  }
}

負外邊距的做用效果

  • marin-left或者margin-top是負值:它會將元素在相應的方向進行移動。left就是左右方向移動,top就是上下方向移動。也就是會使元素在文檔流裏的位置發生變化
  • margin-right或者margin-bottom是負值:它不會移動該元素(該元素不變化),但會使該元素後面的元素往前移動。也就是說,若是margin-bottom爲負值,那麼該元素下面的元素會往上移動;若是margin-right爲負值,那麼該元素右邊的元素會往左移動,從而覆蓋該元素。

配置子路由

需求:在歌手頁面下須要一個歌手詳情頁。

export default new Router({
    routes:[
        {
            path: '/',
            component: Singer,
            children: [
                {
                    path: ':id',
                    compoonent: SingerDetail
                }
            ]
        },
        ...
    ]
});

當監聽到用戶點擊以後進行路由跳轉:

this.$router.push({
  path: `singer/${singer.id}`
});

// 別忘了在`Singer`頁面中:
<router-view></router-view>

Vuex的使用

Vuex是什麼?

簡單來講:Vuex解決項目中多個組件之間的數據通訊和狀態管理。

Vuex將狀態管理單獨拎出來,應用統一的方式進行處理,採用單向數據流的方式來管理數據。用處負責觸發動做(Action)進而改變對應狀態(State),從而反映到視圖(View)上。

clipboard.png

Vuex怎麼用?

安裝:

npm install vuex --save

引入:

import Vuex from 'vuex';
import Vue from 'Vue';

Vue.use(Vuex);

Vuex的組成部分

使用Vuex開發的應用結構應該是這樣的:

clipboard.png

  • State

State負責存儲整個應用的狀態數據,通常須要在使用的時候在根節點注入store對象,後期就可使用this.$store.state直接獲取狀態。

import store from './store';
..

new Vue({
    el: '#app',
    store,
    render: h => h(App)
});

那麼這個store又是什麼?從哪來的呢?

store能夠理解爲一個容器,包含應用中的state。實例化生成store的過程是:

const mutations = {...};
const actions = {...};
const state = {...};

// 實例化store對象並導出
export defautl new Vuex.Store({
    state,
    actions,
    mutations
});
  • Mutations

中文意思是「變化」,利用它能夠來更改狀態,本質上就是用來處理數據的函數。
store.commit(mutationName)是用來觸發一個mutation的方法。
須要記住的是,定義的mutation必須是同步函數。

const mutations = {
    changState(state) {
        // 在這裏改變state中的數據
    }
};

// 能夠在組件中這樣觸發
this.$store.commit('changeState');
  • Actions

Actions也能夠用於改變狀態,不過是經過觸發mutation實現的,重要的是能夠包含異步操做。

直接觸發可使用this.$store.dispatch(actionName)方法。

簡單的多組件數據交互

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

// 狀態
const state = {
  singer: {}
};

// 跟蹤狀態的變化
const mutations = {
  setSinger (state, singer) {
    state.singer = singer;
  }
};

// 實例化store對象
export default new Vuex.Store({
  state,
  mutations
});

// 在singer組件中提交數據
this.$store.commit('setSinger',singer);

// 在singer-detail組件中接收數據
let singer = this.$store.state.singer;

vuex稍微複雜點的使用

在上面的小栗子中,咱們把satemutations等其餘一些內容寫在了一塊兒,
可是這種方式不適合大型點的項目。最好能將這些內容拎出來,單獨做爲一個文件來使用。

在src/store目錄中新建如下文件:

  • state.js 用於存儲狀態信息
const sate = {
    singer: {}    
};

export default state;
  • mutation-types.js 保存一些常量(mutations中函數的函數名)
export const SET_SINGER = 'SET_SINGER';
  • mutations.js 用於更改狀態(state中的數據)
import * as types from './mutation-types';

// 經過這個函數能夠傳入payload信息
const mutations = {
    [types.SET_SINGER](state,singer){
        state.singer = singer;
    }
};

export default mutations;
  • getters.js 對狀態獲取的封裝
export const singer = state => state.singer;
  • actions.js 對mutation進行封裝,或者執行一些異步操做
// 暫時沒有什麼異步操做
  • index.js store的入口文件
// 入口文件
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
import mutations from './mutations';
import * as actions from './actions';
import * as getters from './getters';
import createLogger from 'vuex/dist/logger';

Vue.use(Vuex);

// 調試環境下開啓嚴格模式
const debug = process.env.NODE_ENV !== 'production';

// 建立store對象並導出
export default new Vuex.Store({
  state,
  actions,
  getters,
  mutations,
  strict: debug,
  plugins: debug ? [createLogger()] : []
});

使用:

// main.js中引入
import store from './store';

有了以上內容,那麼咱們就能夠在業務中去使用了:

例如:多組件之間的的數據交互。
需求:singer組件中須要將用戶點擊的那個singer對象傳遞給組件singer-detail組件。

singer.vue 組件中:

// 使用這個語法糖
import { mapMutations } from 'vuex';

methods:{
    ...mapMutations({
        // 將這個函數(setSinger)和mutations中用於修改狀態的函數關聯起來
        setSinger: 'SET_SINGER'
    });
}

// 傳參
this.setSinger(singer);

// 語法糖的本質
  this.$store.commit('setSinger', singer);

singer-detail.vue 組件中:
咱們就能夠去使用這個數據了,固然也是使用咱們的語法糖啦。

import { mapGetters } from 'vuex';

export default {
    // 使用一個計算屬性
    computed: {
        ...mapGetters([
            'singer'   // 這個就是getters.js中的那個singer
        ]);
    },
    created(){
        console.log(this.singer);
    }    
}

// 語法糖的本質:
let singer = this.$store.state.singer;

js中給CSS添加prefix

咱們必定遇到過這種狀況:
須要用JS寫CSS動畫。但咱們又不得不處理前綴的問題。

因此通常是這樣寫的:

this.$refs.image.style.transform = `scale(${scale})`;
this.$refs.image.style.webkitTansform = `scale(${scale})`;
...

那麼問題來了,怎樣用JS處理這種狀況呢?

思路:

  • 檢測瀏覽器的能力。
  • 返回帶着前綴的CSS樣式。

代碼實現:

let elementStyle = document.createElement('div').style;

// 獲得合適的瀏覽器前綴
let vendor = (() => {
  let transformNames = {
    webkit: 'webkitTransform',
    Moz: 'MozTransform',
    O: 'OTransform',
    ms: 'msTransform',
    standard: 'transform'
  };

  for (let key in transformNames) {
    let support = elementStyle[transformNames[key]] !== undefined;
    if (support) {
      return key;
    }
  }
  return false;
})();

// 對外暴露的方法
export function prefixStyle (style) {
  if (vendor === false) {
    return style;
  }
  if (vendor === 'standard') {
    return style;
  }
  let result = vendor + style.charAt(0).toUpperCase() + style.substr(1);
  return result;
}

使用案例:

// 導入該模塊
import { prefixStyle } from 'common/js/dom';

// 加了合適前綴的CSS屬性
const TRANSFORM = prefixStyle('transform');

// 使用該CSS屬性
this.$refs.image.style[TRANSFORM] = `scale(${scale})`;

移動端的touch事件

隨着觸屏設備的普及,w3c爲移動端web新增了touch事件。

最基本的touch事件包括4個事件:

  • touchstart 當在屏幕上按下手指時觸發

當用戶手指觸摸到的觸摸屏的時候觸發。事件對象的 target 就是 touch 發生位置的那個元素。

  • touchmove 當在屏幕上移動手指時觸發

即便手指移出了 原來的target元素,但 touchmove 仍然會被一直觸發,並且 target 仍然是原來的 target 元素。

  • touchend 當在屏幕上擡起手指時觸發

當用戶的手指擡起的時候,會觸發 touchend 事件。若是用戶的手指從觸屏設備的邊緣移出了觸屏設備,也會觸發 touchend 事件。

touchend 事件的 target 也是與 touchstarttarget 一致,即便已經移出了元素。

  • touchcancel 當一些更高級別的事件發生的時候(如電話接入或者彈出信息)會取消當前的touch操做,即觸發touchcancel。通常會在touchcancel時暫停遊戲、存檔等操做。

若是你使用了觸摸事件,能夠調用 event.preventDefault()來阻止鼠標事件被觸發。

與移動端相關的interface主要有三個:

  • TouchEvent 表示觸摸狀態發生改變時觸發的event

能夠經過檢查觸摸事件的 TouchEvent.type 屬性來肯定當前事件屬於哪一種類型。

dom.addEventListener('touchstart',(e) => {
    // 獲取事件類型
    let type = e.type;
    // toch事件發生時那個位置的元素對象
    let target = e.target;    
    
});
  • Touch 表示用戶和觸屏設備之間接觸時單獨的交互點(a single point of contact)

screenXscreenY:觸點相對於屏幕左邊緣或上邊緣的x、y座標。
clientXclientY:觸點相對於瀏覽器viewport左邊緣或上邊緣的x、y座標。(不包含滾動距離)

pageXpageY:觸點相對於document的左邊緣或上邊緣的x、y座標。與client不一樣的是,包含左邊滾動的距離。

target:觸摸開始時的element。

// 獲取touchList
let touchList = e.changedTouches;
// 獲取第i個touch對象
let touch = touchList[i];

touch.screenX
touch.clientX
touch.pageX
touch.target
...
  • TouchList 表示一組touches。當發生多點觸摸的時候才用的到。

若是一個用戶用三根手指接觸屏幕(或者觸控板), 與之相關的TouchList對於每根手指都會生成一個 Touch 對象, 共計 3 個.
能夠經過三種方式獲取這個對象:

dom.addEventListener('touchstart',(e) => {
    // 這個 TouchList對象列出了和這個觸摸事件對應的那些發生了變化的 Touch 對象
    e.changedTouches
    // 這個TouchList列出了那些 touchstart發生在這個元素,而且尚未離開 touch surface 的touch point(手指)
    e.targetTouches
    // 這個 TouchList 列出了事件觸發時: touch suface上全部的 touch point。
    e.touches
});

播放器內核開發

audio標籤

對於音樂的播放,咱們使用了audio標籤,監聽它的事件和操做DOM,能夠達到對音樂播放、
暫停、進度控制等操做。

<audio ref="audio" :src="currentSongUrl"
    @canplay="songCanPlay"
    @error="songError"
    @ended="songEnd"
    @timeupdate="updateTime">
</audio>

audio進行操做

let audio = this.$refs.audio;
// 暫停和播放
audio.pause();
audio.play();

// Audio對象的屬性(部分)

audio.currentTime // 設置或返回音頻中的當前播放位置(以秒計)。

audio.duration    // 返回音頻的長度(以秒計)。

audio.loop    // 設置或返回音頻是否應在結束時再次播放。(默認false)

audio.volume    // 設置或返回音頻的音量。[0,1]

// Audio對象多媒體事件(Media Events)

onerror // 加載發生錯誤時的回調

ontimeupdate // 當播放位置改變時調用
updateTime(e) {
    if(this.currentSongReady){
        // 獲取當前播放的進度
        this.currentSongTime=e.traget.currentTime;
    }
}
oncanplay // 可以播放時調用

// 經過監聽這個事件,設置標誌位,這個標誌位能夠幫助咱們
// 防止用戶快速切換歌曲引發一些錯誤。
songCanPlay(){
    this.currentSongReady = true;
}


onended // 到達結尾時調用

onplay、onpause...

進度條組件

1.progress-bar.vue接收一個percent參數,用來顯示當前播放的一個進度。

2.對於進度條用戶手動拖動進度的實現。

<div class="progress-btn" ref="btn"
     @touchstart="touchStart"
     @touchmove="touchMove"
     @touchend="touchEnd">
</div>

思路:主要是經過監聽ontouchstartontouchmoveontouchend事件來完成。

// 首先得定義一個`touch`對象
let touch = {};

// 在監聽的方法中
touchStart(e){
    this.touch.initialized = true;
    // 獲取touch的起始位置
    this.touch.startX = e.touches[0].pageX;
    // 獲取整個進度條的寬度
    this.touch.barW = xxx;
    // 獲取已經播放的進度
    this.touch.offset = xxx;    
}

touchMove(e){
    // 判斷有無初始化
    ...
    // 獲取用戶滑動的距離
    let deltaX = e.touches[0].pageX - this.touch.startX;
    let barW = xxx; // 進度條的寬度 - 拖動btn的寬度
    let offset = Math.min(Math.max(0, this.touch.offset + detail), barW);
    
    // 最後設置btn的位置和progress的進度就OK
    ...
}

touchEnd(){
    this.touch.initialized = false;
    // 而後將進度推送出去就行了
    this.$emit('percentChange',percent);
}

svg實現圓形進度條

<template>
  <div class="progress-circle">
    <svg :width="radius" :height="radius" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
      <circle class="progress-background" r="50" cx="50" cy="50" fill="transparent"/>
      <circle class="progress-bar" r="50" cx="50" cy="50" fill="transparent"
              :stroke-dasharray="dashArray"
              :stroke-dashoffset="offset"/>
    </svg>
    <slot></slot>
  </div>
</template>

經過svg能夠實現各類進度條,有一個問題,怎樣去動態的修改它的進度值呢?

這就不能不提 SVG Stroke 屬性

  • stroke 定義一條線,文本或元素輪廓顏色
  • stroke-width 文本或元素輪廓的厚度
  • stroke-dasharray 該屬性可用於建立虛線
  • stroke-dashoffset 設置虛線邊框的偏移量

OK,知道了以上屬性,就足以實現一個可設置進度的SVG進度條了。

思路:stroke-dasharray適用於建立虛線的,若是這個虛線長度爲整個輪廓的周長呢。
stroke-dashoffset能夠設置虛線的偏移量,利用這兩個屬性,咱們就能夠完成對進度的控制。

且看一個小栗子:
圖片描述

因此,經過父組件傳入的percent,不斷地修改stroke-dashoffset就能達到進度的顯示了。

全屏和退出全屏

// 全屏顯示
document.documentElement.webkitRequestFullScreen();
// 退出全屏
document.webkitExitFullscreen();

// 1.得根據不一樣的瀏覽器添加前綴
// 2.程序主動調用無論用,得用戶操做才能夠(點擊按鈕)

歌詞頁的顯示

經過網絡接口獲取的歌詞:
圖片描述

對於歌詞的解析,播放是經過一個插件lyric-parser完成的。

這個插件很簡單:
1.經過正則把時間和對應的歌詞切分出來建立成對象。
2.當調用play方法時,經過定時器完成歌詞的播放,並將對應的行號和歌詞經過回調函數傳遞出去。

當播放的歌詞超過5行時,就可使用封裝的scroll組件完成滾動操做。

if (lineNum > 5) {
  let elements = this.$refs.lyricLine;
  this.$refs.lyricScroll.scrollToElement(elements[lineNum - 5], 1000);
} else {
  this.$refs.lyricScroll.scrollTo(0, 0, 1000);
}

Vue中的mixin

爲何要使用mixin?

多個組件公用同樣的代碼,咱們能夠將這部分抽離出來做爲mixin,只要引入對應的組件中就能夠了。

例以下面的mixin

import { mapGetters } from 'vuex';

export const playListMixin = {

  mounted () {
    this.handlePlayList(this.playList);
  },
  // 當路由對應的頁面激活時調用
  activated () {
    this.handlePlayList(this.playList);
  },
  watch: {
    playList (newPlayList) {
      this.handlePlayList(newPlayList);
    }
  },
  computed: {
    ...mapGetters([
      'playList'
    ])
  },
  methods: {
      // 這個方法須要對應的組件本身去實現,直接調用拋出錯誤
    handlePlayList () {
      throw new Error('Components must implement handlePlayList method.');
    }
  }
};

有了mixin咱們在組件中就能夠這樣使用了:

import { playListMixin } from 'common/js/mixin';

export default{
    mixins: [playListMixin],
    ...
}

節流處理

在搜索頁面,咱們須要處理用戶的輸入,而後向服務器發起請求。
爲了避免必要的請求、節省流量和提升頁面性能,咱們都有必要作節流處理。

在搜索框search-box這個基礎組件中:

// 在created鉤子中,咱們監聽用戶輸入字符串(query)變化,而後將變化後的字符串
// 提交給父組件

// 能夠看到在回調函數中,又包了一層debounce函數

created () {
  this.$watch('query', debounce(() => {
    this.$emit('queryChange', this.query);
  }, 500));
}

因此debounce函數,就是咱們的節流函數,這個函數,接收一個函數,返回一個新的函數

function debounce(func,delay){
    let timer = null;
    return function(...args){
        if(timer){
            clearTimeout(timer);
        }
        timer = setTimeout(()=>{
            func.apply(this,args);
        },delay)        
    }
}

// 測試
function show(){
    console.log('hello...');
}

var func = debounce(show,3000);

// 調用
func(); 

// 連續調用時,沒有超過三秒是不會有任何輸出的

animation動畫

語法:

animation: name duration timing-function delay iteration-count direction fill-mode play-state;
animation: 動畫名稱 執行時間 速度曲線 延時時間 執行次數 動畫播放順序 結束時應用的樣式 播放的狀態(paused|running)

封裝localStorage操做

const __VERSION__ = '1.0.1';
const store = {
  version: __VERSION__,
  storage: window.localStorage,
  session: {
    storage: window.sessionStorage
  }
};

// 操做store的api
const api = {
  set (key, val) {
    if (this.disabled) {
      return false;
    }
    if (val === undefined) {
      return this.remove(key);
    }
    this.storage.setItem(key, this.serialize(val));
    return val;
  },
  get (key, val) {
    if (this.disabled) {
      return false;
    }
    let result = this.storage.getItem(key);
    if (!result) {
      return val;
    }
    return this.deSerialize(result);
  },
  getAll () {
    if (this.disabled) {
      return false;
    }
    let ret = {};
    for (let key in this.storage) {
      if (this.storage.hasOwnProperty(key)) {
        ret[key] = this.get(key);
      }
    }
    return ret;
  },
  remove (key) {
    if (this.disabled) {
      return false;
    }
    this.storage.removeItem(key);
  },
  removeAll () {
    if (this.disabled) {
      return false;
    }
    this.storage.clear();
  },
  forEach (cb) {
    if (this.disabled) {
      return false;
    }
    for (let key in this.storage) {
      if (this.storage.hasOwnProperty(key)) {
        cb && cb(key, this.get(key));
      }
    }
  },
  has (key) {
    if (this.disabled) {
      return false;
    }
    return key === this.get(key);
  },
  serialize (val) {
    try {
      return JSON.stringify(val) || undefined;
    } catch (e) {
      return undefined;
    }
  },
  deSerialize (val) {
    if (typeof val !== 'string') {
      return undefined;
    }
    try {
      return JSON.parse(val) || undefined;
    } catch (e) {
      return undefined;
    }
  }
};

// 擴展store對象
Object.assign(store, api);
Object.assign(store.session, api);

// 瀏覽器能力檢測
try {
  let testKey = 'test_key';
  store.set(testKey, testKey);
  if (store.get(testKey) !== testKey) {
    store.disabled = true;
  }
  store.remove(testKey);
} catch (e) {
  store.disabled = true;
}

export default store;

路由懶加載

爲何須要?

若是開發的App太大的話,就會致使首屏渲染過慢,爲了加強用戶體驗,加快渲染速度,
須要用到懶加載功能。讓首屏的內容先加載出來,其餘路由下的組件按需加載。

vue官網描述:

異步組件
在大型應用中,咱們可能須要將應用分割成小一些的代碼塊,而且只在須要的時候才從服務器加載一個模塊。
爲了簡化,Vue 容許你以一個工廠函數的方式定義你的組件,這個工廠函數會異步解析你的組件定義。
Vue 只有在這個組件須要被渲染的時候纔會觸發該工廠函數,且會把結果緩存起來供將來重渲染。
const AsyncComponent = () => ({
  // 須要加載的組件 (應該是一個 `Promise` 對象)
  component: import('./MyComponent.vue'),
  // 異步組件加載時使用的組件
  loading: LoadingComponent,
  // 加載失敗時使用的組件
  error: ErrorComponent,
  // 展現加載時組件的延時時間。默認值是 200 (毫秒)
  delay: 200,
  // 若是提供了超時時間且組件加載也超時了,
  // 則使用加載失敗時使用的組件。默認值是:`Infinity`
  timeout: 3000
})

注意:若是你但願在 Vue Router 的路由組件中使用上述語法的話,你必須使用 Vue Router 2.4.0+ 版本。

固然爲了簡單起見:

router/index.js路由配置文件中這樣加載組件:

// import Recommend from '@/components/recommend/recommend';
const Recommend = () => ({
  component: import('@/components/recommend/recommend')
});
相關文章
相關標籤/搜索