本文正在参加「金石计划」
一:引言
本篇文章仿写antd组件库的Carousel组件,也就是我们常用的轮播图(swiper)。文章重点是模拟Carousel组件的内部功能实现。
二:组件分析
观察下图,我们设计一个可配置的轮播图组件,其配置项包含如下
- className 类名
- autoplay 是否自定轮播
- duration 轮播间隔时间
- onChange 轮播图切换触发回调函数
三:问题拆解
在设计组件前,我们可以先考子问题,最后组装起来就实现了目标组件。
问题1:如何控制轮播子元素的展示隐藏?
- 为了简单,我们通过css的display:block和none来控制当前元素的显示与隐藏。
- 初始时设置index为0标记第一个元素展示,当点击轮播控制或自动轮播时更新index
问题2:如何用定时器控制轮播?
这个比较容易实现,根据用户是否传递autoplay属性控制自动轮播,如果autoplay是true,我们可以在useEffect的第一次加载阶段开启定时器控制轮播图index的变化。
问题3:如何对组件内部的子元素添加index和样式控制?
为了对组件内部传递的子元素进行二次处理,我们需要借助React.Children.map方法对传递的子元素进行二次封装。通过React.cloneElement对每个传递的子元素添加动态index和class类名来控制不同展示。
四:React.Children.map介绍
为了后续工作的进展,我们必须学会React.Children.map这个api。
children属性介绍
每个组件都会在props上保存一个children属性,其保存自己包裹的组件,元素,文本等。
index.tsx
import Box from "./Box";
const Home = () => {
return (
<Box>
<div>1</div>
<p>2</p>
3
</Box>
)
}
export default Home;
Box.tsx
import React from "react";
const Box = (props:any)=>{
const {children} = props;
console.log(children)
return (
<div>box</div>
)
}
export default Box;
通过输出观察到,box组件包裹的div,p,文本都被children属性保存起来了。
React.children.map
学习了children属性,我们可以介绍React.children.map这个api了。它接受两个参数,第一个参数是上面介绍的children对象,保存了当前组件内部包裹的所有元素。第二个参数是一个回调函数,其功能和Array.map一模一样,可以对其内部的children的每一个子元素进行二次处理。通常我们会借助React.cloneElement去给当前遍历的元素添加想要的属性。
React.children.map最后返回的是一个二次处理后的dom节点。
注意点:React.children.map对组件内部元素的封装大都需要结合React.cloneElement去完成
React.children.map(children,(item,index)=>{
return ...
})
示例
index.tsx
import Box from "./Box";
const Home = () => {
return (
<Box>
<div>1</div>
<div>2</div>
<div>3</div>
</Box>
)
}
export default Home;
Box.tsx
import React from "react";
const Box = (props:any)=>{
const {children} = props;
return (
<div>
{ React.Children.map(children,(item,index)=>{
return React.cloneElement(item,{index})
})
}
</div>
)
}
export default Box;
效果展示
我们通过 React.children.map和React.cloneElement对box组件内部传递的元素进行了二次封装处理,给每个元素都添加了index属性
五:轮播图代码实现
import React,{useEffect,useState} from "react";
import classNames from "classnames";
interface SwiperProps {
autoplay?:boolean,
children?:React.ReactNode,
className?:string,
duration?:number,
onChange?:(e:any)=>void
}
const Swiper = (props:SwiperProps) => {
const {autoplay,className,children,duration,onChange} = props;
const [activedIndex,setActivedIndex] = useState(0);
const [count,setCount] = useState(0);
//根据autoplay判断是否开启轮询定时器
useEffect(()=>{
let timer:any = null;
if(autoplay) {
timer = setInterval(()=>{
let index = activedIndex+1;
if(index===count) {
index = 0;
}
setActivedIndex(index);
onChange && onChange(index)
},duration);
}
return () => {
timer && clearInterval(timer);
}
},[activedIndex])
useEffect(()=>{
if(!children) {
console.error('至少传递一个子元素');
return;
}
if(Array.isArray(children)) setCount(children.length);
else{
setCount(0);
}
},[])
//接受用户传递的className
const classes = classNames('swiper',className);
//轮播图改变时触发回调
const changeIndex = (index:number) => {
setActivedIndex(index);
onChange && onChange(index);
}
//React.Children.map对组件传递的子元素进行处理,添加index标记和class控制
const swiperChilds = (
<>
{
React.Children.map(children,(item:any,index:number)=>{
return (
<div
key={index}
className={index===activedIndex?'swiper-item swiper-item-actived':'swiper-item'}
>
{ React.cloneElement(item,{
index:index
})
}
</div>
)
})
}
</>
)
return (
<div className={classes}>
{swiperChilds}
<ul className="swiper-control">
{Array.from(new Array(count)).map((item,index)=>(
<li
onClick={()=>changeIndex(index)}
key={index}
className={index===activedIndex?"swiper-control-item swiper-control-item-actived":"swiper-control-item"}
>
</li>
))}
</ul>
</div>
)
}
Swiper.defaultProps = {
duration:1500,
autoplay:false
}
export default Swiper;
六:功能演示
演示1:默认状态
<Swiper>
<div>1</div>
<div>2</div>
<div>3</div>
</Swiper>
演示2:添加自动轮播
duration延迟时间属性默认为1500,可以不传递。
<Swiper autoplay duration={2000}>
<div>1</div>
<div>2</div>
<div>3</div>
</Swiper>
演示3:添加轮播图切换回调函数
//e传递当前展示元素的index下标。
const change = (e:any) => {
console.log(e)
}
return (
<div>
<Swiper autoplay duration={2000} onChange={change}>
<div>1</div>
<div>2</div>
<div>3</div>
</Swiper>
</div>
)
总结
今天的轮播图组件到此结束,希望大家多多支持,我们下一个组件见。