React 实现一个交互完整的拖拽上传组件 仅需120行代码
你将在该篇学到:
- 将如何现有组件改写为 React Hooks 函数组件
- useState,useEffect,useRef 的英文如何替代原生命周期状语从句:Ref 的。
- 一个完整拖拽上传行为覆盖的四个事件:dragover,dragenter,drop,dragleave
- 使用如何 React Hooks 关系编写自己的 UI 组件库。
逛国外社区时看到这篇:

如何在 React 中实现拖放文件
讲文章了 React 拖拽上传的精简实现,但直接翻译照搬显然不是我的风格。
我于是汉语中类似的用 React Hooks 重写了一版,除 CSS 的代码总数 120 行。
效果如下:

添加基本目录骨架
app.js
import React from 'react';
import PropTypes from 'prop-types';
import { FilesDragAndDrop } from '../components/Common/FilesDragAndDropHook';
export default class App extends React.Component {
static propTypes = {};
onUpload = (files) => {
console.log(files);
};
render() {
return (
<div>
<FilesDragAndDrop
onUpload={this.onUpload}
/>
</div>
);
}
}
FilesDragAndDrop.js(非钩):
import React from 'react';
import PropTypes from 'prop-types';
import '../../scss/components/Common/FilesDragAndDrop.scss';
export default class FilesDragAndDrop extends React.Component {
static propTypes = {
onUpload: PropTypes.func.isRequired,
};
render() {
return (
<div className='FilesDragAndDrop__area'>
传下文件试试?
<span
role='img'
aria-label='emoji'
className='area__icon'
>
😎
</span>
</div>
);
}
}
1.如何改写为 Hooks 组件?
请看动图:


2.改写组件
Hooks 版组件属于函数组件,将以上改造:
import React, { useEffect, useState, useRef } from "react";
import PropTypes from 'prop-types';
import classNames from 'classnames';
import classList from '../../scss/components/Common/FilesDragAndDrop.scss';
const FilesDragAndDrop = (props) => {
return (
<div className='FilesDragAndDrop__area'>
传下文件试试?
<span
role='img'
aria-label='emoji'
className='area__icon'
>
😎
</span>
</div>
);
}
FilesDragAndDrop.propTypes = {
onUpload: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
count: PropTypes.number,
formats: PropTypes.arrayOf(PropTypes.string)
}
export { FilesDragAndDrop };
FilesDragAndDrop.scss
.FilesDragAndDrop {
.FilesDragAndDrop__area {
width: 300px;
height: 200px;
padding: 50px;
display: flex;
align-items: center;
justify-content: center;
flex-flow: column nowrap;
font-size: 24px;
color: #555555;
border: 2px #c3c3c3 dashed;
border-radius: 12px;
.area__icon {
font-size: 64px;
margin-top: 20px;
}
}
}
然后就可以看到页面:

实现分析
从操作 DOM,组件复用,事件触发,阻止默认行为,以及 Hooks 应用方面分析。
1.操作 DOM:`useRef`
由于需要拖拽文件上传以及操作组件实例,用到需要 ref 属性。
React Hooks 中新增了 useRef API
语法
const refContainer = useRef(initialValue);
- useRef 一个报道查看可变的 ref 对象。
- 其.current 属性被初始化为传递的参数(initialValue)
- 返回的对象将存留在整个组件的生命周期中。
...
const drop = useRef();
return (
<div
ref={drop}
className='FilesDragAndDrop'
/>
...
)
2.事件触发

完成具有动态交互的拖拽行为并不简单,需要用到四个事件控制:
- 区域外:dragleave,离开范围
- 区域内:dragenter,用来确定放置目标是否接受放置。
- 区域内移动:dragover,用来确定给用户显示怎样的反馈信息
- 完成拖拽(落下)drop:,允许放置对象。
这四个事件并存,才能阻止 Web 浏览器默认行为和形成反馈。
3.阻止默认行为
代码很简单:
e.preventDefault() //阻止事件的默认行为(如在浏览器打开文件) e.stopPropagation() // 阻止事件冒泡
每个事件阶段都需要阻止,为啥呢举个🌰栗子?
const handleDragOver = (e) => {
// e.preventDefault();
// e.stopPropagation();
};

不阻止的话,就会触发打开文件的行为,这显然不是我们想看到的。

