Vue折腾记|Vue 2.x折腾记 - (13) Nuxt.js写一个常规音频的播放组件,动态注入微信,新浪微博的js-sdk

前言 【Vue折腾记|Vue 2.x折腾记 - (13) Nuxt.js写一个常规音频的播放组件,动态注入微信,新浪微博的js-sdk】只是一个常规的播放组件,需要考虑微信,微博这类环境的播放
微信和微博,若没有用其官方的js-sdk初始化,没法播放。
我的文章从来都不推崇copy,仅供参考学习…具体业务具体分析定制才是最合理的
前置基础

  • vue && vuex
  • ES5+
  • Nuxt的基本用法
这篇文章的内容需基于上篇内容的,要用到一些设备信息
效果图 这是当前服务端版本的效果,因为还没上线,LOGO已经马赛克
Vue折腾记|Vue 2.x折腾记 - (13) Nuxt.js写一个常规音频的播放组件,动态注入微信,新浪微博的js-sdk
文章图片

实现思路 之前老的客户端实现思路
  • 在主入口实现一个单例,绑定到vue.prototype
  • 在音频组件的beforeMount创建script标签,引入对应js,然后用promise拿到成功加入head的状态
  • vuex来维护播放状态
  • 在对应的函数初始化音频的加载,之后就可以正常使用了
服务端的思路也差不多
考虑的东西多些,在之前客户端实现的基础上加以完善
用中间件这些来动态注入js-sdk
代码实现 客户端渲染实现的版本
版本1 全部耦合到组件内,虽然可以正常播放(包括微信和微博)
且不是单例模式,对于多音频页面,有毒
> 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 = https://www.it610.com/article/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(); } } }; 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); } }

版本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; } } };

播放组件组件
> 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 = https://www.it610.com/article/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 = https://www.it610.com/article/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); } } }, }; 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; } }

服务端渲染实现的版本(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.jsimport 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.jsimport 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 = https://www.it610.com/article/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来管理,这样对于多音频或者跨组件控制播放非常有帮助
> 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}`; }, } } ="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; }

总结 有不对之处请留言,会及时修正,谢谢阅读。

    推荐阅读