我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!
大家好,我是小七月,今天我为大家带来了走马灯组件封装,组件效果如下所示:
我目前实现的功能有:在使用该组件时,只需要绘制每一页的显示内容,其中每页使用一个元素包裹即可。使用样例如下:
<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页,那么第n的key为n-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
}
}
}