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

背景

产品经理前段时间给官网首页轮播图加上了视频播放的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>)
    }

结果

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

相关推荐
Fighting_p8 分钟前
【el-upload】el-upload组件 - list-type=“picture“ 时,文件预览展示优化
javascript·vue.js·ecmascript
nt110713 分钟前
大模型实现sql生成 --- 能力不足时的retry
前端·langchain·llm
霸王蟹14 分钟前
Vue的性能优化方案和打包分析工具。
前端·javascript·vue.js·笔记·学习·性能优化
绿草在线22 分钟前
路由Vue Router基本用法
前端·javascript·vue.js
Anlici23 分钟前
embedding 搜索功能怎么实现
前端·人工智能
霸王蟹26 分钟前
Pinia-构建用户仓库和持久化插件
前端·vue.js·笔记·ts·pinia·js
iOS阿玮31 分钟前
Apple开发者已入驻微信公众号
前端·app·apple
她的双马尾1 小时前
React组件复用
javascript·react.js·ecmascript
2013编程爱好者1 小时前
React的Hellow React小案例
前端·javascript·react.js
IT、木易1 小时前
React 中的错误边界(Error Boundaries),如何使用它们捕获组件错误
前端·react.js·前端框架