React滑块组件的详细教程(附代码)

1,066 阅读5分钟

在这个React组件的实例教程中,我们将用React HooksFunction组件创建一个React Slider组件。你可以在这个CodeSandboxGitHub仓库中看到这个实现的最终输出。如果你想一步一步地实现它,只需按照教程进行。

以React滑块为例

让我们马上开始给我们的React Slider赋予它的风格。这样,我们就可以在浏览器中马上看到我们的组件。我们将使用Styled Components来设计我们的Slider,但也可以随意使用其他东西,比如CSS Modules。

import React from 'react';
import styled from 'styled-components';

const StyledSlider = styled.div`
  position: relative;
  border-radius: 3px;
  background: #dddddd;
  height: 15px;
`;

const StyledThumb = styled.div`
  width: 10px;
  height: 25px;
  border-radius: 3px;
  position: relative;
  top: -5px;
  opacity: 0.5;
  background: #823eb7;
  cursor: pointer;
`;

const Slider = () => {
  return (
    <>
      <StyledSlider>
        <StyledThumb />
      </StyledSlider>
    </>
  );
};

const App = () => (
  <div>
    <Slider />
  </div>
);

export default App;

现在你应该看到滑块和它的拇指已经被React渲染了。我们在React应用程序的上下文中使用Slider组件,同时还使用了一个App组件。让我们看看如何实现它的业务逻辑,以便让用户与它进行交互。

React滑块范围

现在让我们只关注滑块组件。我们将给滑块的每个部分,滑块本身和它的拇指,一个React ref,以便以后直接用DOM操作从这些DOM元素读取(和写入)。否则我们就无法在接下来的步骤中访问滑块的宽度或拇指的位置等属性。

const Slider = () => {
  const sliderRef = React.useRef();
  const thumbRef = React.useRef();

  const handleMouseDown = event => {};

  return (
    <>
      <StyledSlider ref={sliderRef}>
        <StyledThumb ref={thumbRef} onMouseDown={handleMouseDown} />
      </StyledSlider>
    </>
  );
};

此外,我们还为滑块的拇指添加了一个onMouseDown 处理器。这个处理程序实际上是用来捕捉用户与滑块的互动的。在下一步中,我们将再添加两个事件处理程序,这些处理程序只有在鼠标下拉事件被触发后才会被激活。其中一个新的事件--鼠标向上事件--将确保取消对这些新事件的注册。

const Slider = () => {
  const sliderRef = React.useRef();
  const thumbRef = React.useRef();

  const handleMouseMove = event => {
    // TODO:

    // set new thumb position while moving
    // by using the saved horizontal start position
  };

  const handleMouseUp = () => {
    document.removeEventListener('mouseup', handleMouseUp);
    document.removeEventListener('mousemove', handleMouseMove);
  };

  const handleMouseDown = event => {
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  };

  return (
    <>
      <StyledSlider ref={sliderRef}>
        <StyledThumb ref={thumbRef} onMouseDown={handleMouseDown} />
      </StyledSlider>
    </>
  );
};

onMouseDown 处理程序的函数做了两件事。

首先,它又为拇指注册了两个处理程序,这只发生在鼠标下降事件被触发之后。这确保了拇指只在鼠标向下时移动。如果鼠标向上事件最终被触发--刚刚被注册过--所有新注册的处理程序将被再次删除。鼠标移动事件是范围滑块的实际逻辑发生的地方,但同样,只有当鼠标向下事件被激活时才会发生。

其次,它存储了拇指位置和实际点击X轴的差值--只是为了在这里更准确。我们只存储一次,以便以后在每个鼠标移动事件中重复使用它。我们将再次使用React ref,这将确保该值不会在组件重现之间丢失。另外,我们在这里没有使用React状态,因为我们不想触发组件的重新渲染。

