xrkffgg

xrkffgg 查看完整档案

北京编辑南京理工大学  |  通信工程 编辑  |  填写所在公司/组织 xrkffgg.github.io/#/ 编辑
编辑

~

个人动态

xrkffgg 收藏了文章 · 6月19日

Canvas + WebSocket + Redis 实现一个视频弹幕

在这里插入图片描述


阅读原文


页面布局

首先,我们需要实现页面布局,在根目录创建 index.html 布局中我们需要有一个 video 多媒体标签引入我们的本地视频,添加输入弹幕的输入框、确认发送的按钮、颜色选择器、字体大小滑动条,创建一个 style.css 来调整页面布局的样式,这里我们顺便创建一个 index.js 文件用于后续实现我们的核心逻辑,先引入到页面当中。

HTML 布局代码如下:

<!-- 文件:index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="style.css">
    <title>视频弹幕</title>
</head>
<body>
    <div id="cantainer">
        <h2>Canvas + WebSocket + Redis 实现视频弹幕</h2>
        <div id="content">
            <canvas id="canvas"></canvas>
            <video id="video" data-original="./barrage.mp4" controls></video>
        </div>
        <!-- 输入弹幕内容 -->
        <input type="text" id="text">
        <!-- 添加弹幕按钮 -->
        <button id="add">发送</button>
        <!-- 选择文字颜色 -->
        <input type="color" id="color">
        <!-- 调整字体大小 -->
        <input type="range" max="40" min="20" id="range">
    </div>
    <script data-original="./index.js"></script>
</body>
</html>

CSS 样式代码如下:

/* 文件:style.css */
#cantainer {
    text-align: center;
}
#content {
    width: 640px;
    margin: 0 auto;
    position: relative;
}
#canvas {
    position: absolute;
}
video {
    width: 640px;
    height: 360px;
}
input {
    vertical-align: middle;
}

布局效果如下图:

在这里插入图片描述


定义接口,构造假数据

我们弹幕中的弹幕数据正常情况下应该是通过与后台数据交互请求回来,所以我们需要先定义数据接口,并构造假数据来实现前端逻辑。

数据字段定义:

  • value:表示弹幕的内容(必填)
  • time:表示弹幕出现的时间(必填)
  • speed:表示弹幕移动的速度(选填)
  • color:表示弹幕文字的颜色(选填)
  • fontSize:表示弹幕的字体大小(选填)
  • opacity:表示弹幕文字的透明度(选填)

上面的 valuetime 是必填参数,其他的选填参数可以在前端设置默认值。

前端定义的假数据如下:

// 文件:index.js
let data = [
    {
        value: "这是第一条弹幕",
        speed: 2,
        time: 0,
        color: "red",
        fontSize: 20
    },
    {
        value: "这是第二条弹幕",
        time: 1
    }
];


实现前端弹幕的逻辑

我们希望是把弹幕封装成一个功能,只要有需要的地方就可以使用,从而实现复用,那么不同的地方使用这个功能通常的方式是 new 一个实例,传入当前使用该功能对应的参数,我们也使用这种方式来实现,所以我们需要封装一个统一的构造函数或者类,参数为当前的 canvas 元素、video 元素和一个 options 对象,options 里面的 data 属性为我们的弹幕数据,之所以不直接传入 data 是为了后续参数的扩展,严格遵循开放封闭原则,这里我们就统一使用 ES6 的 class 类来实现。

1、创建弹幕功能的类及基本参数处理

布局时需要注意 Canvas 的默认宽为 300px,高为 150px,我们要保证 Canvas 完全覆盖整个视频,需要让 Canvas 与 video 宽高相等。
因为我们不确定每一个使用该功能的视频的宽高都是一样的,所以 Canvas 画布的宽高并没有通过 CSS 来设置,而是通过 JS 在类创建实例初始化属性的时候动态设置。

