产品经理:那谁不在,你来做一下吧

背景

产品经理前段时间给官网首页轮播图加上了视频播放的feature,不过因为还要支持自动轮播,所以每次视频都放不完就切走了。因为之前急着上线,当时的解决方案是首页关掉了自动轮播,只能用户手动切换。最近新的迭代需求,产品又把这个提了出来,需要优化:

  1. 视频文件播放完,切换至下一个Slider展示;
  2. 静态图片固定展示3s后切至下一个Slider
  3. 依然支持鼠标移入后的暂停轮播功能。

而且还急着上线,排在了T0优先级。结果当初做支持视频播放功能的前端同学这两周请假了,这活落在了我的头上😅。想想当初我还嘲笑那位同学被安排了这么个活,现在回旋镖打到自己身上了🫤。

调研

首先我想着能不能找到现成的完美支持我们需求的轮播图组件,不过我简单搜索了一下,并没有找到合适的开源组件;考虑了一下,要么装个Swiper.js手搓,要么在现有正在使用的Ant Design组件库的Carousel组件上手搓了。既然都手搓,还是用现成的吧🤣。

分析

对需求进行了一下分析 :

  1. 视频文件播放完,切换至下一个 Slider 展示。这个实现起来不难,主要有以下几种思路:

    • 后端返回视频资源路径的同时,返回该视频资源的时长,前端手动给对应的Slider实现相应的播放时长,需要后端接口改动;

    • 前端拿到视频资源,手动加载获取视频的时长,接着和之前一样实现对应的播放时长,需要加载整个视频才能拿到;

    • 给视频加上onEnded事件监听,准确获取视频播放结束时刻,进行后续操作。

    隐性功能:切换到下一页之前,这一页的视频要停止播放;下次展示该视频时,需要重新播放。

  2. 静态图片固定展示 3s 后切至下一个 Slider。这个比较简单;

  3. 依然支持鼠标移入后的暂停轮播功能 。看起来比较简单,Carousel组件本身就支持pauseOnfocused

    隐性功能:视频页在pauseOnfocused时,需要轮播,而不是播放完一次就暂停不播了。

