你将在该篇学到:
React Hooks
函数组件useState
、useEffect
、useRef
是如何替代原生命周期和Ref
的。dragover
、dragenter
、drop
、dragleave
React Hooks
编写自己的UI组件库。How To Implement Drag and Drop for Files in 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 (
);
}
}
FilesDragAndDrop.js(非Hooks):
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 (
传下文件试试?
😎
);
}
}
Hooks
组件?请看动图:
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 (
传下文件试试?
😎
);
}
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
应用方面分析。
useRef
由于需要拖拽文件上传以及操作组件实例,需要用到ref
属性。
React Hooks
中 新增了useRef API
语法
const refContainer = useRef(initialValue);
useRef
返回一个可变的 ref
对象,。initialValue
)...
const drop = useRef();
return (
...
)
dragleave
, 离开范围dragenter
,用来确定放置目标是否接受放置。dragover
,用来确定给用户显示怎样的反馈信息drop
,允许放置对象。这四个事件并存,才能阻止 Web 浏览器默认行为和形成反馈。
代码很简单:
e.preventDefault() //阻止事件的默认行为(如在浏览器打开文件)
e.stopPropagation() // 阻止事件冒泡
每个事件阶段都需要阻止,为啥呢?举个?栗子:
const handleDragOver = (e) => {
// e.preventDefault();
// e.stopPropagation();
};
不阻止的话,就会触发打开文件的行为,这显然不是我们想看到的。
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 });
除了drop
事件,另外三个事件都是动态变化的,而在拖动元素时,每隔 350
毫秒会触发 dragover
事件。
此时就需要第二ref
来统一控制。
所以全部的`ref``为:
const drop = useRef(); // 落下层
const drag = useRef(); // 拖拽活动层
我们在应用组件时,prop
需要传入类型和数量来控制
传下文件试试?
😎
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);
};
需要触发定时器来回到初始状态
原本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 (
{message.show && (
{message.text}
{message.type === 'error' ? <>😢> : <>😘>}
)}
{dragging && (
请放手
😝
)}
{props.children}
);
}
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.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;
}
}
然后你就可以拿到文件慢慢耍了。。。