Vue 框架实现网页弹幕功能的详细技术方案
Vue实现弹幕功能技术方案一、弹幕功能概述弹幕是一种在视频、直播或网页上实时显示用户评论的交互形式,具有很强的实时性和互动性。在Vue项目中实现弹幕功能,需要考虑以下几个核心方面:
弹幕渲染:如何在容器中动态生成并移动弹幕轨道管理:如何合理分配弹幕轨道,避免重叠性能优化:大量弹幕同时出现时如何保证流畅度样式定制:支持不同颜色、大小、速度的弹幕交互控制:弹幕的发送、暂停、屏蔽等功能二、技术实现方案(一)核心实现思路使用CSS动画:弹幕的移动使用CSS动画实现,性能最优轨道系统:将弹幕区域划分为多个轨道,避免弹幕重叠对象池:复用弹幕元素,减少DOM操作响应式设计:根据容器大小动态调整轨道数量和弹幕速度(二)核心代码实现弹幕组件设计代码语言:javascript复制
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
const props = defineProps({
// 弹幕数据
messages: {
type: Array,
default: () => []
},
// 轨道数量
trackCount: {
type: Number,
default: 10
},
// 默认字体大小
fontSize: {
type: Number,
default: 18
},
// 默认速度
speed: {
type: Number,
default: 10
},
// 是否暂停
paused: {
type: Boolean,
default: false
}
});
const emits = defineEmits(['send']);
const container = ref(null);
const danmakuArea = ref(null);
const containerWidth = ref(0);
const containerHeight = ref(0);
const usedTracks = ref(new Set());
const activeDanmakus = ref(new Map()); // 活跃的弹幕
const danmakuId = ref(0); // 弹幕ID计数器
// 初始化
onMounted(() => {
updateContainerDimensions();
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
// 处理已有弹幕
renderExistingDanmakus();
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
// 清除所有动画
activeDanmakus.value.forEach(danmaku => {
danmaku.animation.cancel();
});
});
// 监听弹幕数据变化
watch(() => props.messages, (newMessages, oldMessages) => {
// 只处理新增的弹幕
if (newMessages.length > oldMessages.length) {
const newItems = newMessages.slice(oldMessages.length);
newItems.forEach(message => {
addDanmaku(message);
});
}
});
// 更新容器尺寸
const updateContainerDimensions = () => {
if (container.value) {
containerWidth.value = container.value.offsetWidth;
containerHeight.value = container.value.offsetHeight;
}
};
// 处理窗口大小变化
const handleResize = () => {
updateContainerDimensions();
// 重新计算所有弹幕位置
activeDanmakus.value.forEach(danmaku => {
updateDanmakuPosition(danmaku);
});
};
// 渲染已有弹幕
const renderExistingDanmakus = () => {
props.messages.forEach(message => {
addDanmaku(message);
});
};
// 添加弹幕
const addDanmaku = (message) => {
if (!danmakuArea.value || props.paused) return;
const track = getAvailableTrack();
if (track === -1) return; // 没有可用轨道
const id = `danmaku-${danmakuId.value++}`;
const danmaku = document.createElement('div');
danmaku.className = 'danmaku-item';
danmaku.id = id;
// 设置弹幕样式
const color = message.color || '#FFFFFF';
const fontSize = message.fontSize || props.fontSize;
const opacity = message.opacity !== undefined ? message.opacity : 1;
danmaku.style.color = color;
danmaku.style.fontSize = `${fontSize}px`;
danmaku.style.opacity = opacity;
danmaku.style.fontWeight = 'bold';
danmaku.style.textShadow = '0 0 2px rgba(0,0,0,0.8)';
danmaku.style.zIndex = '10';
danmaku.style.whiteSpace = 'nowrap';
danmaku.style.position = 'absolute';
// 设置弹幕内容
danmaku.textContent = message.text;
// 添加到容器
danmakuArea.value.appendChild(danmaku);
// 计算弹幕宽度
const danmakuWidth = danmaku.offsetWidth;
// 设置初始位置
const top = track * (containerHeight.value / props.trackCount) + 5;
danmaku.style.top = `${top}px`;
danmaku.style.left = `${containerWidth.value}px`;
// 标记轨道为已使用
usedTracks.value.add(track);
// 计算动画持续时间
const speed = message.speed || props.speed;
const duration = (containerWidth.value + danmakuWidth) / speed * 100;
// 应用动画
const animation = danmaku.animate(
[
{ transform: `translateX(0)` },
{ transform: `translateX(${-containerWidth.value - danmakuWidth}px)` }
],
{
duration: duration,
easing: 'linear'
}
);
animation.onfinish = () => {
// 动画结束后移除弹幕
if (danmakuArea.value && danmaku.parentNode === danmakuArea.value) {
danmakuArea.value.removeChild(danmaku);
}
// 释放轨道
usedTracks.value.delete(track);
// 从活跃弹幕列表中移除
activeDanmakus.value.delete(id);
};
// 保存弹幕信息
activeDanmakus.value.set(id, {
element: danmaku,
animation,
track,
width: danmakuWidth,
startTime: Date.now()
});
};
// 获取可用轨道
const getAvailableTrack = () => {
// 优先随机选择一个轨道
for (let i = 0; i < 3; i++) {
const randomTrack = Math.floor(Math.random() * props.trackCount);
if (!usedTracks.value.has(randomTrack)) {
return randomTrack;
}
}
// 如果没有可用轨道,尝试使用最早的轨道
if (usedTracks.value.size < props.trackCount) {
for (let i = 0; i < props.trackCount; i++) {
if (!usedTracks.value.has(i)) {
return i;
}
}
}
// 如果所有轨道都被占用,返回-1
return -1;
};
// 更新弹幕位置
const updateDanmakuPosition = (danmaku) => {
if (!containerWidth.value) return;
// 计算当前位置
const progress = danmaku.animation.effect.getComputedTiming().progress;
const originalWidth = containerWidth.value + danmaku.width;
const currentX = originalWidth * (1 - progress);
// 重新计算动画
danmaku.animation.cancel();
const newDuration = (currentX / originalWidth) * danmaku.animation.effect.getComputedTiming().duration;
danmaku.animation = danmaku.element.animate(
[
{ transform: `translateX(${containerWidth.value - currentX}px)` },
{ transform: `translateX(${-danmaku.width}px)` }
],
{
duration: newDuration,
easing: 'linear'
}
);
danmaku.animation.onfinish = () => {
if (danmakuArea.value && danmaku.element.parentNode === danmakuArea.value) {
danmakuArea.value.removeChild(danmaku.element);
}
usedTracks.value.delete(danmaku.track);
activeDanmakus.value.delete(danmaku.element.id);
};
};
// 发送弹幕
const sendDanmaku = (text, options = {}) => {
const message = {
text,
...options
};
emits('send', message);
addDanmaku(message);
};
// 暂停/恢复弹幕
const togglePause = (paused) => {
activeDanmakus.value.forEach(danmaku => {
if (paused) {
danmaku.animation.pause();
} else {
danmaku.animation.play();
}
});
};
// 监听暂停状态变化
watch(() => props.paused, (newValue) => {
togglePause(newValue);
});
export default {
methods: {
sendDanmaku,
togglePause
}
};
.danmaku-container {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
}
.danmaku-area {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.danmaku-item {
position: absolute;
white-space: nowrap;
will-change: transform;
transition: none !important;
}
弹幕发送组件代码语言:javascript复制
type="text"
v-model="message"
:placeholder="placeholder"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50"
@keyup.enter="send"
>
{{ danmakuCount }}条弹幕
import { ref, computed, defineProps, defineEmits } from 'vue';
const props = defineProps({
placeholder: {
type: String,
default: '发送一条弹幕...'
},
danmakuCount: {
type: Number,
default: 0
}
});
const emits = defineEmits(['send', 'toggleVisibility', 'toggleScroll']);
const message = ref('');
const color = ref('white');
const showDanmaku = ref(true);
const scrollDanmaku = ref(true);
const send = () => {
if (!message.value.trim()) return;
emits('send', {
text: message.value,
color: color.value,
timestamp: Date.now()
});
message.value = '';
};
const toggleShowDanmaku = () => {
showDanmaku.value = !showDanmaku.value;
emits('toggleVisibility', showDanmaku.value);
};
const toggleScrollDanmaku = () => {
scrollDanmaku.value = !scrollDanmaku.value;
emits('toggleScroll', scrollDanmaku.value);
};
三、应用实例(一)在页面中使用弹幕组件代码语言:javascript复制
ref="danmakuRef" :messages="danmakuList" :paused="!isPlaying" @send="handleSendDanmaku" >
@send="handleSendDanmaku" :danmakuCount="danmakuList.length" /> {{ item.text }} {{ formatTime(item.timestamp) }}最新弹幕
import { ref, onMounted, computed } from 'vue';
import Danmaku from '@/components/Danmaku.vue';
import DanmakuSender from '@/components/DanmakuSender.vue';
const videoRef = ref(null);
const danmakuRef = ref(null);
const danmakuList = ref([]);
const isPlaying = ref(false);
onMounted(() => {
// 监听视频播放状态
if (videoRef.value) {
videoRef.value.addEventListener('play', () => {
isPlaying.value = true;
});
videoRef.value.addEventListener('pause', () => {
isPlaying.value = false;
});
}
// 添加一些示例弹幕
addSampleDanmakus();
});
const addSampleDanmakus = () => {
const samples = [
{ text: '666666', color: 'white' },
{ text: '前方高能', color: 'red' },
{ text: '这个视频太精彩了', color: 'white' },
{ text: '主播太强了', color: 'yellow' },
{ text: '第一次看这个视频', color: 'white' }
];
samples.forEach((sample, index) => {
setTimeout(() => {
const message = {
...sample,
timestamp: Date.now()
};
danmakuList.value.push(message);
}, index * 2000);
});
};
const handleSendDanmaku = (message) => {
// 添加时间戳
const newMessage = {
...message,
timestamp: Date.now()
};
// 添加到弹幕列表
danmakuList.value.push(newMessage);
// 如果需要,可以发送到服务器
// sendDanmakuToServer(newMessage);
};
const formatTime = (timestamp) => {
const date = new Date(timestamp);
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
};
(二)弹幕管理插件为了方便在整个应用中使用弹幕功能,可以创建一个Vue插件:
代码语言:typescript复制// plugins/danmaku.ts
import type { App } from 'vue';
import Danmaku from '@/components/Danmaku.vue';
import DanmakuSender from '@/components/DanmakuSender.vue';
export const DanmakuPlugin = {
install(app: App) {
app.component('Danmaku', Danmaku);
app.component('DanmakuSender', DanmakuSender);
// 添加全局方法
app.config.globalProperties.$danmaku = {
send(text: string, options: any = {}) {
// 这里可以实现全局发送弹幕的逻辑
console.log('发送弹幕:', text, options);
}
};
} Vue, 网页弹幕,前端开发,JavaScript,HTML,CSS,WebSocket, 实时通信,动画效果,用户交互,组件化开发,数据绑定,响应式设计,前端框架,动态渲染
资源地址:
https://pan.quark.cn/s/e8c2520dc9db