nuxt 中的 eventbus。
写在前面
在nuxt中自定义音频播放组件,遇到了一个问题:当存在多个音频播放器时,一次只允许播放一个音频。Google了大半天,写出来的效果并不好,后面老大出手解决了,觉得他的这个方式很精巧,记录下以备忘。
最终效果
做一个最简单的自定义音频播放组件AudioPlayer,props中只有一个参数:文件的source地址。
效果图:
实现细节
为了实现这个效果,需要将功能拆分下:
1、获取音频文件的时长duration,这个可以通过audio的canplay事件。
2、获取音频的currentTime, 这个可以通过audio的timeupdate事件
3、用变量state来显示audio的三种状态:paused, playing, stopped,并分别定义三种状态下的方法: play(), pause(), stop(),根据用户操作来调用不同的方法。
4、定义numberToStardardMinuteSecond函数,将currentTime,duration处理成时间,这里引入库numbro来做这件事。
5、自定义进度条,添加click事件处理函数seek,通过获取点击位置占整个长度的百分比,换算出此时audio的currentTime,设置audio的currentTime
6、使用eventbus进行组件间的通信,在audio play之前,触发beforeplay事件,同时在AuidoPlay组件中监听beforeplay事件。
分步来:
先布局出音频播放组件的模子:
<template>
<div class="d-flex w-100 align-items-top">
<div class="audio-player mr-2">
<audio :src="src" ref="audio" />
<i v-if="state === 'playing'" class="far fa-pause-circle fa-2x" @click="pause"></i>
<i v-if="state != 'playing'" class="far fa-play-circle fa-2x" @click="play"></i>
</div>
<div class="w-100">
<span class="float-right text-muted small">{{ currentTime }} / {{ mediaDuration }}</span>
<span class="small">点击后开始播放</span>
<div class="progress">
<div class="progress-bar bg-primary" style="transition-duration: 0.2s; transition-timing-function: linear;" role="progressbar" :style="`width: ${currentPercentage}`"></div>
</div>
</div>
</div>
</template>
<script>
import numbro from 'numbro'
export default {
props: [ 'src' ],
data() {
return {
currentTime: 0,
mediaDuration: 0,
state: 'stopped'
}
},
computed: {
currentPercentage() {
if (this.mediaDuration === 0) {
return numbro(this.mediaDuration).format({ output: 'percent', mantissa: 0 })
} else {
return numbro(this.currentTime / this.mediaDuration ).format({ output: 'percent', mantissa: 0 })
}
}
}
}
</script>
Step1: 获取duration
在audio上添加事件监听@canplay,在methods中定义对应的处理函数onCanplay。
<audio :src="src" ref="audio" @canplay="onCanplay" />
.....
onCanplay(event) {
const audio = event.target
this.mediaDuration = parseInt(audio.duration)
}
Step2: 获取currentTime
在audio上添加事件监听@timeupdate,在methods中定义对应的处理函数onTimeupdate. 这里调用的stop函数,会在下一步定义。
<audio :src="src" @canplay="onCanplay" @timeupdate="onTimeupdate" ref="audio" />
.......
onTimeupdate(event) {
const audio = event.target
if (audio.currentTime >= this.mediaDuration) {
this.currentTime = this.mediaDuration
this.stop()
} else {
this.currentTime = audio.currentTime
}
}
Step3: 在methods中,定义play, pause, stop函数,用以处理audio的状态。
play() {
const audio = this.$refs.audio
if (this.state === 'stopped') {
audio.currentTime = 0
}
if (audio.paused) {
audio.play()
}
this.state = 'playing'
},
pause() {
const audio = this.$refs.audio
if (!audio.paused) {
audio.pause()
}
this.state = 'paused'
},
stop() {
const audio = this.$refs.audio
if (!audio.paused) {
audio.pause()
}
this.state = 'stopped'
}
Step4: 定义numberToStardardMinuteSecond函数,将currentTime,duration转化为时间。
引入numbro.
yarn add numbro
在methods中定义numberToStardardMinuteSecond:
numberToStardardMinuteSecond(num) {
const number = parseInt(num)
const time = numbro(number).format({ output: 'time' })
if (time.substr(0, 1) === '0') {
return time.substr(2, time.length - 2)
} else {
return time
}
}
Step5: 在进度条上监听click事件,在methods中定义seek函数来处理click事件。
<div class="progress" @click="seek">
<div class="progress-bar bg-primary" style="transition-duration: 0.2s; transition-timing-function: linear;" role="progressbar" :style="`width: ${currentPercentage}`"></div>
</div>
......
seek(event) {
const $this = $(event.target)
const widthclicked = event.pageX - $this.offset().left
const totalWidth = $this.width()
this.$refs.audio.currentTime = widthclicked / totalWidth * this.mediaDuration
}
为了使progress可以点击,需要在style中给progress设置pointer-events。
.progress {
pointer-events: auto;
}
Step6: 使用eventbus进行组件间的通信,实现一次只播放一个视频。
在audio play之前,$emit一个事件beforeplay, 同时将audio作为参数传出去。
修改play函数,添加事件派发:
play() {
const audio = this.$refs.audio
if (this.state === 'stopped') {
audio.currentTime = 0
}
if (audio.paused) {
this.$nuxt.$emit('beforeplay', audio)
audio.play()
}
this.state = 'playing'
}
任何其他的组件都可以监听这个beforeplay事件,当然也包括AudioPlayer组件。
在AudioPlayer组件添加对beforeplay的监听:
created() {
this.$nuxt.$on('beforeplay', (player) => {
if (player === this.$refs.audio) {
return
}
if (this.state === 'playing') {
this.pause()
}
})
}
这里判断传过来的参数,如果是自身emit了这个事件,就跳过,如果是其他audio,同时自身还在播放,则自身停止播放。
OK,大功告成。
最终的完整代码:
<template>
<div class="d-flex w-100 align-items-top">
<div class="audio-player mr-2">
<audio :src="src" @timeupdate="onTimeupdate" @canplay="onCanplay" ref="audio" />
<i v-if="state === 'playing'" class="far fa-pause-circle fa-2x" @click="pause"></i>
<i v-if="state != 'playing'" class="far fa-play-circle fa-2x" @click="play"></i>
</div>
<div class="w-100">
<span class="float-right text-muted small">{{ numberToStardardMinuteSecond(currentTime) }} / {{ numberToStardardMinuteSecond(mediaDuration) }}</span>
<span class="small">点击后开始播放</span>
<div class="progress" @click="seek">
<div class="progress-bar bg-primary" style="transition-duration: 0.2s; transition-timing-function: linear;" role="progressbar" :style="`width: ${currentPercentage}`"></div>
</div>
</div>
</div>
</template>
<script>
import $ from 'jquery'
import numbro from 'numbro'
export default {
props: [ 'src' ],
data() {
return {
currentTime: 0,
mediaDuration: 0,
state: 'stopped'
}
},
created() {
this.$nuxt.$on('beforeplay', (player) => {
if (player === this.$refs.audio) {
return
}
if (this.state === 'playing') {
this.pause()
}
})
},
computed: {
currentPercentage() {
if (this.mediaDuration === 0) {
return numbro(this.mediaDuration).format({ output: 'percent', mantissa: 0 })
} else {
const progress = this.currentTime / this.mediaDuration
return numbro(progress).format({ output: 'percent', mantissa: 0 })
}
}
},
methods: {
play() {
const audio = this.$refs.audio
if (this.state === 'stopped') {
audio.currentTime = 0
}
if (audio.paused) {
this.$nuxt.$emit('beforeplay', audio)
audio.play()
}
this.state = 'playing'
},
pause() {
const audio = this.$refs.audio
if (!audio.paused) {
audio.pause()
}
this.state = 'paused'
},
stop() {
const audio = this.$refs.audio
if (!audio.paused) {
audio.pause()
}
this.state = 'stopped'
},
onTimeupdate(event) {
const audio = event.target
if (audio.currentTime >= this.mediaDuration) {
this.currentTime = this.mediaDuration
this.stop()
} else {
this.currentTime = audio.currentTime
}
},
seek(event) {
const $this = $(event.target)
const widthclicked = event.pageX - $this.offset().left
const totalWidth = $this.width()
this.$refs.audio.currentTime = widthclicked / totalWidth * this.mediaDuration
},
onCanplay(event) {
const audio = event.target
this.mediaDuration = parseInt(audio.duration)
},
numberToStardardMinuteSecond(num) {
const number = parseInt(num)
const time = numbro(number).format({ output: 'time' })
if (time.substr(0, 1) === '0') {
return time.substr(2, time.length - 2)
} else {
return time
}
}
}
}
</script>