// 文件:index.js
class CanvasBarrage {
    constructor(canvas, video, options = {}) {
        // 如果没有传入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 当前的 canvas 元素
        this.video = video; // 当前的 video 元素

        // 设置 canvas 与 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默认暂停播放,表示不渲染弹幕
        this.isPaused = true;

        // 没传参数的默认值
        let defaultOptions = {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
        Object.assign(this, defaultOptions, options);
    }
}

应该挂在实例上的属性除了有当前的 canvas 元素、video 元素、弹幕数据的默认选项以及弹幕数据之外,还应该有一个代表当前是否渲染弹幕的参数,因为视频暂停的时候,弹幕也是暂停的,所以没有重新渲染,因为是否暂停与弹幕是否渲染的状态是一致的,所以我们这里就用 isPaused 参数来代表当前是否暂停或重新渲染弹幕,值类型为布尔值。

2、创建构造每一条弹幕的类

我们知道,后台返回给我们的弹幕数据是一个数组,这个数组里的每一个弹幕都是一个对象,而对象上有着这条弹幕的信息,如果我们需要在每一个弹幕对象上再加一些新的信息或者在每一个弹幕对象的处理时用到了当前弹幕功能类 CanvasBarrage 实例的一些属性值,取值显然是不太方便的,这样为了后续方便扩展,遵循开放封闭原则,我们把每一个弹幕的对象转变成同一个类的实例,所以我们创建一个名为 Barrage 的类,让我们每一条弹幕的对象进入这个类里面走一遭,挂上一些扩展的属性。

// 文件:index.js
class Barrage {
    constructor(item, ctx) {
        this.value = item.value; // 弹幕的内容
        this.time = item.time; // 弹幕出现的时间
        this.item = item; // 每一个弹幕的数据对象
        this.ctx = ctx; // 弹幕功能类的执行上下文
    }
}

在我们的 CanvasBarrage 类上有一个存储弹幕数据的数组 data,此时我们需要给 CanvasBarrage 增加一个属性用来存放 “加工” 后的每条弹幕对应的实例。

// 文件:index.js
class CanvasBarrage {
    constructor(canvas, video, options = {}) {
        // 如果没有传入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 当前的 canvas 元素
        this.video = video; // 当前的 video 元素

        // 设置 canvas 与 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默认暂停播放,表示不渲染弹幕
        this.isPaused = true;

        // 没传参数的默认值
        let defaultOptions = {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
        Object.assign(this, defaultOptions, options);

        // ********** 以下为新增代码 **********
        // 存放所有弹幕实例,Barrage 是创造每一条弹幕的实例的类
        this.barrages = this.data.map(item => new Barrage(item, this));
        // ********** 以上为新增代码 **********
    }
}

其实通过上面操作以后,我们相当于把 data 里面的每一条弹幕对象转换成了一个 Barrage 类的一个实例,把当前的上下文 this 传入后可以随时在每一个弹幕实例上获取 CanvasBarrage 类实例的属性,也方便我们后续扩展方法,遵循这种开放封闭原则的方式开发,意义是不言而喻的。

3、在 CanvasBarrage 类实现渲染所有弹幕的 render 方法

CanvasBarragerender 方法是在创建弹幕功能实例的时候应该渲染 Canvas 所以应该在 CanvasBarrage 中调用,在 render 内部,每一次渲染之前都应该先将 Canvas 画布清空,所以需要给当前的 CanvasBarrage 类新增一个属性用于存储 Canvas 画布的内容。

// 文件:index.js
class CanvasBarrage {
    constructor(canvas, video, options = {}) {
        // 如果没有传入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 当前的 canvas 元素
        this.video = video; // 当前的 video 元素

        // 设置 canvas 与 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默认暂停播放,表示不渲染弹幕
        this.isPaused = true;

        // 没传参数的默认值
        let defaultOptions = {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
        Object.assign(this, defaultOptions, options);

        // 存放所有弹幕实例,Barrage 是创造每一条弹幕的实例的类
        this.barrages = this.data.map(item => new Barrage(item, this));

        // ********** 以下为新增代码 **********
        // Canvas 画布的内容
        this.context = canvas.getContext("2d");

        // 渲染所有的弹幕
        this.render();
        // ********** 以上为新增代码 **********
    }

    // ********** 以下为新增代码 **********
    render() {
        // 渲染整个弹幕
        // 第一次先进行清空操作,执行渲染弹幕,如果没有暂停,继续渲染
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 渲染弹幕
        this.renderBarrage();
        if (this.isPaused == false) {
            // 递归渲染
            requestAnimationFrame(this.render.bind(this));
        }
    }
    // ********** 以上为新增代码 **********
}

在上面的 CanvasBarragerender 函数中,清空时由于 Canvas 性能比较好,所以将整个画布清空,所以从坐标 (0, 0) 点,清空的宽高为整个 Canvas 画布的宽高。

只要视频是在播放状态应该不断的调用 render 方法实现清空画布、渲染弹幕、判断是否暂停,如果非暂停状态继续渲染,所以我们用到了递归调用 render 去不断的实现渲染,但是递归时如果直接调用 render,性能特别差,程序甚至会挂掉,以往这种情况我们会在递归外层加一个 setTimeout 来定义一个短暂的递归时间,但是这个过程类似于动画效果,如果使用 setTimeout 其实是将同步代码转成了异步执行,会增加不确定性导致画面出现卡顿的现象。

这里我们使用 H5 的新 API requestAnimationFrame,可以在平均 1/60 S 内帮我执行一次该方法传入的回调,我们直接把 render 函数作为回调函数传入 requestAnimationFrame,该方法是按照帧的方式执行,动画流畅,需要注意的是,render 函数内使用了 this,所以应该处理一下 this 指向问题。

由于我们使用面向对象的方式,所以渲染弹幕的具体细节,我们抽离出一个单独的方法 renderBarrage,接下来看一下 renderBarrage 的实现。

4、CanvasBarrage 类 render 内部 renderBarrage 的实现

// 文件:index.js
class CanvasBarrage {
    constructor(canvas, video, options = {}) {
        // 如果没有传入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 当前的 canvas 元素
        this.video = video; // 当前的 video 元素

        // 设置 canvas 与 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默认暂停播放,表示不渲染弹幕
        this.isPaused = true;

        // 没传参数的默认值
        let defaultOptions = {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
        Object.assign(this, defaultOptions, options);

        // 存放所有弹幕实例,Barrage 是创造每一条弹幕的实例的类
        this.barrages = this.data.map(item => new Barrage(item, this));

        // Canvas 画布的内容
        this.context = canvas.getContext("2d");

        // 渲染所有的弹幕
        this.render();
    }
    render() {
        // 渲染整个弹幕
        // 第一次先进行清空操作,执行渲染弹幕,如果没有暂停,继续渲染
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 渲染弹幕
        this.renderBarrage();
        if (this.isPaused == false) {
            // 递归渲染
            requestAnimationFrame(this.render.bind(this));
        }
    }

    // ********** 以下为新增代码 **********
    renderBarrage() {
        // 将数组的弹幕一个一个取出,判断时间和视频的时间是否符合,符合就执行渲染此弹幕
        let time = this.video.currentTime;
        this.barrages.forEach(barrage => {
            // 当视频时间大于等于了弹幕设置的时间,那么开始渲染(时间都是以秒为单位)
            if (time >= barrage.time) {
                // 初始化弹幕的各个参数,只有在弹幕将要出现的时候再去初始化,节省性能,初始化后再进行绘制
                // 如果没有初始化,先去初始化一下
                if (!barrage.isInited) {
                    // 初始化后下次再渲染就不需要再初始化了,所以创建一个标识 isInited
                    barrage.init();
                    barrage.isInited = true;
                }
            }
        });
    }
    // ********** 以上为新增代码 **********
}

此处的 renderBarrage 方法内部主要对每一条弹幕实例所设置的出现时间和视频的播放时间做对比,如果视频的播放时间大于等于了弹幕出现的时间,说明弹幕需要绘制在 Canvas 画布内。

之前我们的每一条弹幕实例的属性可能不全,弹幕的其他未传参数并没有初始化,所以为了最大限度的节省性能,我们在弹幕该第一次绘制的时候去初始化参数,等到视频播放的时间变化再去重新绘制时,不再初始化参数,所以初始化参数的方法放在了判断弹幕出现时间的条件里面执行,又设置了代表弹幕实例是不是初始化了的参数 isInited,初始化函数 init 执行过一次后,马上修改 isInited 的值,保证只初始化参数一次。

renderBarrage 方法中我们可以看出来,其实我们是循环了专门存放每一条弹幕实例(Barrage 类的实例)的数组,我们在内部用实例去调用的方法 init 应该是在 Barrage 类的原型上,下面我们去 Barrage 类上实现 init 的逻辑。

5、Barrage 类 init 的实现

// 文件:index.js
class Barrage {
    constructor(item, ctx) {
        this.value = item.value; // 弹幕的内容
        this.time = item.time; // 弹幕出现的时间
        this.item = item; // 每一个弹幕的数据对象
        this.ctx = ctx; // 弹幕功能类的执行上下文
    }