const Slider = () => {
  const sliderRef = React.useRef();
  const thumbRef = React.useRef();

  const diff = React.useRef();

  const handleMouseMove = event => {
    let newX =
      event.clientX -
      diff.current -
      sliderRef.current.getBoundingClientRect().left;
  };

  const handleMouseUp = () => {
    document.removeEventListener('mouseup', handleMouseUp);
    document.removeEventListener('mousemove', handleMouseMove);
  };

  const handleMouseDown = event => {
    diff.current =
      event.clientX - thumbRef.current.getBoundingClientRect().left;

    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  };

  ...
};

注意:我们只计算X轴上的数值,因为我们在这里处理的不是一个垂直的滑块。你可以在以后的练习中自己尝试将这个滑块组件转换为一个垂直的滑块。

在我们计算了鼠标移动事件中的新位置后,我们可以检查新位置是否会超出我们的滑块范围。如果是这样的话,我们就使用滑块范围的边界,而不是新的x-position。

const Slider = () => {
  ...

  const handleMouseMove = event => {
    let newX =
      event.clientX -
      diff.current -
      sliderRef.current.getBoundingClientRect().left;

    const end = sliderRef.current.offsetWidth - thumbRef.current.offsetWidth;

    const start = 0;

    if (newX < start) {
      newX = 0;
    }

    if (newX > end) {
      newX = end;
    }
  };

  ...
};

接下来,我们将使用这两个值,即新的位置和范围的end ,来计算我们的拇指要从左边移动多远的百分比。由于拇指本身的宽度为10px,我们需要通过移除其一半的尺寸来居中,以避免拇指向右或向左溢出。

const getPercentage = (current, max) => (100 * current) / max;

const getLeft = percentage => `calc(${percentage}% - 5px)`;

const Slider = () => {
  ...

  const handleMouseMove = event => {
    let newX =
      event.clientX -
      diff.current -
      sliderRef.current.getBoundingClientRect().left;

    const end = sliderRef.current.offsetWidth - thumbRef.current.offsetWidth;

    const start = 0;

    if (newX < start) {
      newX = 0;
    }

    if (newX > end) {
      newX = end;
    }

    const newPercentage = getPercentage(newX, end);

    thumbRef.current.style.left = getLeft(newPercentage);
  };

  ...
};

React滑块的例子现在应该工作了。我们已经使用了直接的DOM操作来设置滑块的拇指的新位置left 。你也可以在这里使用React状态,但在移动滑块的拇指时,会经常触发React的内部状态管理,导致每次鼠标移动都要重新渲染组件。按照我们的方法,我们直接使用DOM操作,避免React的实际重新渲染,而是自己对DOM进行操作。

React滑块组件

最后,我们希望有一个真正的React滑块组件,并有一个细小的API给外部。目前,我们不能向滑块组件传递任何道具,也不能通过回调函数从它那里获得任何当前值。让我们来改变这一点。

首先,我们将传递一些初始值给我们的滑块组件。比方说,我们希望有一个拇指的初始位置和一个范围的最大值。我们可以通过以下方式传递并使用它们来进行初始渲染。

...

const Slider = ({ initial, max }) => {
  const initialPercentage = getPercentage(initial, max);

  const sliderRef = React.useRef();
  const thumbRef = React.useRef();

  ...

  return (
    <>
      <StyledSlider ref={sliderRef}>
        <StyledThumb
          style={{ left: getLeft(initialPercentage) }}
          ref={thumbRef}
          onMouseDown={handleMouseDown}
        />
      </StyledSlider>
    </>
  );
};

const App = () => (
  <div>
    <Slider initial={10} max={25} />
  </div>
);

第二,我们将为滑块组件提供一个回调函数,将最近的设定值传递给外部。否则,使用我们的Slider组件的React组件将无法从它那里接收任何更新。

...

const getPercentage = (current, max) => (100 * current) / max;

const getValue = (percentage, max) => (max / 100) * percentage;

const getLeft = percentage => `calc(${percentage}% - 5px)`;