4.组件内部状态:useState
拖拽上传组件,除了基础的拖拽状态控制,还应有成功上传文件或未通过验证时的消息提醒。
状态组成应为:
state = {
dragging: false,
message: {
show: false,
text: null,
type: null,
},
};
对应写成 useState 前先回归下写法:
const [属性, 操作属性的方法] = useState(默认值);
于是便成了:
const [dragging, setDragging] = useState(false);
const [message, setMessage] = useState({ show: false, text: null, type: null });
5.需要第二个叠加层
除了 drop 事件,另外三个事件都是动态变化的,而在拖动元素时,每隔 350 毫秒会触发 dragover 事件。
就此时需要第二 ref 来统一控制。
所以全部的裁判为:
const drop = useRef(); // 落下层 const drag = useRef(); // 拖拽活动层
6.文件类型,数量控制
我们在应用组件时,prop 需要传入类型和数量来控制
<FilesDragAndDrop
onUpload={this.onUpload}
count={1}
formats={['jpg', 'png']}
>
<div className={classList['FilesDragAndDrop__area']}>
传下文件试试?
<span
role='img'
aria-label='emoji'
className={classList['area__icon']}
>
😎
</span>
</div>
</FilesDragAndDrop>
- onUpload:拖拽完成处理事件
- count:数量控制
- formats:文件类型。
对应的组件 Drop 内部事件 handleDrop:
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setDragging(false)
const { count, formats } = props;
const files = [...e.dataTransfer.files];
if (count && count < files.length) {
showMessage(`抱歉,每次最多只能上传${count} 文件。`, 'error', 2000);
return;
}
if (formats && files.some((file) => !formats.some((format) => file.name.toLowerCase().endsWith(format.toLowerCase())))) {
showMessage(`只允许上传 ${formats.join(', ')}格式的文件`, 'error', 2000);
return;
}
if (files && files.length) {
showMessage('成功上传!', 'success', 1000);
props.onUpload(files);
}
};
.endsWith 是判断字符串结尾,如:”abcd”.endsWith(“cd”); // true
showMessage 则是控制显示文本:
const showMessage = (text, type, timeout) => {
setMessage({ show: true, text, type, })
setTimeout(() =>
setMessage({ show: false, text: null, type: null, },), timeout);
};
需要触发定时器来回到初始状态
7.事件在生命周期里的触发与销毁
原本 EventListener 的事件需要在 componentDidMount 添加,在 componentWillUnmount 中销毁:
componentDidMount () {
this.drop.addEventListener('dragover', this.handleDragOver);
}
componentWillUnmount () {
this.drop.removeEventListener('dragover', this.handleDragOver);
}
但 Hooks 中有内部操作方法状语从句:对应 useEffect 来取代上述两个生命周期
useEffect 示例:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
而每个 effect 都可以返回一个清除函数。如此可以将添加(componentDidMount)和移除(componentWillUnmount)订阅的逻辑放在一起。
于是上述就可以写成:
useEffect(() => {
drop.current.addEventListener('dragover', handleDragOver);
return () => {
drop.current.removeEventListener('dragover', handleDragOver);
}
})