    // ********** 以下为新增代码 **********
    init() {
        this.opacity = this.item.opacity || this.ctx.opacity;
        this.color = this.item.color || this.ctx.color;
        this.fontSize = this.item.fontSize || this.ctx.fontSize;
        this.speed = this.item.speed || this.ctx.speed;

        // 求自己的宽度,目的是用来校验当前是否还要继续绘制(边界判断)
        let span = document.createElement("span");

        // 能决定宽度的只有弹幕的内容和文字的大小,和字体,字体默认为微软雅黑,我们就不做设置了
        span.innerText = this.value;
        span.style.font = this.fontSize + 'px "Microsoft YaHei';

        // span 为行内元素,取不到宽度,所以我们通过定位给转换成块级元素
        span.style.position = "absolute";

        document.body.appendChild(span); // 放入页面
        this.width = span.clientWidth; // 记录弹幕的宽度
        document.body.removeChild(span); // 从页面移除

        // 存储弹幕出现的横纵坐标
        this.x = this.ctx.canvas.width;
        this.y = this.ctx.canvas.height;

        // 处理弹幕纵向溢出的边界处理
        if (this.y < this.fontSize) {
            this.y = this.fontSize;
        }
        if (this.y > this.ctx.canvas.height - this.fontSize) {
            this.y = this.ctx.canvas.height - this.fontSize;
        }
    }
    // ********** 以上为新增代码 **********
}

在上面代码的 init 方法中我们其实可以看出,每条弹幕实例初始化的时候初始的信息除了之前说的弹幕的基本参数外,还获取了每条弹幕的宽度(用于后续做弹幕是否已经完全移出屏幕的边界判断)和每一条弹幕的 xy 轴方向的坐标并为了防止弹幕在 y 轴显示不全做了边界处理。

6、实现每条弹幕的渲染和弹幕移除屏幕的处理

我们当时在 CanvasBarrage 类的 render 方法中的渲染每个弹幕的方法 renderBarrage中(原谅这么啰嗦,因为到现在内容已经比较多,说的具体一点方便知道是哪个步骤,哈哈)只做了对每一条弹幕实例的初始化操作,并没有渲染在 Canvas 画布中,这时我们主要做两部操作,实现每条弹幕渲染在画布中和左侧移出屏幕不再渲染的边界处理。

// 文件:index.js
class CanvasBarrage {
    constructor(canvas, video, options = {}) {
        // 如果没有传入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 当前的 canvas 元素
        this.video = video; // 当前的 video 元素

        // 设置 canvas 与 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默认暂停播放,表示不渲染弹幕
        this.isPaused = true;

        // 没传参数的默认值
        let defaultOptions = {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
        Object.assign(this, defaultOptions, options);

        // 存放所有弹幕实例,Barrage 是创造每一条弹幕的实例的类
        this.barrages = this.data.map(item => new Barrage(item, this));

        // Canvas 画布的内容
        this.context = canvas.getContext("2d");

        // 渲染所有的弹幕
        this.render();
    }
    render() {
        // 渲染整个弹幕
        // 第一次先进行清空操作,执行渲染弹幕,如果没有暂停,继续渲染
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 渲染弹幕
        this.renderBarrage();
        if (this.isPaused == false) {
            // 递归渲染
            requestAnimationFrame(this.render.bind(this));
        }
    }
    renderBarrage() {
        // 将数组的弹幕一个一个取出,判断时间和视频的时间是否符合,符合就执行渲染此弹幕
        let time = this.video.currentTime;
        this.barrages.forEach(barrage => {
            // ********** 以下为改动的代码 **********
            // 当视频时间大于等于了弹幕设置的时间,那么开始渲染(时间都是以秒为单位)
            if (!barrage.flag && time >= barrage.time) {
                // ********** 以上为改动的代码 **********

                // 初始化弹幕的各个参数,只有在弹幕将要出现的时候再去初始化,节省性能,初始化后再进行绘制
                // 如果没有初始化,先去初始化一下
                if (!barrage.isInited) {
                    // 初始化后下次再渲染就不需要再初始化了,所以创建一个标识 isInited
                    barrage.init();
                    barrage.isInited = true;
                }

                // ********** 以下为新增代码 **********
                barrage.x -= barrage.speed;
                barrage.render(); // 渲染该条弹幕
                if (barrage.x < barrage.width * -1) {
                    barrage.flag = true; // 是否出去了,出去了,做停止渲染的操作
                }
                // ********** 以上为新增代码 **********
            }
        });
    }
}

每个弹幕实例都有一个 speed 属性,该属性代表着弹幕移动的速度,换个说法其实就是每次减少的 x 轴的差值,所以我们其实是通过改变 x 轴的值再重新渲染而实现弹幕的左移,我们创建了一个标识 flag 挂在每个弹幕实例下,代表是否已经离开屏幕,如果离开则更改 flag 的值,使外层的 CanvasBarrage 类的 render 函数再次递归时不进入渲染程序。

每一条弹幕具体是怎么渲染的,通过代码可以看出每个弹幕实例在 x 坐标改变后都调用了实例方法 render 函数,注意此 render 非彼 render,该 render 函数属于 Barrage 类,目的是为了渲染每一条弹幕,而 CanvasBarrage 类下的 render,是为了在视频时间变化时清空并重新渲染整个 Canvas 画布。

7、Barrage 类下的 render 方法的实现

// 文件:index.js
class Barrage {
    constructor(item, ctx) {
        this.value = item.value; // 弹幕的内容
        this.time = item.time; // 弹幕出现的时间
        this.item = item; // 每一个弹幕的数据对象
        this.ctx = ctx; // 弹幕功能类的执行上下文
    }
    init() {
        this.opacity = this.item.opacity || this.ctx.opacity;
        this.color = this.item.color || this.ctx.color;
        this.fontSize = this.item.fontSize || this.ctx.fontSize;
        this.speed = this.item.speed || this.ctx.speed;

        // 求自己的宽度,目的是用来校验当前是否还要继续绘制(边界判断)
        let span = document.createElement("span");

        // 能决定宽度的只有弹幕的内容和文字的大小,和字体,字体默认为微软雅黑,我们就不做设置了
        span.innerText = this.value;
        span.style.font = this.fontSize + 'px "Microsoft YaHei';

        // span 为行内元素,取不到宽度,所以我们通过定位给转换成块级元素
        span.style.position = "absolute";

        document.body.appendChild(span); // 放入页面
        this.width = span.clientWidth; // 记录弹幕的宽度
        document.body.removeChild(span); // 从页面移除

        // 存储弹幕出现的横纵坐标
        this.x = this.ctx.canvas.width;
        this.y = this.ctx.canvas.height;

        // 处理弹幕纵向溢出的边界处理
        if (this.y < this.fontSize) {
            this.y = this.fontSize;
        }
        if (this.y > this.ctx.canvas.height - this.fontSize) {
            this.y = this.ctx.canvas.height - this.fontSize;
        }
    }

