React 仿真抖音组件

66 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 8 天,点击查看活动详情

在这个组件中,我们将会使用 React 作为框架,Antd 组件库辅助设计,Swiper 库作为主要的功能实现。

我们可以先来看一下效果

图文

图片.png

视频

图片.png

评论

图片.png

选择状态

图片.png

页面结构

图片.png

Nav

这个比较简单,就不详细说了,可以使用 dom 编写,也可以使用 img。

Swiper

swiperjs.com/react

导入

"swiper": "8.4.2",笔者实验,在这个版本中 swiper/css 导入会报错

// Import Swiper React components
import { Swiper, SwiperSlide } from 'swiper/react';

// Import Swiper styles
import 'swiper/css';

export default () => {
  return (
    <Swiper
      spaceBetween={50}
      slidesPerView={3}
      onSlideChange={() => console.log('slide change')}
      onSwiper={(swiper) => console.log(swiper)}
    >
      <SwiperSlide>Slide 1</SwiperSlide>
      <SwiperSlide>Slide 2</SwiperSlide>
      <SwiperSlide>Slide 3</SwiperSlide>
      <SwiperSlide>Slide 4</SwiperSlide>
      ...
    </Swiper>
  );
};

视频

这里图文和视频可以通过 type 进行判断

{data.includes(3) && (
  <>
    <div
      onClick={() => onSwitchPlayVideo(`video${data.id}`)}
    >
      <video
        id={`video${data.id}`}
        style={{
          width: '100vw',
          height: `calc(100vh - 80px)`
        }}
        src={data.attachmentVos[0].resourceUrl}
        controls={false}
        loop={true}
      />
    </div>
    {/* 暂停按钮 */}
    {!videoList[`video${data.id}`]?.isPlay && (
      <div
        onClick={() => onSwitchPlayVideo(`video${data.id}`)}
      >
        <img src={PlayIcon} alt="" />
      </div>
    )}
  </>
)}

图文

这里我们需要用到 Pagination,Autoplay 两个 Swiper 组件

import React, { useState } from 'react'
import { Pagination, Autoplay } from 'swiper'
import { Swiper, SwiperSlide } from 'swiper/react'
import './index.less'

const ImageTextComp = ({ images }) => {
  return (
    <Swiper
      modules={[Pagination, Autoplay]}
      centeredSlides
      pagination={{ clickable: true }}
      autoplay={true}
      loop={isLoop}
    >
      {images?.map((img, index) => {
        return (
          <SwiperSlide key={index}>
            <img width="100%" src={img.resourceUrl} alt="" />
          </SwiperSlide>
        )
      })}
    </Swiper>
  )
}

这里的话,比较麻烦的点是 swiper pagination 的编写

.swiper-pagination 使用 flex 布局

.swiper-pagination {
  width: 100%;
  padding: 0 7px 0px 12px;
  display: flex;
  align-items: center;
  position: absolute;
  bottom: 11px;
  z-index: 99;
  box-sizing: border-box;
}

.swiper-pagination-bullet 单个,采用 flex 弹性适应

.swiper-pagination-bullet {
  display: block;
  flex: 1 1 auto;
  height: 3px;
  background: rgba(220, 220, 220, 0.4);
  position: relative;
  margin-right: 2px;
  border-radius: 2px;
}

组件过渡的动画,通过改变 width 来实现。因此需要在原先的 dom 上面再次覆盖一层,通过 伪元素来实现。

最开始设置为 0

&::after {
  content: '';
  position: absolute;
  width: 0%;
  left: 0;
  top: 0;
  height: 3px;
}

active 的时候,使用 animation 来实现从 0 到 100% 的逐渐显示效果。

&.swiper-pagination-bullet-active {
  &::after {
    background-color: #fff;
    animation-name: widthChange;
    animation-duration: 2s;
    animation-fill-mode: forwards;
    border-radius: 2px;
  }
}

滑动播放逻辑

拿到数据的最开始,通过 videoid 绑定当前 video 的播放状态

useEffect(() => {
  let arr = originData
  if (arr?.length) {
    let _videoList = {}
    arr?.forEach((v, i) => {
      if (v?.simulationDisplayFormCodes?.includes(3)) {
        _videoList[`video${v.id}`] = {
          isPlay: false
        }
      }
    })
    setVideoList(_videoList)
  }
  setDataList(arr)
}, [originData])

触发 swiper 上的 onSlideChange 事件。

获取当前的 index,在 dataList 中找到当前的 video 的 id,使用 id 在 dom 中找到 video 标签,调用 video 的 play 或者 pause 方法

if (dataList[swiper.realIndex].type.includes(3)) {
  let currentId = `video${dataList[swiper.realIndex].id}`
  let _videoList = JSON.parse(JSON.stringify(videoList))
  Object.keys(_videoList).forEach((videoId) => {
    _videoList[videoId].isPlay = currentId === videoId
    if (_videoList[videoId].isPlay) {
      // @ts-ignore
      document.getElementById(videoId)?.play()
    } else {
      // @ts-ignore
      document.getElementById(videoId)?.pause()
    }
  })
  setVideoList(_videoList)
}

向上滑动,判断当前为 video 调用播放,继续向上滑动,此时视频应该是处于暂停状态的才对。

判断当前如果是图文组件或者前一个是视频组件,那么就应该暂停播放。

if (
  dataList[swiper.realIndex].type.includes(2) &&
  dataList[prevIndex].type.includes(3)
) {
  let currentId: string = `video${dataList[prevIndex].id}`
  let _videoList = JSON.parse(JSON.stringify(videoList))
  Object.keys(_videoList).forEach((videoId) => {
    _videoList[videoId].isPlay = currentId === videoId
    if (_videoList[videoId].isPlay) {
      // @ts-ignore
      document.getElementById(videoId)?.pause()
    }
  })
  setVideoList(_videoList)
}

点击切换播放

传入当前视频的 id

const onSwitchPlayVideo = (videoId: number) => {
  let _videoList = JSON.parse(JSON.stringify(videoList))
  _videoList[videoId].isPlay = !_videoList[videoId].isPlay
  if (_videoList[videoId].isPlay) {
    // @ts-ignore
    document.getElementById(videoId)?.play()
  } else {
    // @ts-ignore
    document.getElementById(videoId)?.pause()
  }
  setVideoList(_videoList)
}

到此为止,主逻辑基本上编写完毕

剩下的,基本上是一些比较简单的逻辑。

完整代码

完整代码比较多,所以这里笔者这里就不一一粘贴了,感兴趣的各位可以访问

gitee.com/suiboyu/eve…