Vue 2.x折騰記 - (13) Nuxt.js寫一個常規音頻的播放組件,動態注入微信,新浪微博的js-sdk

前言

只是一個常規的播放組件,須要考慮微信,微博這類環境的播放javascript

微信和微博,若沒有用其官方的js-sdk初始化,無法播放。css

個人文章歷來都不推崇copy,僅供參考學習..具體業務具體分析定製纔是最合理的html

前置基礎

  • vue && vuex
  • ES5+
  • Nuxt的基本用法

這篇文章的內容需基於上篇內容的,要用到一些設備信息vue

效果圖

這是當前服務端版本的效果,由於還沒上線,LOGO已經馬賽克java

實現思路

以前老的客戶端實現思路web

  • 在主入口實現一個單例,綁定到vue.prototype
  • 在音頻組件的beforeMount建立script標籤,引入對應js,而後用promise拿到成功加入head的狀態
  • vuex來維護播放狀態
  • 在對應的函數初始化音頻的加載,以後就能夠正常使用了

服務端的思路也差很少vuex

考慮的東西多些,在以前客戶端實現的基礎上加以完善api

用中間件這些來動態注入js-sdkpromise

代碼實現

客戶端渲染實現的版本

版本1

所有耦合到組件內,雖然能夠正常播放(包括微信和微博) 且不是單例模式,對於多音頻頁面,有毒緩存

<template>
  <div class="play-voice-area">
    <div class="cover-player">
      <div :class="playState?'active':''" class="cover-pic">
        <img :src="coverUrl ? coverUrl:defaultAvatar">
        <i :class="playState? 'sx-mobile-icon-':'sx-mobile-bofang'" class="sx-mobile cover-icon" @click="playAudio" />
      </div>
    </div>
    <div class="sound-desrc">
      <p class="username">{{ userName }}的聲兮</p>
      <p class="timeline">{{ currentPlayTime }}/{{ voiceTime }}</p>
    </div>
  </div>
</template>