开发

  1. 视频文件播放完,切换至下一个 Slider 展示 。因为优先考虑前端单独实现且重视性能,所以直接采用video标签的onEnded事件属性方案。

    • 首先确定手动播放视频的方法:

      ts 复制代码
      // 视频播放方法
      const playVideo = (ele: HTMLVideoElement) => {
        if (ele?.paused)
          try {
            ele?.play()
          } catch (_err) {
            ele?.play()
          }
      }
    • 在视频播放时,将Carousel组件的autoplay属性改为false,播放结束后再改为true,且暂停视频播放:

      ts 复制代码
      // 视频播放
      const playVideo = (ele: HTMLVideoElement) => {
        if (ele.paused){
          setAutoplay(true)
          try {
            ele.play()
          } catch (_err) {
            ele.play()
          }
        }
      }
      // 视频结束
      const handleEnded = (e: any) => {
        e.target.pause();
        setAutoplay(true);
      }
    • 发现修改Carousel组件的autoplay属性,会导致整个组件重新渲染,内部的视频标签也会重新渲染导致闪烁。所以给内部的媒体组件加上React.memo()

      tsx 复制代码
      const MedaNode = memo(({
        video,
        imageUrl,
        index,
        handleEnded
      }: MedaProps) => {
        // 视频
        if (video) {
          return <video
            src={imageUrl} autoPlay muted preload="auto" disablePictureInPicture
            id={`video-${index}`}
            onEnded={handleEnded}
          />
        }
        // 静图
        return <img src={imgUrl} />
      })
      
      export default Banner ({ bannerList }: BannerProps) {
        // ...
        const handleEnded = useCallback((e: any) => {
          e.target.pause();
          setAutoplay(true)
        }, [])
        // ...
        return (
          <Carousel>
             {list?.length &&
               list.map((item, index) => {
                 const { id, imageUrl, linkUrl, video } = item;
                 return (
                   <div
                      key={id}
                      onClick={() => linkUrl && Router.push(linkUrl)}
                    >
                      <MedaNode 
                         video={video} 
                         imageUrl={imageUrl} 
                         index={index} 
                         handleEnded={handleEnded} 
                       />
                    </div>)
                })}
          </Carousel>)
      }
  2. 静态图片固定展示 3s 后切至下一个 Slider 。这个功能,只要开启了Carousel组件的autoplay就可以直接实现;

  3. 依然支持鼠标移入后的暂停轮播功能 。分析一下并不难,因为Slider是图片时,该功能是自动支持,且Slider是视频时,自动轮播是关闭的,播放完需要前端手动切换到下一页。

    • 只要对视频播放结束方法进行改造,添加手动切换:

      ts 复制代码
      const handleEnded = useCallback((e: any) => {
          e.target.pause();
          carouselRef.current?.next();
          setAutoplay(true)
        }), [])
    • 此外要注意功能分析时发现的隐性功能:视频页在pauseOnfocused时,需要轮播,而不是播放完一次就暂停不播了。一开始认为只要handleEnded方法在mouseEntertrue时,不做任何操作:

      ts 复制代码
      const handleEnded = useCallback((e: any) => {
        if(mouseEnter)
          return;
        e.target.pause();
        carouselRef.current?.next();
        setAutoplay(true);
      }), [mouseEnter])

      就可以实现,因为video标签加上了autoPlay、 loop属性,不去pause,自然就会一直重复播放下去。结果发现,加上了loop属性的video标签无法触发onEnded事件,加上了autoPlay属性后去pause时,视频总是无法回到播放时间为0的时刻,导致重新play时,会闪烁。经过重复尝试最终修改:

      ts 复制代码
      // 视频播放
      const playVideo = (ele: HTMLVideoElement) => {
        if (ele?.paused){
          ele.currentTime = 0;
          try {
            ele?.play()
          } catch (_err) {
            ele?.play()
          }
        }
      }
      
      // 视频结束
      const handleEnded = useCallback((e: any) => {
        if (!mouseEnter)
          playVideo(e.target)
        else {
          e.target.pause();
          carouselRef.current?.next();
          setAutoplay(true)
        }
      }, [mouseEnter])

    最终代码

    tsx 复制代码
    import { Carousel } from "antd";
    import { CarouselRef } from "antd/es/carousel";
    import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
    import { BannerProps, MedaProps } from "src/interfaces/type";
    import { getFileExt } from "src/utils";
    
    // 视频播放方法
    const playVideo = (ele: HTMLVideoElement) => {
      if (ele?.paused)
        try {
          ele?.play()
        } catch (_err) {
          ele?.play()
        }
    }
    
    const MedaNode = memo(({
      video,
      imageUrl,
      index,
      handleEnded
    }: MedaProps) => {
      // 视频
      if (video) {
        return <video
          src={imageUrl} autoPlay muted preload="auto" disablePictureInPicture
          id={`video-${index}`}
          onEnded={handleEnded}
        />
      }
      // 静图
      return <img src={imgUrl} />
    })
    
    export default Banner ({ bannerList }: BannerProps) {
      const [autoplay, setAutoplay] = useState<boolean>(false);
      const [current, setCurrent] = useState<number>(0);
      const carouselRef = useRef<any>(null);
        
      const list = useMemo(() => {
        return bannerList.map((item: any) => {
          const ext = getFileExt(item.imageUrl); // 截取文件后缀
          return { ...item, video: ext === 'mp4' }; // 返回新对象
        });
      }, [bannerList]);
       
      useEffect(() => {
        const currentItem = list[current];
        if (!currentItem) return;
        const videoElem = document.getElementById(`video-${current}`) as HTMLVideoElement;
    
        if (currentItem.video) {
          setAutoplay(false)
          playVideo(videoElem)
        }
      }, [current, list]); 
        
      const handleEnded = useCallback((e: any) => {
        if (!mouseEnter)
          playVideo(e.target)
        else {
          e.target.pause();
          carouselRef.current?.next();
          setAutoplay(true)
        }
      }, [mouseEnter])
      
      
      return (
        <Carousel 
           afterChange={(cur) => setCurrent(cur)} 
           autoplay={autoplay}
           ref={carouselRef} 
           speed={800}
         >{list?.length &&
             list.map((item, index) => {
               const { id, imageUrl, linkUrl, video } = item;
               return (
                 <div
                    key={id}
                    onClick={() => linkUrl && Router.push(linkUrl)}
                  >
                    <MedaNode 
                       video={video} 
                       imageUrl={imageUrl} 
                       index={index} 
                       handleEnded={handleEnded} 
                     />
                  </div>)
              })
          }
        </Carousel>)
    }

结果

功能完美实现,通过产品审核,"暂时"还没出现问题🫡。

相关推荐
excel4 分钟前
前端必备:从能力检测到 UA-CH,浏览器客户端检测的完整指南
前端
前端小巷子11 分钟前
Vue 3全面提速剖析
前端·vue.js·面试
悟空聊架构18 分钟前
我的网站被攻击了,被干掉了 120G 流量,还在持续攻击中...
java·前端·架构
CodeSheep19 分钟前
国内 IT 公司时薪排行榜。
前端·后端·程序员
尖椒土豆sss23 分钟前
踩坑vue项目中使用 iframe 嵌套子系统无法登录,不报错问题!
前端·vue.js
遗悲风24 分钟前
html二次作业
前端·html
江城开朗的豌豆27 分钟前
React输入框优化:如何精准获取用户输入完成后的最终值?
前端·javascript·全栈
CF14年老兵27 分钟前
从卡顿到飞驰:我是如何用WebAssembly引爆React性能的
前端·react.js·trae
画月的亮30 分钟前
前端处理导出PDF。Vue导出pdf
前端·vue.js·pdf
江城开朗的豌豆36 分钟前
拆解Redux:从零手写一个状态管理器,彻底搞懂它的魔法!
前端·javascript·react.js