在React中使用原生JS方法实现组件简单拖拽

399 阅读5分钟

前言

最近刚学完react的技术栈,于是打算做点个人小页面啥的。在设计页面样式和布局时候,设想要用到一个返回上一页面的按钮,并把它放在页面的左边,但是想了想做成可拖拽的似乎更加好,查了下,react有现成的插件,但是想着既然是学习react,为何不自己做一个,于是查阅了一些方法,用Html标签中的dragable属性,再借助原生js和react提供的方法实现一个简单的组件拖拽。于是记录一下

功能描述

实现一个组件,该组件功能是点击后返回上级页面或者回到顶部,并把组件设置简单的能可拖拽的,所谓的简单可拖拽,以放置在页面左边为例,该组件可以上下拖拽,但是停止推拽(松开鼠标左键)时候始终位于挨着页面左边框的位置,并且可以从挨着页面左边拖拽至页面右边而且始终挨着页面右边框。位置如图所示(由于不知道怎么弄gif,所以就不展示效果了)

image.png

代码实现:

主要用到两个事件监听机制,即onDragStart和onDragEnd onDragStart:表示在开始拖动时触发的方法

onDragEnd:表示松开鼠标左键时候(结束拖动)触发的方法

原理就是利用e.clientX和e.clientY两个数值,这两个数值是表示鼠标在浏览器坐标系下的位置(x,y),以浏览器工作区左上角为原点(0,0)。

可拖拽组件实现代码如下

import React,{useState}from 'react'
import { useNavigate } from 'react-router-dom'
//引入属性依赖prop-types用于对props的数入的静态类型进行校验
import PropTypes from "prop-types"
import './index.less'
import { Space } from 'antd';
import { LeftCircleOutlined } from '@ant-design/icons';
export default function Index(props) {
  /**
   * distanceVal:表示距离浏览器边框的距离
   * bottomVal:表示距离浏览器底部边框的距离
   * type:表示初始化时候组件是放左边还是放右边
   */
    const {distanceVal,bottomVal,type} = props;
  
    const [compDistance,setCompDistance] =useState(distanceVal);
    const [compBottom,setCompBottom] = useState(bottomVal);
    
    //点击后退至前一页面
        
    const navigate = useNavigate();
    const backToList = ()=>{
      //-1表示后退一位
        navigate(-1)
    }
    
    //用于记录开始拖拽时候浏览器(当前页面)的宽度
    let rootWidth;
    
    //拖拽开始时候的浏览器坐标
    let curBottom;
    
    //开始拖动
    const onDragStart=(e)=>{
        //获取当前页面(不包括滚动条的)的宽度
        rootWidth = document.body.clientWidth;
        //记录组件当前在浏览器坐标系中的坐标Y
        curBottom = e.clientY;
    }
    //松开鼠标,停止拖动
    const onDragEnd=(e)=>{
      if(type=="left"){
      
      //计算拖动了多少px,然后设置bottom的px距离
        setCompBottom(compBottom-(e.clientY-curBottom));
        //判断是否是想从浏览器的一侧移动到另一侧
        if(e.clientX>rootWidth/2){
    
          //工作区宽度 - 本身div距离左边的距离 - 该组件div的宽
          setCompDistance(rootWidth-distanceVal-40);

        }else{
        
          setCompDistance(distanceVal)
          
        }
      }else if(type =="right"){

        setCompBottom(compBottom-(e.clientY-curBottom));
        //初始化放在右边时候刚好跟左边相反
        if(e.clientX>rootWidth/2){
          setCompDistance(distanceVal)
        }else{
        
          //工作区宽度 - 本身div距离右边的距离 - div的宽
          setCompDistance(rootWidth-distanceVal-40);
        }
      }
    }
  return (
    <div className='backToList' 
    draggable="true" 
    onDragStart={onDragStart} 
    onDragEnd={onDragEnd} 
    style={
      type=='left'?{left:compDistance,bottom:compBottom}:
      (type=="right"?{right:compDistance,bottom:compBottom}:
      {display:"none"})}>
    <Space>
      {
        <LeftCircleOutlined onClick={()=>{backToList()}}></LeftCircleOutlined>
      }
    </Space>
  </div>
  )
}

//propTypes:校验props类型
Index.propTypes = {
  distanceVal:PropTypes.number,
  bottomVal:PropTypes.number,
  type:PropTypes.string,
}
//defaultProps:设置Props默认值
Index.defaultProps = {
  distanceVal:0,
  bottomVal:130,
  type:"left"
}
css代码
.backToList{
    font-size: 40px;
    position: fixed;
    cursor: pointer;
    color: #7a295a;
    z-index:99;
}

各种坐标位置宽度如图所示,如果对于setCompDistance不太理解的可以对照这个图看一下

微信图片编辑_20220607213324.jpg

注意

在写这个组件的时候踩了个小坑,就是关于

    const onDragStart=(e)=>{
        //获取当前页面(不包括滚动条的)的宽度
        rootWidth = document.body.clientWidth;
        //记录组件当前在浏览器坐标系中的坐标Y
        curBottom = e.clientY;
    }

首先,为什么要每次拖拽都要重新获取当前页面宽度,逻辑上页面的宽度是不变的,因此只需要一次赋值即可。其实我最开始也是这样想的,我在新建rootWidth时候直接给他赋值 let rootWidth =document.body.clientWidth;

但是这个发现一个问题,我的页面实际可用宽度明明是1113Px(不包括滚动轴宽度),但是他给我返回的是1130px(包括滚动轴宽度),这样在设置距离右边框距离时候就会有问题。并且再次通过点击按钮来获取页面宽度的测试方式获取宽度时候它又变成了1113px,这就很玄学。 在摸索了半天之后我发现这跟react渲染顺序有关?通过打断点测试发现,当走到let rootWidth =document.body.clientWidth;时候页面内容还没加载出来也没滚动条,这时候获取的是整个页面宽度,但是渲染完成之后页面内容加载出来了滚动条出现了。

所以逻辑是我个人认为是当代码走到let rootWidth =document.body.clientWidth;这行时候页面没加载出来,浏览器也不知道你需要不需要滚动条,所以就没给页面加滚动条,然后给返回的是总的宽度,当渲染完之后,发现有高度并且需要滚动条,所以出现了滚动条,但是这时候赋值已经完毕了。

所以,这也解释了我之前测试的为什么页面初始化时候返回宽度是1130px,当手动点击获取宽度时候又成了1113px。我目前就想到每次点击获取宽度的这个方法去规避问题,有哪个大佬知道还有什么方法的可以告诉我一下下。

最后

由于是刚学习完react并且还在实习的菜鸡,所以对react使用和规范还不如vue那么熟悉,所以存在很多漏洞,发出来一是很讨厌网上各种复制粘贴的帖子,二是希望自己垃圾的代码能得到大佬的指点好帮助进步。