<script> export default { props: { userName: { type: String, default: 'Super Hero' }, duration: { type: [String, Number], default: '' }, autoplay: { type: [Boolean, String], default: false }, sourceUrl: { type: String, default: '' }, coverpic: { type: String, default: '' } }, data() { return { defaultAvatar: require('@/assets/share/yourAppIcon@2x.png'), // 默認頭像 audioElm: '', // 音頻播放器 DOM soundCurrentStopTime: '', // 當前聲音暫停的時間戳 playState: false, // 播放狀態的圖標控制 timeStepState: '', // 時間迭代 voicePlayMessage: '', // 音頻資源的情況 currentPlayTime: '00:00', // 當前播放的時間,默認爲0 cacheCurrentTime: 0 // 緩存播放時間 }; }, computed: { coverUrl() { if (!this.coverpic) { return this.defaultAvatar; } return this.coverpic; }, voiceTime() { if (this.duration) { return this.second2time(Number(this.duration)); } } }, watch: { sourceUrl(newVal, oldVal) { if (newVal) { this.playAudio(); } } }, created() { this.$store.commit('OPEN_LOADING'); }, beforeMount() { // 初始化音頻播放器 this.initAudioElm(); }, mounted() { // 檢測微博微信平臺 this.checkWeiBo_WeiChat(); this.audioElm.addEventListener('stalled', this.stalled); this.audioElm.addEventListener('loadstart', this.loadstart); this.audioElm.addEventListener('loadeddata', this.loadeddata); this.audioElm.addEventListener('canplay', this.canplay); this.audioElm.addEventListener('ended', this.ended); this.audioElm.addEventListener('pause', this.pause); this.audioElm.addEventListener('timeupdate', this.timeupdate); this.audioElm.addEventListener('error', this.error); this.audioElm.addEventListener('abort', this.abort); }, beforeDestroy() { this.audioElm.removeEventListener('loadstart', this.loadstart); this.audioElm.removeEventListener('stalled', this.stalled); this.audioElm.removeEventListener('canplay', this.canplay); this.audioElm.removeEventListener('timeupdate', this.timeupdate); this.audioElm.removeEventListener('pause', this.pause); this.audioElm.removeEventListener('error', this.error); this.audioElm.removeEventListener('ended', this.ended); }, methods: { initAudioElm() { let audio = new Audio(); audio.autobuffer = true; // 自動緩存 audio.preload = 'metadata'; audio.src = this.sourceUrl; audio.load(); this.audioElm = audio; }, checkWeiBo_WeiChat() { let ua = navigator.userAgent.toLowerCase(); // 獲取判斷用的對象 const script = document.createElement('script'); if (/micromessenger/.test(ua)) { // 返回一個獨立的promise script.src = 'https://res.wx.qq.com/open/js/jweixin-1.2.0.js'; new Promise((resolve, reject) => { let done = false; script.onload = script.onreadystatechange = () => { if ( !done && (!script.readyState || script.readyState === 'loaded' || script.readyState === 'complete') ) { done = true; // 避免內存泄漏 script.onload = script.onreadystatechange = null; resolve(script); } }; script.onerror = reject; document .getElementsByTagName('head')[0] .appendChild(script); }).then(res => { this.initWeixinSource(); }); } if (/WeiBo|weibo/i.test(ua)) { script.src = 'https://tjs.sjs.sinajs.cn/open/thirdpart/js/jsapi/mobile.js'; new Promise((resolve, reject) => { let done = false; script.onload = script.onreadystatechange = () => { if ( !done && (!script.readyState || script.readyState === 'loaded' || script.readyState === 'complete') ) { done = true; // 避免內存泄漏 script.onload = script.onreadystatechange = null; resolve(script); } }; script.onerror = reject; document .getElementsByTagName('head')[0] .appendChild(script); }).then(res => { this.initWeiboSource(); }); } }, canplay() { this.$store.commit('CLOSE_LOADING'); }, initWeixinSource() { wx.config({ // 配置信息, 即便不正確也能使用 wx.ready debug: false, appId: '', timestamp: 1, nonceStr: '', signature: '', jsApiList: [] }); wx.ready(() => { let st = setTimeout(() => { clearTimeout(st); this.audioElm.load(); }, 50); }); }, initWeiboSource() { window.WeiboJS.init( { appkey: '3779229073', debug: false, timestamp: 1429258653, noncestr: '8505b6ef40', scope: [ 'getNetworkType', 'networkTypeChanged', 'getBrowserInfo', 'checkAvailability', 'setBrowserTitle', 'openMenu', 'setMenuItems', 'menuItemSelected', 'setSharingContent', 'openImage', 'scanQRCode', 'pickImage', 'getLocation', 'pickContact', 'apiFromTheFuture' ] }, ret => { this.audioElm.load(); } ); }, playAudio() { // 播放暫停音頻 if (this.audioElm.readyState > 2) { // 當資源能夠播放的時候 if (this.audioElm.paused) { this.cacheCurrentTime === 0 ? (this.audioElm.currentTime = 0) : (this.audioElm.currentTime = this.cacheCurrentTime); this.playState = true; this.audioElm.play(); } else { this.audioElm.pause(); } } }, second2time(currentTime) { // 秒數化爲分鐘 let min = Math.floor(currentTime / 60); // 向下取整分鐘 let second = Math.floor(currentTime % 60); // 取模獲得剩餘秒數 if (min < 10) { min = '0' + min; } if (second < 10) { second = '0' + second; } return `${min}:${second}`; }, stalled() { // 資源須要緩存的時候暫停 this.audioElm.pause(); // 緩存加載待播的時候,如果當前播放時間已經走動則觸發播放 if (this.audioElm.currentTime !== 0) { // 判斷當前播放的時間是否到達結束,不然則繼續播放 if (this.audioElm.currentTime !== this.audioElm.duration) { this.playAudio(); } else { this.ended(); } } }, timeupdate() { if ( this.audioElm.readyState > 2 && this.audioElm.currentTime > 0.2 ) { this.cacheCurrentTime = this.audioElm.currentTime; this.currentPlayTime = this.second2time( Number(this.audioElm.currentTime) ); if ( this.audioElm.ended || this.audioElm.currentTime === this.audioElm.duration ) { this.ended(); } } }, ended() { this.audioElm.pause(); // 清除緩存的時間 this.cacheCurrentTime = 0; this.voicePlayMessage = ''; }, pause() { // 當音頻/視頻已暫停時 this.playState = false; }, error(err) { // 當在音頻/視頻加載期間發生錯誤時 this.audioElm.pause(); this.voicePlayMessage = '音頻加載資源錯誤!'; console.log('我報錯了:' + err); }, abort() { this.audioElm.pause(); } } }; </script>

<style lang="scss" scoped> .play-voice-area { display: flex; align-items: center; flex-direction: column; justify-content: center; } .cover-player { position: relative; display: flex; align-items: center; flex-direction: column; flex-shrink: 0; justify-content: center; .cover-pic { display: block; overflow: hidden; width: 446px; height: 446px; transition: animation 0.28s; border: 15px solid hsla(0, 0%, 100%, 0.1); border-radius: 223px; img { display: inline-block; width: 446px; height: 446px; } &.active { animation: rotation 8s 0.1s linear infinite; } } .cover-icon { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #fff; font-size: 100px; } a, button, input, textarea { -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } } .sound-desrc { display: flex; overflow: hidden; align-items: center; flex-direction: column; justify-content: center; padding: 40px 0 0 0; .username { min-width: 243px; height: 38px; margin: 22px 0; text-align: center; letter-spacing: 0px; text-overflow: ellipsis; color: #c4c9e2; font-size: 36px; font-weight: normal; font-weight: 700; font-stretch: normal; line-height: 38px; } .timeline { width: 243px; height: 38px; text-align: center; color: #c4c9e2; font-size: 36px; font-weight: normal; font-stretch: normal; line-height: 38px; line-height: 38px; } } @keyframes rotation { from { -webkit-transform: rotate(0deg); } to { -webkit-transform: rotate(360deg); } } </style>

複製代碼

版本2

這個版本考慮了多音頻播放,因此在主入口直接單例掛載了一個播放器

其次考慮音頻的切換播放,因此必須依賴Vuex來共享狀態

main.js-主入口
// 建立全局播放器
const music = new Audio();
Vue.prototype.player = music;

複製代碼
  • 狀態

狀態很簡單,就一些基礎信息,module的方式,state經過getters暴露

export default {
    state: {
        index: '',
        playState: false,
        curTime: '00:00'
    },
    mutations: {
        CURRENT_PLAY: (state, index) => {
            state.index = index;
        },
        CURRENT_TIME: (state, time) => {
            state.curTime = time;
        },

        SetPlayState(state, status) {
            state.playState = status;
        }
    }
};

複製代碼
播放組件組件
<template>
  <div @click="playstop" class="icon-wrap" :class="iconSize" :style="{color:iconColor}">
    <i class="sx-mobile" :class="playState ? iconShow.stop : iconShow.play" />
  </div>

</template>

<script> export default { props: { iconShow: { type: Object, default: function() { return { play: 'sx-mobile-bofang', stop: 'sx-mobile-icon-' }; } }, iconSize: { type: String, default: 'normal' }, iconColor: { type: String, default: '#FFF' }, playState: { type: Boolean, default: false }, sourceUrl: { type: String, default: '' }, mode: { type: String, default: 'self' } }, created() { // 檢測微博微信平臺 this.checkWeiBo_WeiChat(); console.log(this.sourceUrl); }, mounted() { this.player.addEventListener('end', this.voiceEnd); }, methods: { checkWeiBo_WeiChat() { let ua = navigator.userAgent.toLowerCase(); // 獲取判斷用的對象 const script = document.createElement('script'); if (/micromessenger/.test(ua)) { // 返回一個獨立的promise script.src = 'https://res.wx.qq.com/open/js/jweixin-1.2.0.js'; new Promise((resolve, reject) => { let done = false; script.onload = script.onreadystatechange = () => { if ( !done && (!script.readyState || script.readyState === 'loaded' || script.readyState === 'complete') ) { done = true; // 避免內存泄漏 script.onload = script.onreadystatechange = null; resolve(script); } }; script.onerror = reject; document .getElementsByTagName('head')[0] .appendChild(script); }).then(res => { this.initWeixinSource(); }); } if (/WeiBo|weibo/i.test(ua)) { script.src = 'https://tjs.sjs.sinajs.cn/open/thirdpart/js/jsapi/mobile.js'; new Promise((resolve, reject) => { let done = false; script.onload = script.onreadystatechange = () => { if ( !done && (!script.readyState || script.readyState === 'loaded' || script.readyState === 'complete') ) { done = true; // 避免內存泄漏 script.onload = script.onreadystatechange = null; resolve(script); } }; script.onerror = reject; document .getElementsByTagName('head')[0] .appendChild(script); }).then(res => { this.initWeiboSource(); }); } }, initWeixinSource() { wx.config({ // 配置信息, 即便不正確也能使用 wx.ready debug: false, appId: '', timestamp: 1, nonceStr: '', signature: '', jsApiList: [] }); wx.ready(() => { let st = setTimeout(() => { clearTimeout(st); this.player.load(); }, 50); }); }, initWeiboSource() { window.WeiboJS.init( { appkey: '3779229073', debug: false, timestamp: 1429258653, noncestr: '8505b6ef40', scope: [ 'getNetworkType', 'networkTypeChanged', 'getBrowserInfo', 'checkAvailability', 'setBrowserTitle', 'openMenu', 'setMenuItems', 'menuItemSelected', 'setSharingContent', 'openImage', 'scanQRCode', 'pickImage', 'getLocation', 'pickContact', 'apiFromTheFuture' ] }, ret => { this.player.load(); } ); }, second2time(currentTime) { /* 秒數化爲分鐘 */ let min = parseInt(currentTime / 60, 10); // 向下取整分鐘 let second = parseInt(currentTime % 60, 10); // 取模獲得剩餘秒數 if (min < 10) { min = '0' + min; } if (second < 10) { second = '0' + second; } return `${min}:${second}`; }, playstop() { if (this.mode === 'self') { this.player.paused ? this.playVoice() : this.pauseVoice(); } else { if (this.$store.getters.vindex === this.index) { this.player.paused ? this.playVoice() : this.pauseVoice(); } else { this.player.src = this.sourceUrl; this.player.play(); if (!this.player.paused) { this.$store.commit('SetPlayState', true); this.$store.commit('CURRENT_PLAY', this.index); } } } }, playVoice() { if (this.player.src !== '') { this.player.play(); if (!this.player.paused) { this.$store.commit('SetPlayState', true); this.$store.commit('CURRENT_PLAY', this.index); if (this.mode === 'self') { this.playState = true; } } } else { this.player.src = this.sourceUrl; this.playVoice(); } }, pauseVoice() { this.player.pause(); this.$store.commit('SetPlayState', false); if (this.mode === 'self') { this.playState = false; } }, voiceEnd() { if (this.mode === 'self') { this.$emit('update:playState', false); } } }, }; </script>

<style lang="scss" scoped> .icon-wrap { &.small { font-size: 16px; } &.normal { font-size: 32px; } &.large { font-size: 64px; } &.huge { font-size: 96px; } &.big { font-size: 128px; } i { font-size: inherit; } } </style>

複製代碼

服務端渲染實現的版本(Nuxt)

audio_browser_inject_head.js件(middleware目錄)

// 這裏給標籤加了spec標記,是爲了防止屢次訪問同一個頁面的時候,
// 無限的插入新增的js
// 此次就再也不nuxt.config.js引入中間件了.由於不是面向全局,直接在對應的頁面引入便可
export default context => {
  const { env } = context.deviceType;
  const HeadScript = context.app.head.script;
  if (env === "wechat") {
    if (!HeadScript[HeadScript.length - 1].spec) {
      HeadScript.push({
        src: "https://res.wx.qq.com/open/js/jweixin-1.3.2.js",
        type: "text/javascript",
        charset: "utf-8",
        spec: true,
      });
    }
  }
  if (env === "weibo") {
    if (!HeadScript[HeadScript.length - 1].spec) {
      HeadScript.push({
        src: "http://tjs.sjs.sinajs.cn/open/thirdpart/js/jsapi/mobile.js",
        type: "text/javascript",
        charset: "utf-8",
        spec: true,
      });
    }
  }
};


複製代碼

單例播放器(plugins目錄)

  • plugins/player.js
import Vue from "vue";

export default ({ app, store }) => {
  let player = new Audio();
  player.preload = "auto";
  // 把單例的播放器提交到vuex去管控
  store.commit("voice/SetPlayer", player);
};



複製代碼
  • nuxt.config.js

由於audio對象只有客戶端纔有,因此不能服務端初始化 設置ssr:false就表明在客戶端的時候才注入,默認不寫ssrtrue

module.exports = {
  plugins: [ { src: "~plugins/player.js", ssr: false }]
};


複製代碼

Vuex(store目錄)

  • 默認的index.js是根狀態,其餘再改目錄下的js文件均默認當作vuexmodule
// index.js

import Vuex from "vuex";
export const state = () => ({
  deviceType: {},
});

export const mutations = {
  SetDeviceType(state, payload) {
    state.deviceType = payload;
  },
};

export const getters = {
  deviceType(state) {
    return state.deviceType;
  },
  player(state) {
    return state.voice.player;
  },
  playState(state) {
    return state.voice.playState;
  },
  playUrl(state) {
    return state.voice.playUrl;
  },
  playIndex(state) {
    return state.voice.playIndex;
  },
  playTime(state) {
    return state.voice.playTime;
  },
  voiceTotalTime(state) {
    return state.voice.voiceTotalTime;
  },
};


// voice.js

import Vuex from "Vuex";
export const state = () => ({
  player: "", // 播放器
  playState: false, // 當前播放的狀態
  playUrl: "", // 播放的連接
  playIndex: 0, // 當前播放的索引
  playTime: "00:00", // 當前的播放時間
  voiceTotalTime: "00:00", // 曲目總時長
});

export const mutations = {
  SetPlayer(state, payload) {
    state.player = payload;
  },
  SetPlayState(state, payload) {
    state.playState = payload;
  },
  SetPlayUrl(state, payload) {
    state.playUrl = payload;
    state.player.src = payload;
  },
  SetPlayIndex(state, payload) {
    state.playIndex = payload;
  },
  SetPlayTime(state, payload) {
    state.playTime = payload;
  },
  SetVoiceTotalTime(state, payload) {
    state.voiceTotalTime = payload;
  },
  ResetVoice(state) {
    state.playState = false;
    state.playUrl = "";
    state.playTime = "00:00";
    state.voiceTotalTime = "00:00";
  },
};


複製代碼

播放器組件

  • VoicePlayer.vue

播放狀態均由vuex來管理,這樣對於多音頻或者跨組件控制播放很是有幫助

<template>
  <div class="player"
       :class="$store.getters.playState ? 'animation-roate':''"
       :style="{background:`url(${CoverImg}) center center no-repeat`,backgroundSize: 'cover'}">
    <div class="icon-wrap">
      <img :src="playstate? StopIcon:PlayIcon"
           alt="播放器操做按鈕"
           @click="changePlayState(playstate)">
    </div>
  </div>
</template>

<script>
  const CoverImg = require('./images/cover@2x.png');
  const PlayIcon = require('./images/play@2x.png');
  const StopIcon = require('./images/stop@2x.png');
  export default {
    data() {
      return {
        CoverImg,
        PlayIcon,
        StopIcon,
      }
    },
    props: {
      playstate: {
        type: Boolean,
        default: false
      },
      playurl: {
        type: String,
        default: 'http://www.ytmp3.cn/down/51013.mp3'
      }

    },
    mounted() {

      this.$store.getters.player.addEventListener('loadedmetadata', () => {
        // 緩存播放總時長
        this.$store.commit('voice/SetVoiceTotalTime', this.second2time(this.$store.getters.player.duration));
      })
      this.$store.getters.player.addEventListener('stalled', () => {
        // 重置播放狀態
        this.$store.commit('voice/ResetVoice');
      })
      this.$store.getters.player.addEventListener('abort', () => {
        // 重置播放狀態
        this.$store.commit('voice/ResetVoice');
      })
      this.$store.getters.player.addEventListener('play', () => {
        this.$store.commit('voice/SetPlayState', true);
      })
      this.$store.getters.player.addEventListener('pause', () => {
        this.$store.commit('voice/SetPlayState', false);
      })
      this.$store.getters.player.addEventListener('timeupdate', () => {
        this.$store.commit('voice/SetPlayTime', this.second2time(this.$store.getters.player.currentTime));
      })
      this.$store.getters.player.addEventListener('ended', () => {
        this.$store.commit('voice/ResetVoice');
      })
    },
    beforeDestroy() {
      this.$store.getters.player.removeEventListener('loadedmetadata', () => {
        this.$store.commit('voice/SetVoiceTotalTime', this.second2time(this.$store.getters.player.duration));
      })
      this.$store.getters.player.removeEventListener('stalled', () => {
        this.$store.commit('voice/ResetVoice');
      })
      this.$store.getters.player.removeEventListener('abort', () => {
        this.$store.commit('voice/ResetVoice');
      })
      this.$store.getters.player.removeEventListener('play', () => {
        this.$store.commit('voice/SetPlayState', true);
      })
      this.$store.getters.player.removeEventListener('pause', () => {
        this.$store.commit('voice/SetPlayState', false);
      })
      this.$store.getters.player.removeEventListener('timeupdate', () => {
        this.$store.commit('voice/SetPlayTime', this.second2time(this.$store.getters.player.currentTime));
        console.log(this.$store.getters.player.currentTime)
      })
      this.$store.getters.player.removeEventListener('ended', () => {
        this.$store.commit('voice/ResetVoice');
      })

    },
    methods: {
      changePlayState(playstate) {
        // 設置播放源
        if (!this.$store.getters.playUrl) {
          this.$store.commit('voice/SetPlayUrl', this.playurl)
        }


        // 設置播放狀態
        if (playstate) {
          this.$store.getters.player.pause();
        } else {
          this.$store.getters.player.play();
        }
        playstate = !playstate;
      },
      initWeixinSource() {
        wx.config({
          // 配置信息, 即便不正確也能使用 wx.ready
          debug: false,
          appId: '',
          timestamp: 1,
          nonceStr: '',
          signature: '',
          jsApiList: []
        });
        wx.ready(() => {
          let st = setTimeout(() => {
            clearTimeout(st);
            this.player.load();
          }, 50);
        });
      },
      initWeiboSource() {
        window.WeiboJS.init(
          {
            appkey: '3779229073',
            debug: false,
            timestamp: 1429258653,
            noncestr: '8505b6ef40',
            scope: [
              'getNetworkType',
              'networkTypeChanged',
              'getBrowserInfo',
              'checkAvailability',
              'setBrowserTitle',
              'openMenu',
              'setMenuItems',
              'menuItemSelected',
              'setSharingContent',
              'openImage',
              'scanQRCode',
              'pickImage',
              'getLocation',
              'pickContact',
              'apiFromTheFuture'
            ]
          },
          ret => {
            this.audioElm.load();
          }
        );
      },
      second2time(currentTime) {
        // 秒數化爲分鐘
        let min = Math.floor(currentTime / 60); // 向下取整分鐘
        let second = Math.floor(currentTime % 60); // 取模獲得剩餘秒數
        if (min < 10) {
          min = '0' + min;
        }
        if (second < 10) {
          second = '0' + second;
        }
        return `${min}:${second}`;
      },
    }
  }
</script>

<style lang="scss" scoped>
  .player {
    height: 100%;
    width: 100%;
    border-radius: 100%;
    position: relative;
    .icon-wrap {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      img {
        display: block;
        height: 94px;
        width: 94px;
      }
    }
  }

  @keyframes fade-rotate {
    from {
      opacity: 0.8;
      transform: rotate(0) scale(1);
    }
    to {
      opacity: 1;
      transform: rotate(360deg) scale(1.1);
    }
  }

  .animation-roate {
    transform: translate3d(0, 0, 0);
    animation: fade-rotate 18s ease-in-out infinite alternate;
  }
</style>


複製代碼

總結

有不對之處請留言,會及時修正,謝謝閱讀。

相關文章
相關標籤/搜索