轮播弹幕的实现

3,051 阅读6分钟

背景

在很多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