    // ********** 以下为新增代码 **********
    render() {
        this.ctx.context.font = this.fontSize + 'px "Microsoft YaHei"';
        this.ctx.context.fillStyle = this.color;
        this.ctx.context.fillText(this.value, this.x, this.y);
    }
    // ********** 以上为新增代码 **********
}

从上面新增代码我们可以看出,其实 Barrage 类的 render 方法只是将每一条弹幕的字号、颜色、内容、坐标等属性通过 Canvas 的 API 添加到了画布上。

8、实现播放、暂停事件

还记得我们的 CanvasBarrage 类里面有一个属性 isPaused,属性值控制了我们是否递归渲染,这个属性与视频暂停的状态是一致的,我们在播放的时候,弹幕不断的清空并重新绘制,当暂停的时候弹幕也应该跟着暂停,说白了就是不在调用 CanvasBarrage 类的 render 方法,其实就是在暂停、播放的过程中不断的改变 isPaused 的值即可。

还记得我们之前构造的两条假数据 data 吧,接下来我们添加播放、暂停事件,来尝试使用一下我们的弹幕功能。

// 文件:index.js
// 实现一个简易选择器,方便获取元素,后面获取元素直接调用 $
const $ = document.querySelector.bind(document);

// 获取 Canvas 元素和 Video 元素
let canvas = $("#canvas");
let video = $("#video");

let canvasBarrage = new CanvasBarrage(canvas, video, {
    data
});

// 添加播放事件
video.addEventListener("play", function() {
    canvasBarrage.isPaused = false;
    canvasBarrage.render();
});

// 添加暂停事件
video.addEventListener("pause", function() {
    canvasBarrage.isPaused = true;
});

9、实现发送弹幕事件

// 文件:index.js
$("#add").addEventListener("click", function() {
    let time = video.currentTime; // 发送弹幕的时间
    let value = $("#text").value; // 发送弹幕的文字
    let color = $("#color").value; // 发送弹幕文字的颜色
    let fontSize = $("#range").value; // 发送弹幕的字体大小
    let sendObj = { time, value, color, fontSize }; //发送弹幕的参数集合
    canvasBarrage.add(sendObj); // 发送弹幕的方法
});

其实我们发送弹幕时,就是向 CanvasBarrage 类的 barrages 数组里添加了一条弹幕的实例,我们单独封装了一个 add 的实例方法。

// 文件:index.js
class CanvasBarrage {
    constructor(canvas, video, options = {}) {
        // 如果没有传入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 当前的 canvas 元素
        this.video = video; // 当前的 video 元素

        // 设置 canvas 与 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默认暂停播放,表示不渲染弹幕
        this.isPaused = true;

        // 没传参数的默认值
        let defaultOptions = {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
        Object.assign(this, defaultOptions, options);

        // 存放所有弹幕实例,Barrage 是创造每一条弹幕的实例的类
        this.barrages = this.data.map(item => new Barrage(item, this));

        // Canvas 画布的内容
        this.context = canvas.getContext("2d");

        // 渲染所有的弹幕
        this.render();
    }
    render() {
        // 渲染整个弹幕
        // 第一次先进行清空操作,执行渲染弹幕,如果没有暂停,继续渲染
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 渲染弹幕
        this.renderBarrage();
        if (this.isPaused == false) {
            // 递归渲染
            requestAnimationFrame(this.render.bind(this));
        }
    }
    renderBarrage() {
        // 将数组的弹幕一个一个取出,判断时间和视频的时间是否符合,符合就执行渲染此弹幕
        let time = this.video.currentTime;
        this.barrages.forEach(barrage => {
            // 当视频时间大于等于了弹幕设置的时间,那么开始渲染(时间都是以秒为单位)
            if (!barrage.flag && time >= barrage.time) {
                // 初始化弹幕的各个参数,只有在弹幕将要出现的时候再去初始化,节省性能,初始化后再进行绘制
                // 如果没有初始化,先去初始化一下
                if (!barrage.isInited) {
                    // 初始化后下次再渲染就不需要再初始化了,所以创建一个标识 isInited
                    barrage.init();
                    barrage.isInited = true;
                }

                barrage.x -= barrage.speed;
                barrage.render(); // 渲染该条弹幕
                if (barrage.x < barrage.width * -1) {
                    barrage.flag = true; // 是否出去了,出去了,做停止渲染的操作
                }
            }
        });
    }

    // ********** 以下为新增代码 **********
    add(item) {
        this.barrages.push(new Barrage(item, this));
    }
    // ********** 以上为新增代码 **********
}

10、拖动进度条实现弹幕的前进和后退

其实我们发现,弹幕虽然实现了正常的播放、暂停以及发送,但是当我们拖动进度条的时候弹幕应该是跟着视频时间同步播放的,现在的弹幕一旦播放过无论怎样拉动进度条弹幕都不会再出现,我们现在就来解决这个问题。

// 文件:index.js
// 拖动进度条事件
video.addEventListener("seeked", function() {
    canvasBarrage.reset();
});

我们在事件内部其实只是调用了一下 CanvasBarrage 类的 reset 方法,这个方法就是在拖动进度条的时候来帮我们初始化弹幕的状态。

// 文件:index.js
class CanvasBarrage {
    constructor(canvas, video, options = {}) {
        // 如果没有传入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 当前的 canvas 元素
        this.video = video; // 当前的 video 元素

        // 设置 canvas 与 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默认暂停播放,表示不渲染弹幕
        this.isPaused = true;

        // 没传参数的默认值
        let defaultOptions = {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
        Object.assign(this, defaultOptions, options);

        // 存放所有弹幕实例,Barrage 是创造每一条弹幕的实例的类
        this.barrages = this.data.map(item => new Barrage(item, this));

        // Canvas 画布的内容
        this.context = canvas.getContext("2d");

        // 渲染所有的弹幕
        this.render();
    }
    render() {
        // 渲染整个弹幕
        // 第一次先进行清空操作,执行渲染弹幕,如果没有暂停,继续渲染
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 渲染弹幕
        this.renderBarrage();
        if (this.isPaused == false) {
            // 递归渲染
            requestAnimationFrame(this.render.bind(this));
        }
    }
    renderBarrage() {
        // 将数组的弹幕一个一个取出,判断时间和视频的时间是否符合,符合就执行渲染此弹幕
        let time = this.video.currentTime;
        this.barrages.forEach(barrage => {
            // 当视频时间大于等于了弹幕设置的时间,那么开始渲染(时间都是以秒为单位)
            if (!barrage.flag && time >= barrage.time) {
                // 初始化弹幕的各个参数,只有在弹幕将要出现的时候再去初始化,节省性能,初始化后再进行绘制
                // 如果没有初始化,先去初始化一下
                if (!barrage.isInited) {
                    // 初始化后下次再渲染就不需要再初始化了,所以创建一个标识 isInited
                    barrage.init();
                    barrage.isInited = true;
                }

                barrage.x -= barrage.speed;
                barrage.render(); // 渲染该条弹幕
                if (barrage.x < barrage.width * -1) {
                    barrage.flag = true; // 是否出去了,出去了,做停止渲染的操作
                }
            }
        });
    }
    add(item) {
        this.barrages.push(new Barrage(item, this));
    }

    // ********** 以下为新增代码 **********
    reset() {
        // 先清空 Canvas 画布
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        let time = this.video.currentTime;
        // 循环每一条弹幕实例
        this.barrages.forEach(barrage => {
            // 更改已经移出屏幕的弹幕状态
            barrage.flag = false;
            // 当拖动到的时间小于等于当前弹幕时间是,重新初始化弹幕的数据,实现渲染
            if (time <= barrage.time) {
                barrage.isInited = false;
            } else {
                barrage.flag = true; // 否则将弹幕的状态设置为以移出屏幕
            }
        });
    }
    // ********** 以上为新增代码 **********
}

其实 reset 方法中值做了几件事:

  • 清空 Canvas 画布;
  • 获取当前进度条拖动位置的时间;
  • 循环存储弹幕实例的数组;
  • 将所有弹幕更改为未移出屏幕;
  • 判断拖动时间和每条弹幕的时间;
  • 在当前时间以后的弹幕重新初始化数据;
  • 以前的弹幕更改为已移出屏幕。

从而实现了拖动进度条弹幕的 “前进” 和 “后退” 功能。


使用 WebSocket 和 Redis 实现前后端通信及数据存储

1、服务器代码的实现

要使用 WebSocket 和 Redis 首先需要去安装 wsredis 依赖,在项目根目录执行下面命令:

npm install ws redis

我们创建一个 server.js 文件,用来写服务端的代码:

// 文件:index.js
const WebSocket = require("ws"); // 引入 WebSocket
const redis = require("redis"); // 引入 redis

// 初始化 WebSocket 服务器,端口号为 3000
let wss = new WebSocket.Server({
    port: 3000
});

// 创建 redis 客户端
let client = redis.createClient(); // key value

// 原生的 websocket 就两个常用的方法 on('message')、on('send')
wss.on("connection", function(ws) {
    // 监听连接
    // 连接上需要立即把 redis 数据库的数据取出返回给前端
    client.lrange("barrages", 0, -1, function(err, applies) {
        // 由于 redis 的数据都是字符串,所以需要把数组中每一项转成对象
        applies = applies.map(item => JSON.parse(item));

        // 使用 websocket 服务器将 redis 数据库的数据发送给前端
        // 构建一个对象,加入 type 属性告诉前端当前返回数据的行为,并将数据转换成字符串
        ws.send(
            JSON.stringify({
                type: "INIT",
                data: applies
            })
        );
    });

    // 当服务器收到消息时,将数据存入 redis 数据库
    ws.on("message", function(data) {
        // 向数据库存储时存的是字符串,存入并打印数据,用来判断是否成功存入数据库
        client.rpush("barrages", data, redis.print);

        // 再将当前这条数据返回给前端,同样添加 type 字段告诉前端当前行为,并将数据转换成字符串
        ws.send(
            JSON.stringify({
                type: "ADD",
                data: JSON.parse(data)
            })
        );
    });
});

服务器的逻辑很清晰,在 WebSocket 连接上时,立即获取 Redis 数据库的所有弹幕数据返回给前端,当前端点击发送弹幕按钮发送数据时,接收数据存入 Redis 数据库中并打印验证数据是否成功存入,再通过 WebSocket 服务把当前这一条数返回给前端,需要注意一下几点:

  • 从 Redis 数据库中取出全部弹幕数据的数组内部都存储的是字符串,需要使用 JSON.parse 方法进行解析;
  • 将数据发送前端时,最外层要使用 JSON.stringify 重新转换成字符串发送;
  • 在初始化阶段 WebSocket 发送所有数据和前端添加新弹幕 WebSocket 将弹幕的单条数据重新返回时,需要添加对应的 type 值告诉前端,当前的操作行为。

2、前端代码的修改

在没有实现后端代码之前,前端使用的是 data 的假数据,是在添加弹幕事件中,将获取的新增弹幕信息通过 CanvasBarrage 类的 add 方法直接创建 Barrage 类的实例,并加入到存放弹幕实例的 barrages 数组中。

现在我们需要更正一下交互逻辑,在发送弹幕事件触发时,我们应该先将获取的单条弹幕数据通过 WebSocket 发送给后端服务器,在服务器重新将消息返还给我们的时候,去将这条数据通过 CanvasBarrage 类的 add 方法加入到存放弹幕实例的 barrages 数组中。

还有在页面初始化时,我们之前在创建 CanvasBarrage 类实例的时候直接传入了 data 假数据,现在需要通过 WebSocket 的连接事件,在监听到连接 WebSocket 服务时,去创建 CanvasBarrage 类的实例,并直接把服务端返回 Redis 数据库真实的数据作为参数传入,前端代码修改如下:

// 文件:index.js
// ********** 下面代码被删掉了 **********
// let canvasBarrage = new CanvasBarrage(canvas, video, {
//     data
// });
// ********** 上面代码被删掉了 **********

// ********** 以下为新增代码 **********
let canvasBarrage;

// 创建 WebSocket 连接
let socket = new WebSocket("ws://localhost:3000");

// 监听连接事件
socket.onopen = function() {
    // 监听消息
    socket.onmessage = function(e) {
        // 将收到的消息从字符串转换成对象
        let message = JSON.parse(e.data);

        // 根据不同情况判断是初始化还是发送弹幕
        if (message.type === "INIT") {
            // 创建 CanvasBarrage 的实例添加弹幕功能,传入真实的数据
            canvasBarrage = new CanvasBarrage(canvas, video, {
                data: message.data
            });
        } else if (message.type === "ADD") {
            // 如果是添加弹幕直接将 WebSocket 返回的单条弹幕存入 barrages 中
            canvasBarrage.add(message.data);
        }
    };
};
// ********** 以上为新增代码 **********

$("#add").addEventListener("click", function() {
    let time = video.currentTime; // 发送弹幕的时间
    let value = $("#text").value; // 发送弹幕的文字
    let color = $("#color").value; // 发送弹幕文字的颜色
    let fontSize = $("#range").value; // 发送弹幕的字体大小
    let sendObj = { time, value, color, fontSize }; //发送弹幕的参数集合

    // ********** 以下为新增代码 **********
    socket.send(JSON.stringify(sendObj));
    // ********** 以上为新增代码 **********

    // ********** 下面代码被删掉了 **********
    // canvasBarrage.add(sendObj); // 发送弹幕的方法
    // ********** 上面代码被删掉了 **********
});

现在我们可以打开 index.html 文件并启动 server.js 服务器,就可以实现真实的视频弹幕操作了,但是我们还是差了最后一步,当前的服务只能同时服务一个人,但真实的场景是同时看视频的有很多人,而且发送的弹幕是共享的。

3、实现多端通信、弹幕共享

我们需要处理两件事情:

  • 第一件事情是实现多端通信共享数据库信息;
  • 第二件事情是当有人离开的时候清除关闭的 WebSocket 对象。
// 文件:server.js
const WebSocket = require("ws"); // 引入 WebSocket
const redis = require("redis"); // 引入 redis

// 初始化 WebSocket 服务器,端口号为 3000
let wss = new WebSocket.Server({
    port: 3000
});

// 创建 redis 客户端
let client = redis.createClient(); // key value

// ********** 以下为新增代码 **********
// 存储所有 WebSocket 用户
let clientsArr = [];
// ********** 以上为新增代码 **********

// 原生的 websocket 就两个常用的方法 on('message')、on('send')
wss.on("connection", function(ws) {
    // ********** 以下为新增代码 **********
    // 将所有通过 WebSocket 连接的用户存入数组中
    clientsArr.push(ws);
    // ********** 以上为新增代码 **********

    // 监听连接
    // 连接上需要立即把 redis 数据库的数据取出返回给前端
    client.lrange("barrages", 0, -1, function(err, applies) {
        // 由于 redis 的数据都是字符串,所以需要把数组中每一项转成对象
        applies = applies.map(item => JSON.parse(item));

        // 使用 websocket 服务器将 redis 数据库的数据发送给前端
        // 构建一个对象,加入 type 属性告诉前端当前返回数据的行为,并将数据转换成字符串
        ws.send(
            JSON.stringify({
                type: "INIT",
                data: applies
            })
        );
    });

    // 当服务器收到消息时,将数据存入 redis 数据库
    ws.on("message", function(data) {
        // 向数据库存储时存的是字符串,存入并打印数据,用来判断是否成功存入数据库
        client.rpush("barrages", data, redis.print);

        // ********** 以下为修改后的代码 **********
        // 循环数组,将某一个人新发送的弹幕在存储到 Redis 之后返回给所有用户
        clientsArr.forEach(w => {
            // 再将当前这条数据返回给前端,同样添加 type 字段告诉前端当前行为,并将数据转换成字符串
            w.send(
                JSON.stringify({
                    type: "ADD",
                    data: JSON.parse(data)
                })
            );
        });
        // ********** 以上为修改后的代码 **********
    });

    // ********** 以下为新增代码 **********
    // 监听关闭连接事件
    ws.on("close", function() {
        // 当某一个人关闭连接离开时,将这个人从当前存储用户的数组中移除
        clientsArr = clientsArr.filter(client => client != ws);
    });
    // ********** 以上为新增代码 **********
});

上面就是 Canvas + WebSocket + Redis 视频弹幕的实现,实现过程可能有些复杂,但整个过程写的还是比较详细,可能需要一定的耐心慢慢的读完,并最好一步一步跟着写一写,希望这篇文章可以让读到的人解决视频弹幕类似的需求,真正理解整个过程和开放封闭原则,认识到前端面向对象编程思想的美。


查看原文

xrkffgg 收藏了文章 · 1月17日

Umi Hooks - 助力拥抱 React Hooks

这是蚂蚁金服内部技术分享的文字稿,本次分享主要介绍了为什么要用 Hooks?以及如何使用 Umi Hooks 提效?

Umi Hooks http://github.com/umijs/hooks

开场

image-20200116191741370

大家好,我叫尽龙,来自体验技术部。社区名称叫 brickspert,砖家的意思。

自从 React 推出 React Hooks 后,就开始尝试使用 Hooks,并逐渐喜欢上了它。目前,几乎 100% 的组件都是使用 Hooks 开发。经过大半年的实践,在 Hooks 使用方面沉淀了一些经验,很高兴今天有机会能分享给大家。

image-20200116191759286

在分享开始之前,我想了解下:“有多少同学目前已经在项目中大量使用 Hooks 了?”

嗯嗯,谢谢。看举手的同学,大概一半一半吧。没关系,听完今天的分享,我相信你一定有兴趣尝试下 Hooks 的。

React Hooks 是 react v16.8 的一个新特性,很佩服这么重磅的功能,在一个小版本中发布,说明 React 团队有足够的信心向上兼容。

Why Hooks?

image-20200116191817331

为什么要放弃 Class,转用 Hooks 呢?在内部外部有很多争论,包括知乎也有类似提问。我们也不免俗套的要对比下 Class 和 Hooks 了。当然为了保证今天的分享效果,我肯定会偏向 Hooks 的(哈哈哈哈)。

image-20200116192324872

Class 学习成本高

Class 学习成本很高。首当其中的就是生命周期,多,太多了。不仅多,还会变!React v15 和 v16 就不一样。下面是我在网上随便找的一张图。

image-a5b927b35025

这个是 React v15 的生命周期,你都掌握了吗?你知道 v16 有什么变化吗?

之前无论你去哪里面试,基本都会有几个必问问题:

  • 讲讲 React 生命周期?React v15 和 React v16 生命周期有啥变化?
  • 如何优化 Class 组件?shouldComponentUpdate 是做什么的?如何用?
  • 一般在哪个生命周期发送网络请求?为什么?
  • ......

生命周期最重要,但是有很高的学习成本,需要大量实践才能积累足够的经验。当然,这几个问题回答不好,百分之八十以上的几率会挂掉。

当然不止是生命周期,this 也是一个很大的问题。你有没有在组件写很多 bind?或者所有的函数都用箭头函数定义?

this.someFunction = this.someFunction.bind(this);

// 或
someFunction = ()=>{}

为什么要这样写呢?如果不写会有什么问题?哎呦,又多了一个面试题,你会吗?

Hooks 学习成本低

对比 Class,Hooks 的学习成本可就太低了!掌握了 useState 和 useEffect,80% 的事情就搞定了。

image-7cbf7879e7cf

Class 业务逻辑分散

Class 业务逻辑分散,实现一个功能,我要写在不同的生命周期里面,不聚合~

比如,如果你有个定时器,你一定要在 componentWillUnMount 去卸载。

image-67b7d915f6af

再比如,我们要写一个请求用户信息的组件,当userId 变化时,要重新发起请求。我们就要在两个生命中期中写请求的逻辑。

image-4059e72aa129

相信上面的逻辑,大家也是经常会写的吧。

奥奥,sorry,上面的 componentWillReceiveProps 已经被废弃了,我们应该用 componentDidUpdate 来代替。

“咦,这是为啥呢?好好的为什么要废弃,不让这么用了?”

又来一个面试题!你知道答案吗?

Hooks 业务逻辑聚合

而 Hooks 的业务逻辑就非常聚合了。上面的两个例子,改成 Hooks 你会写吗?

image-3780dc60b735

image-88e9ba8a7add

简直不要太简单!香啊!我可以提前下班了。

Class 逻辑复用困难

说到逻辑复用,很多同学会说 Class 的 Render Props 和 HOC(高阶组件)可以做逻辑复用!那我们看看 Class 的逻辑复用有多么的惨不忍睹。

首先我们看看 Render Props。

首先我们想复用监听 window size 变化的逻辑,开开心心的写了下面的代码。

image-f9273eefa2ef

然后,我又想复用监听鼠标位置的逻辑,我只能这么写了。

image-d60b6492b570

到这里你应该看到了问题所在。这简直就是地狱!我不忍心复用其它逻辑了。

我们放过 Render Props,来看看 HOC 吧。

如果你要问什么是 HOC,那我不得不推荐我的另外一篇文章《助你完全理解React高阶组件(Higher-Order Components)》。

哪怕你不知道 HOC 是啥,你也一定用过。比如 redux 的 connect。

image-20200116200932301

上面的代码,我用了三个 HOC,分别是 redux 的 connect,react-intl 的 injectIntl,以及 AntD 的 Form.create()。

这是一个非常常见的用法。如果你光看代码,大概已经懵圈了。“我是谁?我在哪?我要干什么?”

这会我仿佛听见 HOC 在说:“我不仅让你看不懂我,我还很容易出各种问题。”

是的,HOC 很容易出问题。大家都往组件的 props 上面挂属性,万一有个重名的,那就只能说一句“不好意思,GG思密达”!

Hooks 逻辑复用简单

Hooks 来了,它表示,我要一个打五个!Render Props 和 HOC 联合起来也被我秒杀!

image-5a6f5d648ca9

Hooks 表示,来十个,来一百个我也能打。

Hooks 最强的能力就是逻辑复用了,这是我最最最爱的能力了。

Hooks 会产生很多闭包问题

是的,我也不偏袒 Hooks,由于 React Hooks 的机制,如果用法不正确,会导致各种奇怪的闭包问题。

如果你要问 React Hooks 的机制是什么的话,我又要给你推荐一篇我之前写的文章了:《React Hooks 原理》。

那面对这个问题,怎么解呢?说实话,我也没有很好的解决办法。

但是,这可能也有好处。如果碰到想不明白的问题,那 99% 是由于闭包导致的,我们有很确定的方向去排查问题。

image-4636b47be14f

记住这句话,你可以少走很多弯路。

Show Case

image-20200116203233594

当然,说再多,吹再好,也没多大用。我上面讲的 Class 和 Hooks 的优缺点,网上的也有很多人讲,大家也肯定都看过。

用程序员的交流方式,就是“Talk is cheap,Show me the code.”。

亮剑吧!

接下来,我会用一个例子,让你折服,拜倒在 Hooks 的石榴裙下。如果你不服,咱们单独撕~

网络请求组件实现

image-20200116214124140

接下来,我们来实现一个最最最常见的组件。该组件接收 userId,然后发起网络请求,获得用户信息。

说白了,就是最简单的发起网络请求的组件。我们先用 Class 来实现看看。

image-20200116214639300

这段代码,是最简单的网络请求。

  • 定义一个 username 状态。
  • componentDidMount 的时候发起网络请求。
  • 网络请求结束,更新 username。

美滋滋。但是少了点东西。网络请求,我们肯定要维护一个 loading 状态,保证用户体验比较好。

那我们加上吧。

image-20200116214918755

这张图,我们增加了 loading 状态,在网络请求发起前,置为 true,在网络请求结束后,置为 false。

美滋滋。但是还是少点东西。userId 变化后,我要重新发起网络请求吧。

我们再加点代码吧。

image-20200116215101730

我们增加了对 userId 变化的监听,如果 userId 变化后,重新发起请求。

这次稳了吧?

不不不,还不够。如果 userId 连续变化了 5 次,发送了 5 个网络请求,我们要保证总是最后一次网络请求有效。也就是经常说的的“竞态处理”或者“时序控制”。

我加!加还不行吗!

image-20200116215409524

其实到这里,有些同学已经懵了。“你说的时序控制,听着很有道理,但我平时都没处理过这个问题,我看下你怎么实现的。”

确实,时序控制不算一个简单的问题,很多新手都不会解决这个问题。

稳了!到这里你觉得稳了吧。

还是年轻啊,小伙子。

image-20200116220003295

如果用上面的代码来玩,你可能会偶尔碰到上面的警告。这个警告是怎么造成的呢?我说一下你就明白了。下面四个步骤执行,必会报警告:

  1. 组件加载
  2. 发起网络请求
  3. 组件卸载
  4. 网络请求请求成功,触发 setState

看出问题了吗?组件已经卸载了,还去 setState,造成了内存溢出。

怎么解决呢?

image-20200116220311200

在组件卸载的时候,放弃最后一次请求。

到这里为止,我们就完成了一个完美的网络请求。这次真结束了!

看下写了多少行代码。

image-20200116220531399

除去空格,我们写了 38 行代码。实话说,38 行代码我能忍,但是这些逻辑我忍不了!回想下我们处理了多少逻辑:

  • 网络请求
  • loading
  • userId 变化重新发起请求
  • 竞态处理
  • 组件卸载放弃网络请求

关键这些逻辑是没办法复用的!每个项目可能有数十上百个组件会发网络请求,我就要写几十,几百遍这样的逻辑。想想我都难受。

说实话,我在写项目的时候经常会偷懒。要不就不写 loading,要不就不管竞态,要不就不管最后的内存溢出警告。

你有没有和我一样呢?嘿嘿。

言归正传,接下来就邀请 Hooks 登场了。

image-20200116221123031

三下五除二,我们用 Hooks 实现了刚才所有的逻辑。

image-20200116221212124

17 行!代码量减少了 50% 以上。好像还行!

但是,别忘了,Hooks 最重要的能力就是逻辑复用!这些逻辑我们完全可以封装起来!我们把刚才的逻辑全部封装起来!

image-20200116221359407

useAsync 封装了刚才我们说的所有功能,一行代码完成了网络请求。

最后整个组件会长这样。

image-20200116221605411

哇!我自己都佩服自己!简直了!美呆了,帅毙了,感觉自己无敌了!提前完成工作,下班回家!

image-20200116221755193

通过这个例子,我想证明一个论点:“使用 Hooks 封装逻辑的能力,可以极大提高开发效率”。

Umi Hooks

这时候你肯定要问,useAsync 在哪里?给我瞧瞧?

image-20200116221941442

useAsync

useAsync 在这里,快来瞧,快来看啦!

useAsync 是 Umi Hooks 库的核心 Hooks 之一,Umi Hooks 提供了大量提炼自业务的 Hooks。一行代码真的可以实现很多功能!

Umi Hooks 在这里在这里!你懂的~~

image-20200116222352082

当然,useAsync 不止包含上面说的功能,还支持“手动触发”执行,还支持“轮询”!

只要简单的配置一个 pollingInterval ,就能轮询发送请求了。快去试试啦

接下来我们会介绍几个更牛的 Hooks 给大家认识!

useAntdTable

image-cc2f4b087aca

AntD 的 Table 组件,想必大家在项目中经常用到吧!除了刚才异步请求的所有逻辑外,你还得处理其它的逻辑。

image-20200116232857059

比如维护 page、pageSize、sorter、filter 的状态,还得考虑搜索条件变化后,重置 page 到第一页。这些逻辑光想想就头疼了,别说写了。

现在一行代码就可以实现了!useAntdTable,封装了所有的逻辑,只要一行代码!如图上所示,你只要 ...tableProps,就可以了。这也许就是幸福的味道吧~

useLoadMore

加载更多的场景,比如下面动图的场景,想必大家在工作中都写过。

image-22fa47992b6f

这样一个加载更多的场景,我们要维护多少状态?写多少行逻辑?本来我打算写个 Class 实现的例子贴出来的,但是我放弃了,因为太难了~~

随便想想要处理的逻辑:

  • 第一次加载时候的 loading
  • 加载更多时候的 loading
  • 维护 page 和 pageSize
  • 网络请求
  • 是不是加载全了
  • 搜索条件变化后,重置到第一页。
  • .....

脑壳疼,真的脑壳疼。我会写,但是写起来真的好累。

还没完,一般产品同学还会要求,上拉加载更多......

image-cf629db68ebc

这时候我们还得监听滚动位置,如果快到底了,触发加载更多。脑壳更疼了!

image-20200116235013101

Umi Hooks 听到了你的求救,派出 useLoadMore 来拯救你了。一行代码就可以实现所有的功能!一个小时变一分钟,又可以早点下班了。

useDynamicList

image-20200116235455431

还有更好用的,比如 useDynamicList,下面的动态列表,一行代码搞定。

image-16556dfcf0e8

useBoolean

不仅是上面讲到的各种复杂逻辑可以封装。简单的逻辑封装起来也是极其好用的,比如 Boolean 值的管理。

我们一般控制 Modal,Popover 等显示隐藏的时候,都要维护一个 visible 状态。大概会是这样。

image-20200116235651552

这样的逻辑,你写过多少遍?没有几千也有几百吧!

image-20200116235850057

以后你就可以用 useBoolean 咯!

More

image-20200116235957789

不仅是上面讲到的这些,我们还有很多很多的 Hooks。

比如 useSearch,就封装了通常异步搜索场景的逻辑,比如 debounce。

比如 useVirtualList,就封装了虚拟列表的逻辑。

image-20200117000249183

比如 useMouse,封装了监听鼠标位置的逻辑。

比如 useKeyPress,封装了监听键盘按键的逻辑。

image-20200117000412951

30+ Hooks 供您选择,并且我们仍然处于婴儿期,快速发展中。我们的愿景就是:封装别人的逻辑,让别人无逻辑可写。

未来规划

image-20200117001130397

更多的 Hooks 开发

如上面所述,我们现在还处于婴儿期,需要不断汲取能量,更多的 Hooks 正在路上!要实现“让别人无逻辑可写”的目标,还需继续奋斗。

更强大的 useRequest

image-20200117001209280

大家应该都听过 useSWR 吧?是 zeit 公司开发的一个专门做网络请求的 Hooks,提供了很多新颖的思路,给了我们非常大的启发,github star 就像坐火箭一样。但在实际项目使用中,还是会有很多地方不符合蚂蚁内部的体系。但是它给我们非常大的启发,基于 swr 的思路,我们可以实现更强大的 useRequest!图上的能力,我们都要!

useRequest 目前已经处于内测期了,下个版本将会与大家见面!我们的目标是:所有的网络请求,只用 useRequest 就够了!

Hooks 生态

目前社区上 Hooks 相关的基础教程、进阶教程、原理深入、常见问题等文档都比较分散,我们准备向 Hooks 生态发展,提供各式各样的文章。以后学习 Hooks,使用 Hooks,找 Umi Hooks 就对了。

当然,生态方面目前正在规划中,预计年后启动。

总结

image-20200117001711649

Umi Hooks,你值得拥有。

我们目前处于发展阶段,欢迎大家一起共建。

你可以提 idea,我们负责实现。

你可以提 issue,我们负责改 bug。

你可以提 PR,将你封装的 Hooks 分享给大家,让更多人收益。

❤️期待您的参与。

查看原文

xrkffgg 回答了问题 · 1月10日

element-ui calendar 点击事件怎么禁用?

改源码,自己单独引用

关注 5 回答 2

xrkffgg 回答了问题 · 1月7日

element-ui中的el-date-picker组件中某一日期上方多渲染一个字?

官网例子也没看到啊

关注 6 回答 4

xrkffgg 回答了问题 · 1月7日

elTree好多坑啊

没遇到过,不知道你是怎么实现的

关注 3 回答 2

xrkffgg 发布了文章 · 1月7日

Pug 介绍和在 Vue 中使用

1 介绍

pug 是一种前端模板引擎,原名 jade

可用来生成 HTML,它的写法类似于 CSS

中文文档

这里先简单举几个 🌰

#hello
<div id="hello"></div>
a.link-button Link
<a class="link-button">Link</a>
a(href="https://xrkffgg.github.io/Knotes/") 我的网站
<a href="https://xrkffgg.github.io/Knotes/">我的网站</a>

易理解,同时极大的简约了我们的代码。

2 安装

2.1 下载

npm i -D pug pug-html-loader pug-plain-loader
# or
yarn add pug pug-html-loader pug-plain-loader

2.2 配置

// vue.config.js
module.exports = {
    chainWebpack: config => {
      config.module.rule('pug')
        .test(/\.pug$/)
        .use('pug-html-loader')
        .loader('pug-html-loader')
        .end()
  }
}

2.3 使用

<template lang="pug">
    div.hello
        h1 Hello World
</template>

3 实践

3.1 举例

下面将拿出实际项目中的一些代码进行改造

