【React】【案例】:简易轮播组件

2020-04-21 16:26:43 浏览数 (1)

代码语言:javascript复制
目录
1. 组件展示
2. 关键技术
3. 关键实现
4. 组件接口

1. 组件展示

组件特性:

  • 滑动箭头,只有当待滑动内容无法完整显示时才出现。
  • 滑动过程使用动画体现。
  • 滑动到左边界时,左滑动箭头给出不可滑动标识。
  • 滑动到右边界时,右滑动箭头给出不可滑动标识。
  • 浏览器缩放时,也能满足上述条件。

2. 关键技术

  • 如何实现竖直居中?
    • absolute top:50% transform(-50%, -50%)
  • 如何避免用户点击滑动箭头时,意外选中文本?
    • css3 -> user-select:none
  • 如何实现 slider 元素横向布局?
    • css -> display:inline-block whitespace:no-wrap
  • 如何实现滑动动画?
    • css3 -> transition:transform translate3d
  • 如何监听 slider 容器尺寸变更?
    • resize-observer-polyfill
  • 如何实现防抖?
    • loadsh -> debounce
  • 如何操作 DOM?
    • React -> Refs
  • 如何指示用户按钮不可点击?
    • css -> cursor: not-allowed;
  • 如何度量组件尺寸?
    • domElement.offsetWidth
  • 如何包装开发自定义HTML结构?
    • React -> React.Chidren.map
      • 这里注意空元素问题
  • 滑动基本原理

3. 关键实现

3.1. Slider.tsx

代码语言:javascript复制
import React from "react"
import classnames from "classnames"
import {LeftOutlined, RightOutlined} from "@ant-design/icons"
import ResizeObserver from 'resize-observer-polyfill';
import debounce from 'lodash/debounce';
import {isTransform3dSupported} from "./util"

export interface SliderProps {
    className?: string,
    style?: React.CSSProperties,
    selectedIndex?: number,
    onClick?: (index: number) => void,
}

export interface SliderState {
    showBtnPrevNext: boolean,
    btnNextDisabled: boolean,
    btnPrevDisabled: boolean
}

export default class Slider extends React.Component<SliderProps, SliderState> {
    private wrapperRef: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>();
    private scrollerRef: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>();
    private containerRef: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>();
    private offset: number = 0;
    private debouncedResize: any;
    private resizeObserver: any;

    constructor(props) {
        super(props);
        this.state = {
            showBtnPrevNext: false,
            btnNextDisabled: false,
            btnPrevDisabled: false,
        };
    }

    componentDidMount(): void {
        this.debouncedResize = debounce(() => {
            this.updateScrollerPosition(this.offset);
        }, 200);
        this.resizeObserver = new ResizeObserver(this.debouncedResize);
        this.resizeObserver.observe(this.wrapperRef.current);
    }

    componentWillUnmount(): void {
        if (this.resizeObserver) {
            this.resizeObserver.disconnect();
        }
        if (this.debouncedResize && this.debouncedResize.cancel) {
            this.debouncedResize.cancel();
        }
    }

    private needShowPrevOrNext() {
        return !(this.wrapperRef.current.offsetWidth > this.scrollerRef.current.offsetWidth);
    }

    private updateScrollerPosition(offset) {
        const maxScrollerXOffset = 0;
        const minScrollerXOffset = this.wrapperRef.current.offsetWidth - this.scrollerRef.current.offsetWidth;

        let target = -1;
        if (!this.needShowPrevOrNext()) {
            target = 0;
        } else {
            target = Math.max(Math.min(maxScrollerXOffset, offset), minScrollerXOffset);
        }

        this.offset = target;
        const scrollerStyle = this.scrollerRef.current.style;
        const transformSupported = isTransform3dSupported(scrollerStyle);
        if (transformSupported) {
            scrollerStyle.transform = `translate3d(${target}px,0,0)`;
        } else {
            scrollerStyle.left = `${target}px`;
        }

        if (this.needShowPrevOrNext()) {
            this.setState({
                showBtnPrevNext: true,
                btnPrevDisabled: this.offset == 0,
                btnNextDisabled: !(this.offset > minScrollerXOffset)
            });
        } else {
            this.setState({
                showBtnPrevNext: false,
                btnPrevDisabled: true,
                btnNextDisabled: true,
            });
        }
    }

    handleClick = (index: number) => {
        const {onClick} = this.props;
        if (onClick) {
            onClick(index);
        }
    };

    handlePrev = () => {
        const containerNode = this.wrapperRef.current;
        const {offset} = this;
        this.updateScrollerPosition(offset   containerNode.offsetWidth);
    };

