Vue 3实战:打造个性化弹幕

1,024 阅读4分钟

项目初始化

首先,确保你已经安装了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>