2

pc版快手、移动端b站弹幕列表效果实现

一个原生js开发的横向弹幕
  • 效果如下:
    444.gif


  • 效果描述:页面上部为直播视频播放器,下半部分是弹幕列表,ui效果类似b站app分享出去的h5直播间,要实现的效果,当弹幕滚动到最底部的时候,新来的的弹幕会自动往上顶,如果向下滑动去看历史弹幕列表新来的弹幕则停止往上顶,然后左下角会出现新消息提示,当点击新消息提示则滚动到最底端新消息提示消失,或者手动滚动到最底端消息提示消失
  • 难点:手动滚动到最底部新消失提示消失(因为要监听scroll事件并在其中获取dom元素尺寸)
  • 误区:刚开始开发中,我的想法是弹幕列表最多存储 150 条数据,如果超出则新push一个,顶部就shift一个,这样有一个问题就是scroll事件是会不断的触发,在观察完pc端快手直播的弹幕列表,发现其大概原理是超出150条,则将前50条一次性去除,这样 scroll仅仅只会触发一次,再加上防抖操作,监听scroll事件的开销就很小了

  • 具体代码如下:
/**
 * h5 直播间弹幕列表组件
 * 
 * 使用方式
 * {liveInfo.danmuChannel && <DanmuPanel ref={ref => this.danmuRef = ref} wrapH={DANMUWRAPH}/> }
 * 父组件socket拿到数据通过组件 ref 实例调用 addDanmu(data)
 * 
 * 如需样式调整请自行修改less样式
 */

import React from "react";
import classnames from "classnames";
import { debounce, isScrollBottom } from "@/util/index"; // 自行实现或参考我下面的代码

import "./index.less";

interface propsType {
    wrapH?: string; // 弹幕容器高度
}

// 弹幕对象类型
type contentType = {
    name: string;
    content: string;
    key: string;
    reactId?: any; // 列表唯一标识,如不传会自动添加
}

export default class DanmuPanel extends React.Component<propsType, any> {
    danmuWrapHeight: number; // 弹幕容器dom高度
    danmuWrapRef: HTMLElement; // 弹幕容器dom
    danmuListRef: HTMLElement; // 弹幕列表dom
    restNums: number;
    reactId: number; // 弹幕列表 key
    debounceCb: Function;
    isBindScrolled: boolean;
    constructor(props) {
        super(props)
        this.state = {
            danmuList: [],
            restDanmu: 0,
        }
        this.restNums = 0;
        this.reactId = 0;
        this.debounceCb = debounce(this.danmuScroll, 200)
    }

    componentDidMount() {
        this.initDom();
        // this.testAddDanmu();
    }
    // 测试代码,后期删掉
    testAddDanmu() {
        let i = 0;
        setInterval(() => {
            ++i
            this.addDanmu({
                name: i + '-我是名字',
                content: i + '-我是内容',
                key: "danmu",
                reactId: i
            })
        }, 100)
    }

    private initDom() {
        this.danmuWrapRef = document.querySelector('.danmu-wrap');
        this.danmuListRef = document.querySelector(".danmu-wrap .list");
        this.danmuWrapHeight = this.danmuWrapRef.offsetHeight;
    }

    private addScroll = () => {
        this.debounceCb();
        this.isBindScrolled = true;
    }

    // 弹幕列表滚动到底部回调
    private danmuScroll = () => {
        const ele: HTMLElement = this.danmuWrapRef;
        const isBottom = isScrollBottom(ele, ele.clientHeight);
        if (isBottom) {
            this.restNums = 0;
            this.setState({ restDanmu: 0 })
        }
    }

    // 供父组件调用 socket拿到结果后调用
    public addDanmu = (data: contentType) => {
        const { danmuList } = this.state;
        data.reactId = ++this.reactId;
        if (danmuList.length >= 150) {
            danmuList.splice(0, 50)
        };
        danmuList.push(data);
        this.setState({ danmuList }, this.renderDanmu)
    }

    private renderDanmu = () => {
        const listH = this.danmuListRef.offsetHeight;
        const diff = listH - this.danmuWrapHeight;
        const top = this.danmuWrapRef.scrollTop;
        if (diff - top < 50) {
            if (diff > 0) {
                if (this.isBindScrolled) {
                    this.isBindScrolled = false;
                    this.danmuWrapRef.removeEventListener('scroll', this.addScroll)
                }
                this.danmuWrapRef.scrollTo({ top: diff + 40, left: 0, behavior: 'smooth' });
                this.restNums = 0;
            }
        } else {
            ++this.restNums;
            if (!this.isBindScrolled) {
                this.isBindScrolled = true;
                this.danmuWrapRef.addEventListener('scroll', this.addScroll)
            }
        }
        this.setState({ restDanmu: this.restNums >= 99 ? '99+' : this.restNums });
    }

    private scrollBottom = () => {
        this.restNums = 0;
        this.setState({ restDanmu: this.restNums })
        this.danmuWrapRef.scrollTo({ top: this.danmuListRef.offsetHeight, left: 0, behavior: 'smooth' });
    }

    render() {
        const { danmuList, restDanmu } = this.state;
        return (
            <div className="danmu-panel">
                <div className="danmu-wrap" style={{ height: this.props.wrapH }}>
                    <ul className="list">
                        {
                            danmuList.map((v) => (
                                <li key={v.reactId}>
                                    <span className="name">{v.name}:</span>
                                    <span className={classnames("content", v.key)}>{v.content}</span>
                                </li>
                            ))
                        }
                    </ul>
                </div>
                {
                    !!restDanmu &&
                    <div className="rest-nums" onClick={this.scrollBottom}>{ restDanmu }条新消息</div>
                }
            </div>
        )
    }
}
  • debounce isScrollBottom 实现如下

/**
 * 防抖函数
 * @param fn 
 * @param wait 
 */
export function debounce(fn:Function, wait:number = 500) {
    let timeout:number = 0;
    return function () {
        // 每次触发 scroll handler 时先清除定时器
        clearTimeout(timeout);
        // 指定 xx ms 后触发真正想进行的操作 handler
        timeout = setTimeout(fn, wait);
    };
};


/**
 * 是否滚到到容器底部
 * @param ele 滚动容器
 * @param wrapHeight 容器高度
 */
export function isScrollBottom(ele: HTMLElement, wrapHeight:number, threshold: number = 30) {
    const h1 = ele.scrollHeight - ele.scrollTop;
    const h2 = wrapHeight + threshold;
    const isBottom = h1 <= h2;
    // console.log('-->', isBottom, ele.scrollHeight, ele.scrollTop, h2)
    return isBottom;
}

大桔子
588 声望51 粉丝

下一步该干什么呢...