  • 原代码
20 行,所有标签完全闭合
<template>
  <el-card shadow="never" class="aui-card--fill">
    <div class="mod-sys__dept">
      <el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
        <el-form-item>
          <el-button type="primary" @click="addOrUpdateHandle()">{{ $t('add') }}</el-button>
        </el-form-item>
      </el-form>
      <el-table v-loading="dataListLoading" :data="dataList" row-key="id" border style="width: 100%;">
        <el-table-column prop="name" :label="$t('dept.name')" header-align="center" min-width="150"></el-table-column>
        <el-table-column prop="parentName" :label="$t('dept.parentName')" header-align="center" align="center"></el-table-column>
        <el-table-column prop="sort" :label="$t('dept.sort')" header-align="center" align="center" width="80"></el-table-column>
        <el-table-column :label="$t('handle')" fixed="right" header-align="center" align="center" width="150">
          <template slot-scope="scope">
            <el-button type="text" size="small" @click="addOrUpdateHandle(scope.row.id)">{{ $t('update') }}</el-button>
            <el-button type="text" size="small" @click="deleteHandle(scope.row.id)">{{ $t('delete') }}</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </el-card>
</template>
  • 改造后
13 行,标签完全简化
<template lang="pug">
  el-card.aui-card--fill(shadow="never")
    .mod-sys__dept
      el-form(:inline="true" :model="dataForm" @keyup.enter.native="getDataList()")
        el-form-item
          el-button(type="primary" @click="addOrUpdateHandle()") {{ $t('add') }}
      el-table(v-loading="dataListLoading" :data="dataList" row-key="id" border style="width: 100%;")
        el-table-column(prop="name" :label="$t('dept.name')" header-align="center" min-width="150")
        el-table-column(prop="parentName" :label="$t('dept.parentName')" header-align="center" align="center")
        el-table-column(prop="sort" :label="$t('dept.sort')" header-align="center" align="center" width="80")
        el-table-column(:label="$t('handle')" fixed="right" header-align="center" align="center" width="150")
          template(slot-scope="scope")
            el-button(type="text"
                      size="small"
                      @click="addOrUpdateHandle(scope.row.id)") {{ $t('update') }}
            el-button(type="text"
                      size="small"
                      @click="deleteHandle(scope.row.id)") {{ $t('delete') }}
</template>

el-button 由于属性过长,使用了分行

4 特性

4.1 支持

  • 支持 Vue:@
  • 支持 ES6 模板字符串

4.2 属性

如果一个标签有多个属性,可使用 分行 或 逗号

// 1
el-button(v-if="ifShow" type="size" size="small" @click="doClidk") 点击

// 2
el-button(v-if="ifShow",type="size",size="small",@click="doClidk") 点击

// 3
el-button(v-if="ifShow"
                  type="size"
                  size="small"
                  @click="doClick") 点击

4.3 注释

  • 单行
// 一些内容
p foo
p bar

<!-- 一些内容 -->
<p>foo</p>
<p>bar</p>
  • 不输出注释
//- 这行不会出现在结果中
p foo
p bar

<p>foo</p>
<p>bar</p>
  • 块注释
body
    //
        一堆
        文字
        进行中

<body>
    <!-- 一堆
         文字
         进行中 -->
</body>

4.4 符号

  • 管道文字( | ):向模板添加纯文本
p
  | 管道符号总是在最开头,
  | 不算前面的缩进。

<p>管道符号总是在最开头, 不算前面的缩进。
</p>
a ……用一个链接结束的句子
| 。

<a>……用一个链接结束的句子</a>。
| 千万别
|
button 按
|
| 我!

千万别
<button>按</button> 我!
  • #[ ] 标签嵌入
p.
  这是一个很长很长而且还很无聊的段落,还没有结束,是的,非常非常地长。
  突然出现了一个 #[strong 充满力量感的单词],这确实让人难以 #[em 忽视]。
p.
  使用带属性的嵌入标签的例子:
  #[q(lang="es") ¡Hola Mundo!]

<p>这是一个很长很长而且还很无聊的段落,还没有结束,是的,非常非常地长。 突然出现了一个 <strong>充满力量感的单词</strong>,这确实让人难以 <em>忽视</em>。</p>
<p>使用带属性的嵌入标签的例子:
  <q lang="es">¡Hola Mundo!</q></p>
p
  | 如果我不用嵌入功能来书写,一些标签比如
  strong strong
  | 和
  em em
  | 可能会产生意外的结果。
p.
  如果用了嵌入,在 #[strong strong] 和 #[em em] 旁的空格就会让我舒服多了。

<p>如果我不用嵌入功能来书写,一些标签比如<strong>strong</strong>和<em>em</em>可能会产生意外的结果。</p>
<p>如果用了嵌入,在 <strong>strong</strong> 和 <em>em</em> 旁的空格就会让我舒服多了。</p>
  • .
// 大文本块
p.
  使用常规的标签可以让您的代码行短小精悍,
  但使用嵌入标签会使代码变得更 #[em 清晰易读]。
  ——如果您的标签和文本之间是用空格隔开的。

<p>使用常规的标签可以让您的代码行短小精悍, 但使用嵌入标签会使代码变得更 <em>清晰易读</em>。 ——如果您的标签和文本之间是用空格隔开的。
</p>
// 纯文本块
div
  p This text belongs to the paragraph tag.
  br
  .
    This text belongs to the div tag.

<div>
  <p>This text belongs to the paragraph tag.</p><br/>This text belongs to the div tag.</div>
  • : 块展开
a: img

<a><img/></a>
  • / 自闭和标签
foo/
foo(bar='baz')/

<foo/>
<foo bar="baz" />

5 后 记

感谢支持。

若不足之处,欢迎大家指出,共勉。

如果觉得不错,记得 点赞,谢谢大家 ʚ💖ɞ

欢迎关注。

5.1 原文地址

https://xrkffgg.github.io/Knotes/blog/15.html

查看原文

赞 0 收藏 0 评论 0

xrkffgg 赞了回答 · 1月1日

html5仿 App Store 首页 Toady 转场效果 插件?

关注 1 回答 1

xrkffgg 回答了问题 · 2019-12-31

vue的项目,本来想打包一下,使用yarn build报错,不知道什么原因,求大佬解答

看看你的 webpack.base.conf.js 文件

关注 2 回答 1

xrkffgg 回答了问题 · 2019-12-30

解决怎么让el-table-column里的label表头做页面跳转?

<el-table-column>  
    <template slot="header" slot-scope="scope">  
        <div @click="跳转方法">点击跳转</div>
    </template\>
</el-table-column>

关注 2 回答 1

xrkffgg 回答了问题 · 2019-12-24

奇怪问题 input的绑定值

v-model 定义变量的

你这个data每一项都是 数字啊

关注 4 回答 3

认证与成就

  • 认证信息 前端工程师
  • 获得 104 次点赞
  • 获得 11 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 10 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • Tools

    记录工作、生活中使用到的各种工具,如JS、收藏夹、文章等等。。。

注册于 2017-07-14
个人主页被 954 人浏览