项目初始化
首先,确保你已经安装了Node.js和Vue CLI。通过Vue CLI创建一个新的Vue 3项目。
组件
弹幕容器
弹幕就像是预备出发的马拉松运动员,循环的就是一圈一圈不停的跑,不循环就是只跑一次,那么跑的每一个位置相当于一条跑道,紧挨的两位运动员不能在同一条跑道。所以我们可以把弹幕区域按照每条弹幕的高度分为不同的跑道。我们这里以30px高度的弹幕为例,总共7条跑道,弹幕区域就是210px。当然也可以根据页面高度实时获取。
<div class="bullet-wrap" ref="danmuContainer">
<p class="bullet-item" v-for="(message, index) in state.showBulletData" :key="index"
:style="{top:message.top+'px'}">
<img src="https://p9-passport.byteacctimg.com/img/user-avatar/8507ccfc52b7323b8b7cb1ba11218ef9~110x110.awebp" alt="">//用户头像
{{ message.text }}
</p>
</div>
数据
const state = reactive({
barrageList: <any>[ // 弹幕原始数据
{ text: '我是第一条弹幕' },
{ text: '我是第二条弹幕' },
{ text: '我是第三条弹幕' },
{ text: '我是第四条弹幕' },
{ text: '我是第五条弹幕' },
{ text: '我是第六条弹幕' },
],
showBulletData: [], // 展示弹幕数组
topList:['0','30','60','90','120','150','180'],// 弹幕位置数组
lines: 7, // 总跑道数量
currentLine: 1, // 当前跑道
});
样式
.bullet-wrap {
position: fixed;
top: 0;
height: 210px;
width: 100%;
z-index: 99;
overflow: hidden;
pointer-events: none;
/* 防止点击弹幕 */
}
.bullet-item {
position: absolute;
animation: rightToleft 9s linear both;
// 动画时间(也就是弹幕从右侧出现到左侧消失的时间),
// 可以根据每一条弹幕内容的长度来给动画时间,可以保证弹幕速度一致
white-space: nowrap;
background-color: rgba(255, 192, 203, 0.285);
border-radius: 30px;
height: 30px;
font-size: 14px;
color: #FFFFFF;
line-height: 30px;
/* padding: 0 10px; */
padding-right: 10px;
display: flex;
}
.bullet-item img {
width: 30px;
height: 30px;
border-radius: 50%;
margin-right: 10px;
}
@keyframes rightToleft { // 弹幕从右到左
0% {
transform: translate(110vw);
}
100% {
transform: translate(-100%);
}
}
处理数据
这里我利用setInterval来使弹幕一条一条播放,根据定位的top值来区分跑道
function showNextBullet() {
if (!state.barrageList?.length) return;// 判断是否还有待发送的弹幕
state.currentLine = (state.currentLine % state.lines) + 2; // 取下标(因为随机数会取到两个紧挨着的相同的随机数,因此会导致弹幕折叠)
const currentBullet = state.barrageList.shift();// 已发送的弹幕从待发送弹幕列表中删除
currentBullet.top = state.topList[state.currentLine - 1]; 给每条弹幕增加top值
state.showBulletData.push(currentBullet);
}
const timer = ref(null);
onMounted(async () => {
showNextBullet(); // 立即显示第一个弹幕
timer.value = setInterval(showNextBullet, 1500);//每隔1.5秒执行一次
});
onBeforeUnmount(() => {
if (timer.value) {
clearInterval(timer.value);//清除setInterval,防止内存溢出
}
});
优化
在页面不可见一段时间后再回到这个页面时,弹幕会挤在一堆从右侧出现,我用了原生的监听事件来使重新回到这个页面时刷新页面,这部分可以写在调用组件的父组件当中,或者重新获取弹幕数据,保证弹幕不会叠在一起同时出现多条。
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
// 页面变为可见状态
if (props.data) {
state.barrageList = props.data
showNextBullet(); // 立即显示第一个弹幕
}
timer.value = setInterval(showNextBullet, 1500);
// window.location.reload()
} else if (document.visibilityState === 'hidden') {
// 页面变为不可见状态,可以在这里执行息屏时的操作
console.log('页面变为不可见');
clearInterval(timer.value);
}
});
若想使弹幕循环播放,可以在删除那一条数据时在重新push到待发送数组中
const currentBullet = state.barrageList.shift();
// 弹幕循环
state.barrageList.push(currentBullet)
currentBullet.line = state.currentLine;
currentBullet.top = state.topList[state.currentLine - 1];
state.showBulletData.push(currentBullet);
但是只加这个循环的话,当弹幕数量小于跑道数量时会出现弹幕乱飘的情况,也就是top值会覆盖导致的。
// 保证数据量少时弹幕不会串行(只有在增加循环时会出现这样的问题)
if (state.barrageList?.length <= 7) {
state.currentLine = (state.currentLine % state.lines) + 2;
let j = 0
for (let i = 0; i <= 7; i++) {
if (!state.barrageList[i]) {
state.barrageList[i] = {
text: state.barrageList[j].text,
top:state.topList[state.currentLine - 1]
}
j++
}
}
return
}
完整代码
<template>
<div class="bullet-wrap" ref="danmuContainer">
<p class="bullet-item" v-for="(message, index) in state.showBulletData" :key="index"
:style="{ top: message.top + 'px' }">
<img src="https://p9-passport.byteacctimg.com/img/user-avatar/8507ccfc52b7323b8b7cb1ba11218ef9~110x110.awebp"
alt="">
{{ message.text }}
</p>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, onBeforeUnmount } from 'vue';
const danmuContainer = ref(null);
const state = reactive({
barrageList: <any>[
{ text: '我是第一条弹幕' },
{ text: '我是第二条弹幕' },
{ text: '我是第三条弹幕' },
],
showBulletData: [],
topList: ['0', '30', '60', '90', '120', '150', '180'],
lines: 7,
page: 1,
currentLine: 1,
});
function showNextBullet() {
if (!state.barrageList?.length) return;
// 保证数据量少时弹幕不会串行(只有在增加循环时会出现这样的问题)
if (state.barrageList?.length <= 7) {
state.currentLine = (state.currentLine % state.lines) + 2;
let j = 0
for (let i = 0; i <= 7; i++) {
if (!state.barrageList[i]) {
state.barrageList[i] = {
text: state.barrageList[j].text,
top: state.topList[state.currentLine - 1]
}
j++
}
}
return
}
state.currentLine = (state.currentLine % state.lines) + 2;
const currentBullet = state.barrageList.shift();
// 弹幕循环
state.barrageList.push(currentBullet)
currentBullet.line = state.currentLine;
currentBullet.top = state.topList[state.currentLine - 1];
state.showBulletData.push(currentBullet);
}
const timer = ref(null);
onMounted(async () => {
showNextBullet(); // 立即显示第一个弹幕
timer.value = setInterval(showNextBullet, 1500);
});
onBeforeUnmount(() => {
if (timer.value) {
clearInterval(timer.value);
}
});
</script>
<style scope>
.bullet-wrap {
position: fixed;
top: 0;
height: 210px;
width: 100%;
z-index: 99;
overflow: hidden;
pointer-events: none;
/* 防止点击弹幕 */
}
.bullet-item {
position: absolute;
animation: rightToleft 9s linear both;
white-space: nowrap;
background-color: rgba(255, 192, 203, 0.285);
border-radius: 30px;
height: 30px;
font-size: 14px;
color: #FFFFFF;
line-height: 30px;
/* padding: 0 10px; */
padding-right: 10px;
display: flex;
}
.bullet-item img {
width: 30px;
height: 30px;
border-radius: 50%;
margin-right: 10px;
}
@keyframes rightToleft {
0% {
transform: translate(110vw);
}
100% {
transform: translate(-100%);
}
}
</style>