前言
最近开发移动端的项目比较多,很多组件都共用着,同时也看了一些课程,啥toast、loading、swiper等等,所以想自己用React搭个移动端的组件库。
正文
封装之后组件的使用代码
首先这是我们想实现的组件使用效果(基础版),一般也都这样差不多,当然组件还可以传一些属性,控制自动播放、循环,指示点indicator效果,这一次先搭个基础的。
javascript
<Swiper>
{data!.banner.map((item, index) => (
<Swiper.Item key={index}>
<img src={item.src} alt={item.alt} height={'100%'} width={'100%'} />
</Swiper.Item>
))}
</Swiper>
实现阶段,大致就三步
- 基础组件代码、属性类型约束、样式搭建
- touch触摸事件、开始touchstart、移动touchmove、结束touchend
- 细节完善,比如第一张、最后一张不可以在往前往后滑动
基础组件代码、属性类型约束、样式搭建
先建个组件框架、写一下基础代码,样式我就不展示了,会放后面
那么我们先看Swiper.Item,它比较简单,它其实就是传递一个子元素进去,所以就接收一个children属性,点击事件可选
Swiper.Item.tsx
typescript
import React from 'react';
import './styles/swiper-item.scss';
export interface SwiperItemProps {
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
children: React.ReactNode;
}
const classPrefix = 'czh_swiper_item';
const SwiperItem: React.FC<SwiperItemProps> = (props) => {
return (
<div className={classPrefix} onClick={props.onClick}>
{props.children}
</div>
);
};
SwiperItem.displayName = 'SwiperItem';
export default SwiperItem;
主要的代码都在Swiper里,围绕着我们封装之后使用组件的代码,Swiper接收着另一个组件Swiper.Item,那么属性我们就可以确认,有个children,再来是默认展示、样式。
ini
import React from 'react';
import './styles/swiper.scss';
export interface SwiperProps {
defultIndex?: number;
children: React.ReactElement | React.ReactElement[];
style?: React.CSSProperties & Partial<Record<'--height' | '--width' | '--border-radius' | '--track-padding', string>>;
}
const classPrefix = 'czh_swiper';
const Swiper: React.FC<SwiperProps> = (props) => {
const renderChild = () => {
return (
<div className={`${classPrefix}_track_inner`}>
{/* React.Children.map,传入子元素可以遍历传入的子元素 */}
{React.Children.map(props.children, (child, index) => {
return (
<div
className={`${classPrefix}_slide`}
// 通过left属性,根据index,让图片横向平铺
style={{
left: `-${index * 100}%`,
}}
>
{child}
</div>
);
})}
</div>
);
};
return (
<div className={classPrefix}>
<div className={`${classPrefix}_track`}>{renderChild()}</div>
</div>
);
};
Swiper.displayName = 'Swiper';
Swiper.defaultProps = {
defultIndex: 0,
};
export default Swiper;
基础代码写完,为了让我们可以先在浏览器可以观看我们写的效果,我们在首页引入一下看看,照搬上面我们封装代码,这里的疑问,主要是怎么实现Swiper.Item这样的使用效果,我们在文件夹在创建个index.tsx.
这其实是个倒推的过程,从下往上看,Swiper上有个Item
typescript
import FinalSwiper from './swiper';
import FinalSwiperItem from './swiper-item';
// 可以直接在间接的导出已经导出的属性
export type { SwiperProps } from './swiper';
export type { SwiperItemProps } from './swiper-item';
type FinalSwiperType = typeof FinalSwiper;
// 那么类型约束里就一个Item,同时还要保留原来Swiper类型
export interface FinalSwiperProps extends FinalSwiperType {
Item: typeof FinalSwiperItem;
}
// 但是我们创建的Swiper没有Item,那么我们就自己在建一个
const Swiper = FinalSwiper as FinalSwiperProps;
// Swiper上有个Item
Swiper.Item = FinalSwiperItem;
export default Swiper;
下面是页面效果,成功展示了,然后子元素也有我们遍历的元素,横向平铺,到这里,我们的第一步就完成。接下来就是通过touch事件,来操作我们的样式进行图片位移
touch触摸事件、开始touchstart、移动touchmove、结束touchend
通过样式transform来控制图片的位移,创建currentIndex,因为我们需要线性的移动过程,translate3d的X值是一点一点的变化,所以我们需要在移动过程不断设置currentIndex,currentIndex的变化0->0.3->0.63... ,finalPosition的值也会一点一点的变化,实现线程移动
typescript
const [currentIndex, setCurrentIndex] = useState(0); //记录当前index,位于哪个图片
const getFinalPosition = (index: number) => {
// 0 * 100 + 0 * 100
// 0.3 * 100 + 0 * 100
// 1 * 100 + 1 * 100
// ...
const finalPosition = -currentIndex * 100 + index * 100;
return finalPosition;
};
style={{
left: `-${index * 100}%`,
transform: `translate3d(${getFinalPosition(index)}%,0,0)`,
}}
我们需要通过滑动的距离来判断是否切换切换下一页,滑动大于50%就下一张,否则反之,那么我们就需要获取touch开始的坐标和移动的距离,我们在图片外壳加个touch事件,使用useRef来记录坐标,ref可以保存上一次的值
handleTouchStart
ini
const startRef = useRef(0); //触摸开始的坐标X
<div className={`${classPrefix}-track`} onTouchStart={(e) => handleTouchStart(e)}>
{renderChild()}
</div>
const handleTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
//创建一个ref,来保存值,获取一开始touch坐标
startRef.current = e.changedTouches[0].clientX;
// 监听原生事件的滑动过程/滑动结束
document.addEventListener('touchmove', handleTouchmove);
document.addEventListener('touchend', handleTouchend);
};
handleTouchmove
ini
const slideRatioRef = useRef(0); //滑动与图片的比例
const traceRef = useRef<HTMLDivElement>(null);
// 获取当前图片滑动比例,是0.5以下,还是0.5以上
const getSlideRatio = (diff: number) => {
const ele = traceRef.current;
if (!ele) return 0;
return diff / ele.offsetWidth; //通过offsetWidth可以获取元素宽度
};
const handleTouchmove = (e: TouchEvent) => {
// 获取滑动最后一个x坐标
const currentX = e.changedTouches[0].clientX;
// 运算得出滑动距离
const diff = startRef.current - currentX;
// 获取滑动比例
slideRatioRef.current = getSlideRatio(diff);
// 不断设置滑动index,以至于translate线性变化
let slideIndex = slideRatioRef.current + currentIndex;
setCurrentIndex(slideIndex);
};
return (
<div className={classPrefix} onTouchStart={(e) => handleTouchStart(e)} ref={traceRef}>
<div className={`${classPrefix}_track`}>{renderChild()}</div>
</div>
);
至此,我们就可以拖动我们的图片了,接下来我们在滑动结束的通过比例判断是否进入下一张
handleTouchend
至此,我们就可以成功滑动图片切换了
ini
const handleTouchend = (e: TouchEvent) => {
//四舍五入判断滑动是否超过50%
const index = Math.round(slideRatioRef.current);
const targetIndex = index + currentIndex;
setCurrentIndex(targetIndex);
// 清除监听
window.removeEventListener('touchmove', handleTouchmove);
window.removeEventListener('touchend', handleTouchend);
};
细节完善
1、transition线性动画
当我们滑动比例超过50%,会进入到下一张,但是进入下一只这个过程是很生硬直接的,唰一下就过了,我们需要添加一个transition线性动画
scss
const [dragging, setDragging] = useState(false);
const getTransition = () => {
if (dragging) return '';
return 'transform 0.3s ease-in-out';
};
style={{
left: `-${index * 100}%`,
transform: `translate3d(${getFinalPosition(index)}%,0,0)`,
transition: getTransition(),
}}
// move的时候不需要线性,因为有translate的动态移动了
const handleTouchmove = (e: TouchEvent) => {
...
setDragging(true);
};
// 生硬的切换是在我们离开touch的时候
const handleTouchend = (e: TouchEvent) => {
...
setDragging(false);
};
效果
2、第一张和最后一张,在往前、往后滑动会划到到空白
应该让其一直保持在第一张或最后一张,被影响的还是currentIndex的值,我们需要给它一个最大最小值的范围,在滑动结束的时候,进行限制
ini
const boundIndex = (currentIndex: number) => {
let min = 0;
// React.Children.count可以获取子元素的数量length
let max = React.Children.count(props.children) - 1;
let ret = currentIndex;
ret = Math.max(currentIndex, min); // -0.5 0 / 5.5 0
ret = Math.min(ret, max); // 0 3 / 5.5 3
return ret;
};
const handleTouchend = (e: TouchEvent) => {
// const targetIndex = index + currentIndex;
const targetIndex = boundIndex(index + currentIndex);
setCurrentIndex(targetIndex);
};
往前滑,currentIndex就会是负数,往后滑,currentIndex又会大于length。
最终
到这,基本就实现了一个基础的轮播图滑动,下一篇就会添加一些属性、新增指示点,优化一些细节
目标
- 添加一些属性,循环播放、自动播放、添加自定义样式
- 指示点展示、子元素判断
- ......看又有啥