    handleNext = () => {
        const containerNode = this.wrapperRef.current;
        const {offset} = this;
        this.updateScrollerPosition(offset - containerNode.offsetWidth);
    };

    render(): React.ReactElement<any, string | React.JSXElementConstructor<any>> | string | number | {} | React.ReactNodeArray | React.ReactPortal | boolean | null | undefined {

        const {
            className,
            style,
            selectedIndex,
            children
        } = this.props;

        const {
            showBtnPrevNext,
            btnNextDisabled,
            btnPrevDisabled
        } = this.state;

        return (
            <div className={classnames("mousex-slider", className)} style={style}>
                <span
                    className={classnames("mousex-slider-btn-prev", {
                        "mousex-slider-btn-show": showBtnPrevNext,
                        "mousex-slider-btn-disabled": btnPrevDisabled
                    })}
                    onClick={this.handlePrev}>
                    <LeftOutlined className={"mousex-slider-btn-prev-icon"}/>
                </span>
                <div className={"mousex-slider-items-wrapper"} ref={this.wrapperRef}>
                    <div className={classnames("mousex-slider-items-scroller", "animated")} ref={this.scrollerRef}>
                        <div className={"mousex-slider-items-container"} ref={this.containerRef}>
                            {
                                React.Children.map(children, (child, index) => {
                                    if(!child){
                                        return null;
                                    }
                                    return (
                                        <div onClick={() => this.handleClick(index)}
                                             className={index === selectedIndex ? "mousex-slider-item selected" : "mousex-slider-item"}>
                                            {child}
                                        </div>
                                    )
                                })
                            }
                        </div>
                    </div>
                </div>
                <span
                    className={classnames("mousex-slider-btn-next", {
                        "mousex-slider-btn-show": showBtnPrevNext,
                        "mousex-slider-btn-disabled": btnNextDisabled
                    })}
                    onClick={this.handleNext}>
                    <RightOutlined className={"mousex-slider-btn-next-icon"}/>
                </span>
            </div>
        );
    }
}

3.2. slider.less

代码语言:javascript复制
.mousex-slider {
  position: relative;
  padding: 0 32px;
}

.mousex-slider-btn-prev,
.mousex-slider-btn-next {
  position: absolute;
  top: 0;
  width: 32px;
  height: 100%;
  color: rgb(213, 219, 230);
  cursor: pointer;
  user-select: none;
}

.mousex-slider-btn-prev,
.mousex-slider-btn-next {
  display: none;
}

.mousex-slider-btn-prev {
  left: 0;
}

.mousex-slider-btn-next {
  right: 0;
}

.mousex-slider-btn-show {
  display: inline;
}

.mousex-slider-btn-disabled {
  cursor: not-allowed;
}

.mousex-slider-btn-prev-icon,
.mousex-slider-btn-next-icon {
  position: absolute;
  top: 50%;
  left: 50%;
  font-size: 32px;
  font-weight: 700;
  transform: translate(-50%, -50%);
}

.mousex-slider-items-wrapper {
  position: relative;
  margin: 0 16px;
  white-space: nowrap;
  overflow: hidden;
}

.mousex-slider-items-scroller {
  display: inline-block;
}

.mousex-slider-items-scroller.animated {
  transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}

.mousex-slider-item {
  display: inline-block;
  width: 166px;
  height: 99px;
  margin: 3px 35px 3px 3px;
  border-radius: 2px;
  cursor: pointer;
}

.mousex-slider-item:last-of-type {
  margin-right: 0;
}

.mousex-slider-item.selected,
.mousex-slider-item.selected:hover {
  box-shadow: 0 0 3px 3px #c0ddff;
}

.mousex-slider-item:hover {
  box-shadow: 0 0 2px 2px rgba(219, 229, 240, 1);
}

4. 组件接口

代码语言:javascript复制
import React, {useState} from "react"

import Slider from "../components/slider"
import "../components/slider/slider.less"

import "./app.less"

export default function App(){

    const [selectedIndex, setSelectedIndex] = useState(0);
    return (
        <div>
            <div className={`gallery b${selectedIndex 1}`}></div>
            <Slider selectedIndex={selectedIndex} onClick={(index)=> setSelectedIndex(index)}>
                <div className={"preview b1"}></div>
                <div className={"preview b2"}></div>
                <div className={"preview b3"}></div>
                <div className={"preview b4"}></div>
                <div className={"preview b5"}></div>
                <div className={"preview b6"}></div>
                <div className={"preview b7"}></div>
                <div className={"preview b8"}></div>
            </Slider>
        </div>
    );
}

参考:

react: https://react.docschina.org/ rc-tabs: https://github.com/react-component/tabs


0 人点赞