const Slider = ({ initial, max, onChange }) => {
  ...

  const handleMouseMove = event => {
    let newX = ...

    ...

    const newPercentage = getPercentage(newX, end);
    const newValue = getValue(newPercentage, max);

    thumbRef.current.style.left = getLeft(newPercentage);

    onChange(newValue);
  };

  return (
    <>
      <StyledSlider ref={sliderRef}>
        <StyledThumb
          style={{ left: getLeft(initialPercentage) }}
          ref={thumbRef}
          onMouseDown={handleMouseDown}
        />
      </StyledSlider>
    </>
  );
};

const App = () => (
  <div>
    <Slider
      initial={10}
      max={25}
      onChange={value => console.log(value)}
    />
  </div>
);

第三,我们将显示滑块的初始和最大范围

...

const SliderHeader = styled.div`
  display: flex;
  justify-content: flex-end;
`;

...

const Slider = ({ initial, max, onChange }) => {
  ...

  return (
    <>
      <SliderHeader>
        <strong>{initial}</strong>
        &nbsp;/&nbsp;
        {max}
      </SliderHeader>
      <StyledSlider ref={sliderRef}>
        <StyledThumb
          style={{ left: getLeft(initialPercentage) }}
          ref={thumbRef}
          onMouseDown={handleMouseDown}
        />
      </StyledSlider>
    </>
  );
};

并将通过再次使用直接的DOM操作将显示的初始范围替换为当前范围--以绕过React在使用其状态管理时的重新渲染机制。

const Slider = ({ initial, max, onChange }) => {
  ...

  const currentRef = React.useRef();

  ...

  const handleMouseMove = event => {
    ...

    thumbRef.current.style.left = getLeft(newPercentage);
    currentRef.current.textContent = newValue;

    onChange(newValue);
  };

  return (
    <>
      <SliderHeader>
        <strong ref={currentRef}>{initial}</strong>
        &nbsp;/&nbsp;
        {max}
      </SliderHeader>
      <StyledSlider ref={sliderRef}>
        <StyledThumb
          style={{ left: getLeft(initialPercentage) }}
          ref={thumbRef}
          onMouseDown={handleMouseDown}
        />
      </StyledSlider>
    </>
  );
};

如果你试试你的滑块组件,你应该看到它的初始值、当前值(鼠标移动后)和最大范围。同样,我们使用了React的直接DOM操作,通过Ref而不是state来防止在每次鼠标移动事件后重新渲染整个组件。通过这种方式,我们保持了组件的高性能,以便在我们的实际React应用中重用。

最后但并非最不重要的是,我们将为我们的滑块的范围显示一个有意见的格式化值--虽然可以通过滑块的组件API从外部指定。

const Slider = ({
  initial,
  max,
  formatFn = number => number.toFixed(0),
  onChange,
}) => {
  ...

  const handleMouseMove = event => {
    ...

    thumbRef.current.style.left = getLeft(newPercentage);
    currentRef.current.textContent = formatFn(newValue);

    onChange(newValue);
  };

  return (
    <>
      <SliderHeader>
        <strong ref={currentRef}>{formatFn(initial)}</strong>
        &nbsp;/&nbsp;
        {formatFn(max)}
      </SliderHeader>
      <StyledSlider ref={sliderRef}>
        <StyledThumb
          style={{ left: getLeft(initialPercentage) }}
          ref={thumbRef}
          onMouseDown={handleMouseDown}
        />
      </StyledSlider>
    </>
  );
};

const App = () => (
  <div>
    <Slider
      initial={10}
      max={25}
      formatFn={number => number.toFixed(2)}
      onChange={value => console.log(value)}
    />
  </div>
);

就这样了。你已经在React中设计了一个滑块组件,使它的交互成为可能,并给了它一个API来从外部与它交互。你可以从这里开始使用或改进这个组件了。

React滑块组件的灵感来自这个纯JavaScript的实现。请在评论中告诉我你是如何改进你的组件的,你是如何喜欢这个教程的。