从零制作视频播放器——别人能搞得出来视频控件,难道我们不能搞?(第三章)

1. 前言

书接上回,前几篇文章我们大都在谈规划,当然后面我们还会谈规划,不过没有前面几篇文章谈的那么多,废话不多说,接下来我们进入正文。

在这篇文章中我们会进行视频控件的编写,不过实现细节很多,后面会再撰写几篇文章细讲。

实现一个功能我们要尽可能先将简单的功能实现,较难的功能放到较后来实现。所以这篇文章要讲的功能实现是视频控件面板的显示和隐藏

另外,各位看客的点赞与收藏就是我源源不断的动力,有兴趣的可以关注该专栏,如果有合适的功能需求,专栏也会进行更新。为了大家,爆发吧小宇宙!!!╰(‵□′)╯

2. 分析

相信大多数人都能快速的写出控件的显示和隐藏的代码,所以这里也就不啰嗦了。不过开始前我们得先规划一下。

控件什么时候显示?

  • 当视频正在播放,并且鼠标在视频播放器中发生了移动,即触发了 mousemove 事件时。
  • 当鼠标移入控件面板并且未移出时。
  • 当视频暂停时。(这个可加可不加,因为控件显示时会遮挡内容,而用户暂停可能就是为了看一下视频内容)

控件什么时候隐藏?

  • 当视频播放时,并且鼠标未发生移动,也不在控件面板中时,会过一段时间自动隐藏。

下面展示了鼠标移动时显示控件面板。

2.1. 实现

下面我们编码顺序是先写页面,再写逻辑。当然这个顺序并不是必须的,只要你觉得哪种顺序你能更快地、更清晰地实现功能,那就是适合你的顺序。

2.1.1. 编写页面

下面是相关的 HTML 代码。其中 class 值为 video-controls 的 div 就是控件面板元素。

html 复制代码
<div class="video-container">
    <div class="video-instance">
        <video src="./spy.mp4" controls></video>
    </div>
    <div class="video-controls"></div>
</div>

下面是 video-controls 的 css 样式。其他元素的样式在上篇文章有提过,这里就不再赘述了。

