背景
在很多App的H5首页,经常会看到顶部的轮播的消息流,类似于弹幕,展示给用户,增加营销感,例如某电商首页:
要实现类似的功能,该如何设计一个通用的组件?
组件分析
- 从弹幕展示层面,弹幕从视口外边移动到屏幕左边处,展示固定时间后,消失,间隔一段时间后又继续展示另一个弹幕
- 定时器可以实现固定时间间隔后更新弹幕,如果前后展示的弹幕之间有间隔,则需要区分两种情况:
如果单个弹幕展示时间等于弹幕间隔时间,利用
aniamtion
keyframes
50% 之后隐藏弹幕即可; 如果单个弹幕展示时间不等于弹幕间隔时间,通过 js 控制 css 的 keyframes 在W3C规范中无法实现,但是可以通过animation-delay + 组件销毁重建实现;
- 定时器可以实现固定时间间隔后更新弹幕,如果前后展示的弹幕之间有间隔,则需要区分两种情况:
- 从弹幕数据层面,弹幕信息包括用户的头像和一段文字信息,由后端返回
- 如果弹幕数据来源固定,一次接口拉取所有弹幕,除非用户刷新页面,否则弹幕数据不变,实现简单;
- 如果弹幕数据动态变化,比如说电商首页的拼单信息,一般是实时动态变化的,这时候就要考虑服务端推送弹幕信息了,即webSocket实现真正意义上的轮播弹幕;
四种类型的弹幕
单个弹幕展示时间等于弹幕间隔时间 + 固定弹幕数据
弹幕的无限循环轮播通过 CSS 动画
animation-iteration-count: infinite
,动画总时间为一个弹幕显示 + 隐藏的总时间
弹幕展示规则如下图所示
// 弹幕容器组件
<Barrage barrageList={barrageList} duration={4} />
// 弹幕渲染组件 duration 表示单个弹幕总时间
<BarrageItem barrageContent={barrageList[barrageIndex]} duration={duration} />
每隔 duration
时间展示下一个弹幕
const { duration } = this.props;
this.timer = setInterval(() => {
const { barrageList } = this.props;
const { barrageIndex } = this.state;
this.setState({
barrageIndex: (barrageIndex + 1) % barrageList.length // 这里取模是为了循环展示弹幕数据
});
}, duration * 1000);
function BarrageItem ({ barrageContent, duration }) {
return (
<div className='barrage' style={{ animation: `showBarrage ${duration}s ease-in-out infinite` }}>
<div className='thumb' style={{ backgroundImage: `url(${barrageContent.avatar})` }} />
<div className='text'>
{barrageContent.text}
</div>
</div>
);
}
CSS 动画飞入飞出,且 动画一直无限循环 来实现弹幕轮播
@keyframes showBarrage {
0% {
opacity: 0;
left: -100%;
}
5% {
opacity: 1;
left: .06rem;
}
45% {
opacity: 1;
left: .06rem;
}
50% {
opacity: 0;
left: -100%;
}
100% {
opacity: 0;
left: -100%;
}
}
这样就实现一个最简单的飞入飞出的弹幕了,效果如下:
单个弹幕展示时间与弹幕间隔时间可配 + 固定弹幕数据
弹幕的无限循环轮播不通过 CSS 动画,而是通过组件的销毁与重建;
弹幕显示时间为动画持续时间,弹幕间隔时间为动画延迟时间(因为
animation-iteration-count: infinite
情况下,延迟时间只在首次动画生效,后续每一个动画循环不会执行延迟效果)
弹幕展示规则如下图所示
// 弹幕容器组件
<Barrage barrageList={barrageList} showTime={3} gapTime={1} />
// 弹幕渲染组件
<BarrageItem barrageContent={barrageList[barrageIndex]} showTime={3} gapTime={1} />
每隔 showTime + gapTime
时间展示下一个弹幕
const { duration } = this.props;
this.timer = setInterval(() => {
const { barrageList } = this.props;
const { barrageIndex, showBarrage } = this.state;
this.setState({
barrageIndex: (barrageIndex + 1) % barrageList.length, // 这里取模是为了循环展示弹幕数据
showBarrage: !showBarrage
});
}, (showTime + gapTime) * 1000);
// 通过组件的key不同来销毁并重建组件
render () {
const { barrageList, showTime, gapTime } = this.props;
const { barrageIndex, showBarrage } = this.state;
return showBarrage
? <BarrageItem key={'before'} barrageContent={barrageList[barrageIndex]} showTime={showTime} gapTime={gapTime} />
: <BarrageItem key={'after'} barrageContent={barrageList[barrageIndex]} showTime={showTime} gapTime={gapTime} />;
}
function BarrageItem ({ barrageContent, duration }) {
return (
<div className='common-barrage' style={{ animation: `showBarrage ${showTime}s ease-in-out ${gapTime}s` }}>
<div className='thumb' style={{ backgroundImage: `url(${barrageContent.avatar})` }} />
<div className='text'>
{barrageContent.txt}
</div>
</div>
);
}
CSS 动画飞入飞出,且 动画一直无限循环 来实现弹幕轮播
@keyframes showBarrage {
0% {
opacity: 0;
left: -100%;
}
10% {
opacity: 1;
left: 6px;
}
90% {
opacity: 1;
left: 6px;
}
100% {
opacity: 0;
left: -100%;
}
}
效果如下:
服务端推送(Websocket)弹幕数据 + 弹幕间隔时间和弹幕展示时间可配
服务端推送,数据发送方为服务端,接收方为客户端,服务端每隔一段时间就推送一定数量的弹幕数据给客户端,客户端拿到数据后更新本地弹幕数据队列,展示顺序依推入弹幕的顺序执行。
首先需要了解下 Websocket
, 参考大神的 阮一峰博客 与 Websocket MDN
作为一名前端,自然而然想到结合基于 Nodejs 的 WebSocket 框架 Socket.io 来实现弹幕数据的推送
弹幕数据推送方
首先基于 Nodejs 的 http 模块和 WebSocket 框架 Socket.io 搭建一个简单的推送服务器, 每隔一段时间往客户端推送一定量的弹幕数据
搭建本地服务器
搭建过程参考官方文档 [搭建基于Node HTTP服务器的Socket](https://socket.io/docs/#Using-with-Node-http-server)
> npm init
> npm install --save socket.io
app.js
const socket = require('socket.io');
const http = require('http');
const server = http.createServer(/** 定义一个路由处理函数 */);
server.listen(8080);
const io = socket(server);
let timer = null;
// 模拟弹幕数据
io.on('connection', socket => {
console.log('连上了');
// 连上之后隔一段时间往客户端推送弹幕数据,每次推送三条随机弹幕
timer = setInterval(() => {
const obj = [
{
avatar: 'xxx1.png',
txt: '弹幕' + Math.floor(Math.random() * 100)
},
{
avatar: 'xxx2.png',
txt: '弹幕' + Math.floor(Math.random() * 100)
},
{
avatar: 'xxx3.png',
txt: '弹幕' + Math.floor(Math.random() * 100)
}
]
socket.send(JSON.stringify(obj)) // 传输序列化后的字符串数据
}, 6000)
socket.on('disconnect', () => {
console.log('断开了');
clearInterval(timer);
})
})
- 注意 WebSocket中的send方法不是任何数据都能发送的,现在只能发送三类数据,包括UTF-8的string类型(会默认转化为USVString),ArrayBuffer和Blob,且只有在建立连接后才能使用
弹幕数据接收方
前端这边通过安装 Socket.io
的客户端,即可监听并接收从服务端推送过来的数据
npm install --save socket.io-client
创建并连接到客户端 socket.io-client
// // Barrage.js
import io from 'socket.io-client';
const socket = io('ws://localhost:8080');
// 监听message事件并更新本地弹幕数据
const [barrageList, getBarrageList] = useState([]);
useEffect(() => {
socket.on('message', (data) => {
getBarrageList(oldBarrageList => {
console.log([...oldBarrageList, ...JSON.parse(data)])
return [...oldBarrageList, ...JSON.parse(data)];
});
})
}, []);
效果如下,右边控制台打印的是每次接收到服务端的推送弹幕后的本地数据
扩展-扫屏实时动态弹幕(类似于b站的弹幕效果)
王司徒镇楼
视频弹幕主要需要考虑如下几个问题
- 多轨道
- 弹幕移动速度
- 同一轨道弹幕是否可以重叠
- 弹幕颜色
- 轨道上下能否重叠(弹幕位置是否随机) 这个由于会导致弹幕很杂乱,所以直接设置为固定的轨道
- 弹幕数据源
在这里我们考虑一种比较简单的情况
-
固定轨道数
-
弹幕移动速度固定
-
统一轨道上弹幕不重叠
-
弹幕颜色随机
-
轨道上下不重叠
-
弹幕数据来自于服务端推送(模拟用户输入弹幕)
-
最后,这里推荐一个比较好用的基于 canvas 的视频弹幕组件 Barrage UI