使用React封装自己的走马灯组件

1,124 阅读2分钟

我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!

大家好,我是小七月,今天我为大家带来了走马灯组件封装,组件效果如下所示:

我目前实现的功能有:在使用该组件时,只需要绘制每一页的显示内容,其中每页使用一个元素包裹即可。使用样例如下:

<MyCarousel>
    <div>page1</div>
    <div>page2</div>
</MyCarousel

现在我们来分析一下该组件是怎么实现的吧!在刚开始打算封装时,我是这样想的,打算以每一页为一个参考维度,当我点击底部的指示器时,获取到当前页,然后再计算当前页需要往左移动多少像素值,思索了一番,发现挺麻烦的。于是我便打开了ant-design-vue里面实现的走马灯组件效果,打开开发者工具,发现当切换页面时,包裹着所有页面的父元素整体向左移动,顿时茅塞顿开。所以下面我详细说说。

下面是我的简化代码:

// 此处是一个可视区域,overflows设置为hidden,超出部分不可见!
<div class="wrapper">
    // 此处是包裹了所有的page的list,此处设置一个transform:translate3d(x,0,0,0),其中x是计算出来的。
    <div class="page-list">
        // 此处是一个个页面,它的宽高与wrapper元素相同
        <div class="page-item" key=0>page1</div>
        <div class="page-item" key=1>page2</div>
    </div>
</div>

所以重点是我们怎么计算这个x值。首先默认会展示第一页,我们若要展示第二页,那么需要往左移动一个wrapper元素的宽度width,即x为负数又代表页面的索引是从0开始的,那么要展示第n页,那么第nkeyn-1,那么x的值为-key * width,所以实现这个效果就简单了,实现代码如下

import React, { useState, useCallback ,useRef,useEffect} from 'react';
import ReactDom from 'react-dom';

function MyCarousel(props) {
  const [curIndex, setCurIndex] = useState(0);
  const [clientWidth, setClientWidth] = useState(0);
  const [pages] = useState([0, 1, 2, 3, 4]);
  const [transformX, setTransformX] = useState(0);

  const wrapperRef = useRef();
  //获取可视区域的宽度
  useEffect(() => {
    setClientWidth(wrapperRef.current.clientWidth);
  }, [wrapperRef]);
// 点击第n个页面的指示器时的方法
  const handleDotClick = (index) => {
    calcTranslateX(index);
    setCurIndex(index);
  };
   // 计算x的值
  const calcTranslateX = (index) => {
    let x = -index * clientWidth;
    setTransformX(x);
  };

  return (
  );
}

走马灯的基本效果就完成了,但是注意,我们封装的是一个组件,并不是仅仅是一个效果。所以我们需要获得从父组件中传过来的page元素,在react中,可以使用props.children获取子组件包裹的所有元素。对于每一个page页面的宽高应该与wrapper相同的样式我们可以直接在组件中添加一个类,并且规定好,后面父组件使用是只需要关注page页面里的样式。遍历props.children来渲染page元素,为了显示效果,我给了默认页,代码如下:

return (
    <div className="carousel-wrapper" ref={wrapperRef}>
      {/* 要播放的页面 */}
      <div
        className="carousel-list"
        style={{ transform: `translate3d(${transformX}px, 0px, 0px)` }}
      >
        {!props.children
          ? pages.map((page, index) => {
              return (
                <div className="carousel-item" key={index}>
                  page{page}
                </div>
              );
            })
          : props.children.map((child) => <div className="carousel-item">{child}</div>)}
      </div>

      {/* 圆点指示器 */}
      <div className="dots">
        {!props.children
          ? pages.map((page, index) => {
              return (
                <div
                  key={index + 'dot'}
                  onClick={() => handleDotClick(index)}
                  className={`dot ${curIndex === index ? 'dot-active' : ''}`}
                ></div>
              );
            })
          : props.children.map((page, index) => {
              return (
                <div
                  key={index + 'dot'}
                  onClick={() => handleDotClick(index)}
                  className={`dot ${curIndex === index ? 'dot-active' : ''}`}
                ></div>
              );
            })}
      </div>
    </div>
  );

到此,走马灯的基本封装就已经完成了,由于精力原因,我本来还要设置一些props,比如父组件指定初始显示第几页,比如当切换页数时,回调父组件的方法等,我都还没有完成,等有时间我会补上。谢谢大家。

最后我遇到一个小问题,我最开始是用Vue2来封装的,但是想要根据父组件传的页面元素重新在子组件渲染,我使用Vue.$slot拿到了Vnode对象,但是如何在template渲染时出现了问题,上网查了一个,说是要自己在component内部创建一个子组件来渲染,但是一直有问题。希望热心的友友能够评论回答我一下,谢谢大家。下面是我查到的方法,但是失败了。

components:{
    renderVnode:{
        props:{
            vNodes:{},
            },
        render:(h,ctx) =>(ctx.props.VNode) // 此处的 ctx 为undefined,this也为undefined
        }
    }
}