css 复制代码
.video-controls {
    position: absolute;
    z-index: 2;
    bottom: 0px;
    width: 100%;
    height: 65px;
    background: -webkit-linear-gradient(top, #ffffff00, #000000db);
}

具体的效果如下图所示:

好了,页面代码是非常简单的,下面我们就开始书写逻辑代码了。

2.2.2. 逻辑实现

下面我们将会涉及到比较深入的问题了,先来分析一下。

  1. 控件面板显示和隐藏需不需要动画过渡?如果需要使用什么动画?如何实现?
  2. 控件面板显示和隐藏时是否也需要执行相关的回调函数?
  3. 鼠标如果持续移动是否需要进行限制?如果限制,使用防抖还是限流?

通过以上几个简短的问题,我们能大致理清控件面板显示和隐藏的实现方向。下面是对于上面问题的回复:

  1. 我们的控件面板显示和隐藏将会使用 渐现、渐隐 的动画过渡,主要通过元素的 opacity 属性来实现,然后通过 display 来控制元素的显示和隐藏。

  2. 我们无法确定控件显示和隐藏是否会 执行相关的回调函数,对于这种不确定项,可加可不加,当然为了提高代码的可维护性,可以实现相关的逻辑。当然之后你可以看到我们是会加上的,因为确实会用到。

  3. 由于在视频播放时我们无法确定用户的鼠标何时会移动,并且在用户的鼠标移动时我们要保证控件一定得显示出来。如果不加限制,很明显可以是可以,但是频率太高,就算电脑吃得消,也是在浪费性能,所以我们需要加以限制。那么使用限流还是防抖呢?如果使用防抖,可是防抖是在用户持续触发结束后的最后一次才执行,由于我们只是限制执行次数,限流是在一段时间内执行一次,很明显限流符合我们的要求。

分析完毕,接下来就是编写逻辑代码了。还是先从简单的开始吧!我给上面的实现难度进行了排序:

难度顺序:执行相关的回调函数 < 操作限流 < 渐现、渐隐过渡动画。

大家可能对我给出难度排序有疑问,动画过渡有什么难的,没事,很正常,因为刚开始我也是这么觉得,哈哈~

2.2.2.1. 执行相关的回调函数

代码实现比较简单,所以我就不解释了哈 = ̄ω ̄=

其中 window.execCallback 是一个通用的函数,后面还会用到,大概知道它是做什么的就行了。

js 复制代码
"use strict";

/**
 * 显示控件后执行的回调
 * @type {[function]}
 */
const showControlsCallbackArr = [];
/**
 * 隐藏控件后执行的回调
 * @type {[function]}
 */
const hideControlsCallbackArr = [];

/**
 * 执行回调函数
 * @param callbackArr {function[]} 回调函数数组
 * @param args {...*} 传入函数的形参
 */
function execCallback(callbackArr, ...args) {
    callbackArr.forEach((fn) => {
        fn(...args);
    });
}

// 在控件面板显示时我们会运行这段代码:
window.execCallback(showControlsCallbackArr);

// 在控件面板隐藏时我们会运行这段代码:
window.execCallback(hideControlsCallbackArr);

2.2.2.2. 操作限流

相信限流的代码大家已经烂熟于心了吧,其实我还是记不太清,记性不好,所以一般好久没用还会去网上搜索 js 限流。骗你们的,其实我是为了给别人的文章增加热度,哈哈~

对于限流,我认为有两种方式,区别大同小异:window.setTimeout()window.performance.now() 这两个 API 都可以实现限流。我使用的是第二种,因为它更准确!

  1. window.setTimeout 实现限流,亲测有效。
js 复制代码
function getThrottle(fn, delay) {
    let timer = null;
    return function() {
        if (!timer) {
            timer = window.setTimeout(() => {
                fn.apply(this, arguments);
                timer = null;
            }, delay);
        }
    }
}

let throttle = window.getThrottle(() => {
    console.log("running");
}, 1000);

for(let i = 0; i < 1000; i++) {
    throttle();
}
  1. window.performance.now() 实现限流,同样亲测有效。
js 复制代码
/**
 * 获取节流函数
 * @param fn {function} 要节流的函数
 * @param delay {number} 延迟执行的时间, 单位是毫秒
 * @return {function(...*): *}
 */
function getThrottle(fn, delay) {
    let now, last;
    return function (...args) {
        now = performance.now();
        if (!(last && now < last + delay)) {
            last = now;
            return fn && fn(...args);
        }
    }
}

不知有没有看客不知道 为什么要返回一个函数 的,这里我多嘴一句,因为我们在其他地方可能还会使用限流,这种方式使用闭包封装了一个私有变量,而 getThrottle 方法每次返回的函数都是不同的,它们之间互不影响。这样我们就能多次使用限流逻辑而不必再编写重复代码了。

同时这里我并没有使用 fn.apply(this, args); 而是使用 fn(...args); 。下面是我的看法:

通常 getThrottle 方法都书写在全局环境中,即 window.getThrottle() 可以调用,所以我认为如果 getThrottle 返回的函数(fn)如果不添加在一个对象(obj)上,在严格模式下 this 的值将始终为 undefined,而通常我们如果能执行这个返回的函数(fn),就也能获取到它所处的那个对象(obj),也就是 this 有点冗余的意思了。

另外需要注意了,如果你在严格模式的全局环境下直接调用函数,this 是 undefined,非严格模式下是 window。具体的原因是严格模式下 this 不会进行默认绑定。举个例子让大家更易懂一点。

js 复制代码
// 处于非严格模式
// "use strict";

// 比如我有个 fn 函数 
function fn() {
    console.log(this);
}

fn(); // 输出 window 对吧?对的!
window.fn(); // 输出 window 对吧?也是对的!

再来个严格模式的例子:

js 复制代码
// 处于严格模式
"use strict";

// 再来个 fn 函数,fn:好的主人!
function fn() {
    console.log(this);
}

window.fn(); // 输出 window 对吧?还是对的!
fn(); // 输出 window 对吧?这里就不对咯!输出 undefined

通过上面的例子我们应该就知道为什么执行 fn(); 时为什么 this 还会是 window 了,是因为非严格模式 this 会进行默认绑定到 window。简而言之就是别人替我们做了,别人替我们负重前行对吧?

如果有跟不上的小伙伴听好了,现在我们还处于打地基的阶段,需要循序渐进,把基本的功能写好,最后慢慢整合,功能自然就实现了!所以跟不上的小伙伴先别慌,放轻松,你都已经这么努力看到这里了,天道酬勤。后面的内容会上点难度,倒不是难在理解,而是难在细心,因为我们会开发一个通用型的渐现、渐隐动画。

好了,扯得有点远了,收!

2.2.2.3. 渐现、渐隐动画过渡

相信大家都遇到过这样的渐变过渡动画制作,大家是如何解决的呢?使用 JQuery 的 $(el).fadeIn(); ?还是 vue 提供的 transition 组件?

如果让你去实现下面这样的效果,你会怎么实现?

看起来很简单对吧?可以自己尝试一下,其中的细节还是比较多的。

我也使用了 JQuery 和 Vue 去实现这个效果,发现纯靠添加或删除类名是实现不了这个效果的,所以 Vue 的 transition 的这个方法是不可行的,如果有小伙伴使用 transition 实现了,请在评论区留言分享一下。亲测 JQuery 是可以实现的,而且代码挺简单的,所以这个效果以后可以使用 JQuery 来快速开发。

我们最终要实现的效果大概就是上面图片的那个样子,可以看到这个透明度的渐变还是需要一定的通用性,因为页面上许多地方都要用到。下面我们会尝试用各种方法去尽可能的实现它,讨论对应的方法是否可行,以及它的优劣。

这里列出了几种方法,我们会尝试下面这几种方式去实现:

  • 通过鼠标 hover 去实现。
  • 通过动画帧 animation 去实现。
  • 通过 js 控制动画。
2.2.2.3.1. 通过鼠标 hover 实现

刚开始时我也是觉得这个实现起来应该挺简单的,因为我第一想到的就是使用 hover 伪元素去实现,开始时我还兴致勃勃的去写了好多代码,觉得马上就写好了,但是写到最后发现了一些问题,接下来我们细说。

注意:下面简单的代码就不给出了,因为本文章篇幅有点长了。所以各位看客在看的时候最好是能进行相应的编码尝试,不然你的理解可能会出现歧义。下面的代码都是实践出来的,所以自行编码会理解的更快。

这里我画了一张简图,通常我们都是希望鼠标移入触发元素然后展示元素渐现,移出则渐隐。按道理 hover 应该能完全胜任才对。我也曾这样想过,还亲自做过😂。(所以我为大家排雷来了,哈哈~)

当你写完了,你会发现你又想把之前写的代码给删了,为什么?我们不仅想让设置元素的 opacity 属性,还想设置它的 display 属性为 none 让它滚蛋,但是直接在 hover 里面加的话,过渡并不能生效,因为 none 不是数值,无法过渡。

当然你可能也会立马说 hover 与 js 事件结合起来怎么样?当然我之前并没有尝试过,这是我写这篇文章的时候想到的,所以说帮助大家学习也是帮助我自己学习啊!

具体的思路是:hover 的那部分代码留着,同时我们给展示元素添加一个 transitionend 事件,当过渡结束时设置它的 display 就行了,我说不能实现不是不能实现,得让我们的水平来发话哈。

好,这段代码我就贴出来了,下面展示了一部分哈,其他的自己脑补~

html 复制代码
<style>
    .container {
        position: relative;
        width: 200px;
        height: 200px;
        background-color: #dfdf33;
    }

    .container:hover .box {
        opacity: 1;
    }

    .box {
        position: absolute;
        top: 0px;
        right: -120px;
        opacity: 0;
        width: 100px;
        height: 100px;
        background-color: #3344ff;
        transition: all 1500ms;
    }
</style>


<div class="container">
    <div class="box"></div>
</div>

<script>
    const container = document.querySelector(".container");
    const box = document.querySelector(".box");

    let show = false;

    box.addEventListener("transitionend", () => {
        let computedStyle = window.getComputedStyle(box);

        if(computedStyle.style.display !== "none") {
            // 这里已经无从下手了
        }
    });
</script>

看完了是不是感觉写的好好的,然后就看到 "无从下手" 了?原因是我们无法确定当前的 transitionend 到底是隐藏元素还是显示元素,可以看到我还试图通过定义一个 show 变量来实现,但是还是于事无补。

其实这里还可以在这个 show 变量上下文章,下面是我思索片刻写出的代码,用来彻底断了大家使用 hover 的念头,哈哈~ 坏人我做定了~~

html 复制代码
<style>
    .container {
        position: relative;
        width: 200px;
        height: 200px;
        background-color: #dfdf33;
    }

    .container:hover .box {
        opacity: 1;
    }

    .box {
        position: absolute;
        top: 0px;
        right: -120px;
        opacity: 0;
        width: 100px;
        height: 100px;
        background-color: #3344ff;
        transition: all 1500ms;
    }
</style>


<div class="container">
    <div class="box"></div>
</div>

<script>
    const container = document.querySelector(".container");
    const box = document.querySelector(".box");

    let enter = false;

    container.addEventListener("mouseenter", () => {
        enter = true;

        let computedStyle = window.getComputedStyle(box);
        // box 的类样式(不是行样式哈)中 display 可能为 flex 或者是其他值
        box.style.display = "";
        document.documentElement.clientHeight;

        computedStyle = window.getComputedStyle(box);
        if(computedStyle.display === "none") {
            // 如果还不显示就上大招了
            computedStyle.display = "block";
        }
    });

    container.addEventListener("mouseleave", () => {
        enter = false;
    });

    box.addEventListener("transitionend", () => {
        if(!enter) {
            box.style.display = "none";
        }
    });
</script>

不知道大家伙们能看得懂增加的这些代码不,如果没看懂的小伙伴可以在评论区求助~~ 救救我~~~ ,也可以私信我,因为后面还会讲到类似的代码。这里的代码已经有最终代码的雏形了嘿,坚持就是胜利!这篇文章确实写得有点长了诶,我也写累了😅

说一下上面的代码为什么不行,由于口说无凭,各位可以复制上面的代码去试试,能发现它在显示元素时,透明度突然变化为 1,没有过渡,与在 hover 的类样式中直接设置 display: block 无异。大概原因也许是 hover 比 mouseenter 事件先执行吧,顺序反一下就能用了。

到这里我们就能淘汰掉这个 hover 方法了,主要就是因为 display 无法过渡的限制。

下面和各位说一下该方法的优劣点:

优点如下:

  • 它能在不添加 js 事件的基础上实现元素的透明度变化。
  • 它能在鼠标移出并快速移入时,保证元素透明度不突然变化。

缺点如下:

  • 它会由于 display: none 导致过渡失效。
2.2.2.3.2. 通过动画帧 animation 实现

通过动画帧实现,这个方法其实刚想到就能淘汰掉了,这里我就说一下不采用它的原因吧。因为它只能从一个状态到另一个状态,中间不能打断,而我们需要能打断。

不能打断的话我们就不能修改 animation 的值,无法让透明度缓慢变化。

2.2.2.2.3. 通过 js 实现

我们最终采用的方法就是这个方法哈,所以接下来就是重头戏了。敲黑板了!!转下篇文章。

从零制作视频播放器------别人能搞得出来视频控件,我们能搞得更好!(第四章)

3. 中场休息

篇幅太长了,差几百个字快一万了,虽然其中有代码和扯蛋的内容,不过这么冷的天,家里又没暖气,我得不时的对手哈气让手暖和一些。这里就进行掐断了哈,当然两篇文章是同时发的,不用担心没下文。

各位家财万贯的老板!你的点赞和收藏就是我源源不断的动力,感兴趣的可以关注一下该专栏,有对人家感兴趣的也可以关注我嘛~ 😋 文章没热度更新频率就低了哈~ 我这个人不开玩笑的哈~~ 😁🤞😘

相关推荐
腾讯TNTWeb前端团队2 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰5 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪5 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪5 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy6 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom6 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom7 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom7 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom7 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom7 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试