完整代码
FilesDragAndDropHook.js:
import React, { useEffect, useState, useRef } from "react";
import PropTypes from 'prop-types';
import classNames from 'classnames';
import classList from '../../scss/components/Common/FilesDragAndDrop.scss';
const FilesDragAndDrop = (props) => {
const [dragging, setDragging] = useState(false);
const [message, setMessage] = useState({ show: false, text: null, type: null });
const drop = useRef();
const drag = useRef();
useEffect(() => {
// useRef 的 drop.current 取代了 ref 的 this.drop
drop.current.addEventListener('dragover', handleDragOver);
drop.current.addEventListener('drop', handleDrop);
drop.current.addEventListener('dragenter', handleDragEnter);
drop.current.addEventListener('dragleave', handleDragLeave);
return () => {
drop.current.removeEventListener('dragover', handleDragOver);
drop.current.removeEventListener('drop', handleDrop);
drop.current.removeEventListener('dragenter', handleDragEnter);
drop.current.removeEventListener('dragleave', handleDragLeave);
}
})
const handleDragOver = (e) => {
e.preventDefault();
e.stopPropagation();
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setDragging(false)
const { count, formats } = props;
const files = [...e.dataTransfer.files];
if (count && count < files.length) {
showMessage(`抱歉,每次最多只能上传${count} 文件。`, 'error', 2000);
return;
}
if (formats && files.some((file) => !formats.some((format) => file.name.toLowerCase().endsWith(format.toLowerCase())))) {
showMessage(`只允许上传 ${formats.join(', ')}格式的文件`, 'error', 2000);
return;
}
if (files && files.length) {
showMessage('成功上传!', 'success', 1000);
props.onUpload(files);
}
};
const handleDragEnter = (e) => {
e.preventDefault();
e.stopPropagation();
e.target !== drag.current && setDragging(true)
};
const handleDragLeave = (e) => {
e.preventDefault();
e.stopPropagation();
e.target === drag.current && setDragging(false)
};
const showMessage = (text, type, timeout) => {
setMessage({ show: true, text, type, })
setTimeout(() =>
setMessage({ show: false, text: null, type: null, },), timeout);
};
return (
<div
ref={drop}
className={classList['FilesDragAndDrop']}
>
{message.show && (
<div
className={classNames(
classList['FilesDragAndDrop__placeholder'],
classList[`FilesDragAndDrop__placeholder--${message.type}`],
)}
>
{message.text}
<span
role='img'
aria-label='emoji'
className={classList['area__icon']}
>
{message.type === 'error' ? <>😢</> : <>😘</>}
</span>
</div>
)}
{dragging && (
<div
ref={drag}
className={classList['FilesDragAndDrop__placeholder']}
>
请放手
<span
role='img'
aria-label='emoji'
className={classList['area__icon']}
>
😝
</span>
</div>
)}
{props.children}
</div>
);
}
FilesDragAndDrop.propTypes = {
onUpload: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
count: PropTypes.number,
formats: PropTypes.arrayOf(PropTypes.string)
}
export { FilesDragAndDrop };
App.js:
import React, { Component } from 'react';
import { FilesDragAndDrop } from '../components/Common/FilesDragAndDropHook';
import classList from '../scss/components/Common/FilesDragAndDrop.scss';
export default class App extends Component {
onUpload = (files) => {
console.log(files);
};
render () {
return (
<FilesDragAndDrop
onUpload={this.onUpload}
count={1}
formats={['jpg', 'png', 'gif']}
>
<div className={classList['FilesDragAndDrop__area']}>
传下文件试试?
<span
role='img'
aria-label='emoji'
className={classList['area__icon']}
>
😎
</span>
</div>
</FilesDragAndDrop>
)
}
}
FilesDragAndDrop.scss:
.FilesDragAndDrop {
position: relative;
.FilesDragAndDrop__placeholder {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
flex-flow: column nowrap;
background-color: #e7e7e7;
border-radius: 12px;
color: #7f8e99;
font-size: 24px;
opacity: 1;
text-align: center;
line-height: 1.4;
&.FilesDragAndDrop__placeholder--error {
background-color: #f7e7e7;
color: #cf8e99;
}
&.FilesDragAndDrop__placeholder--success {
background-color: #e7f7e7;
color: #8ecf99;
}
.area__icon {
font-size: 64px;
margin-top: 20px;
}
}
}
.FilesDragAndDrop__area {
width: 300px;
height: 200px;
padding: 50px;
display: flex;
align-items: center;
justify-content: center;
flex-flow: column nowrap;
font-size: 24px;
color: #555555;
border: 2px #c3c3c3 dashed;
border-radius: 12px;
.area__icon {
font-size: 64px;
margin-top: 20px;
}
}
然后你就可以拿到文件慢慢耍了
文章转载自公众号:前端劝退师
以上关于React 实现一个交互完整的拖拽上传组件 仅需120行代码的文章就介绍到这了,更多相关内容请搜索码云笔记以前的文章或继续浏览下面的相关文章,希望大家以后多多支持码云笔记。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 admin@mybj123.com 进行投诉反馈,一经查实,立即处理!
重要:如软件存在付费、会员、充值等,均属软件开发者或所属公司行为,与本站无关,网友需自行判断
码云笔记 » React 实现一个交互完整的拖拽上传组件 仅需120行代码
微信
支付宝