码云笔记前端博客
Home > 前端技术 > React 实现一个交互完整的拖拽上传组件 仅需120行代码

React 实现一个交互完整的拖拽上传组件 仅需120行代码

2019-08-29 分类:前端技术 作者:码云 阅读(264)

本文共计7799个字,阅读时间预计20分钟,干货满满,记得点赞加收藏哦

你将在该篇学到:

  • 将如何现有组件改写为React Hooks函数组件
  • useState,useEffect,useRef的英文如何替代原生命周期状语从句:Ref的。
  • 一个完整拖拽上传行为覆盖的四个事件:dragover,dragenter,drop,dragleave
  • 使用如何React Hooks关系编写自己的UI组件库。

逛国外社区时看到这篇:
实现一个交互完整的拖拽上传组件 仅需120行代码
如何在React中实现拖放文件
讲文章了React拖拽上传的精简实现,但直接翻译照搬显然不是我的风格。

我于是汉语中类似的用React Hooks重写了一版,除CSS的代码总数120行。

效果如下:
实现一个交互完整的拖拽上传组件 仅需120行代码

添加基本目录骨架

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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(非钩):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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'
                >
                    &#128526;
               </span>
            </div>
        );
    }
}

1.如何改写为Hooks组件?

请看动图:
实现一个交互完整的拖拽上传组件 仅需120行代码
如何改写为Hooks组件?
2.改写组件

Hooks版组件属于函数组件,将以上改造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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'
            >
                &#128526;
           </span>
        </div>
    );
}

FilesDragAndDrop.propTypes = {
    onUpload: PropTypes.func.isRequired,
    children: PropTypes.node.isRequired,
    count: PropTypes.number,
    formats: PropTypes.arrayOf(PropTypes.string)
}

export { FilesDragAndDrop };

FilesDragAndDrop.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.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;
    }
  }
}

然后就可以看到页面:
Hooks版组件属于函数组件

实现分析

从操作DOM,组件复用,事件触发,阻止默认行为,以及Hooks应用方面分析。

1.操作DOM:`useRef`

由于需要拖拽文件上传以及操作组件实例,用到需要ref属性。

React Hooks中新增了useRef API

语法

1
const refContainer = useRef(initialValue);
  • useRef一个报道查看可变的ref对象。
  • 其.current属性被初始化为传递的参数(initialValue)
  • 返回的对象将存留在整个组件的生命周期中。
1
2
3
4
5
6
7
8
9
10
...
const drop = useRef();

return (
    <div
        ref={drop}
        className='FilesDragAndDrop'
    />
    ...
    )

2.事件触发
实现一个交互完整的拖拽上传组件 仅需120行代码
完成具有动态交互的拖拽行为并不简单,需要用到四个事件控制:

  • 区域外:dragleave,离开范围
  • 区域内:dragenter,用来确定放置目标是否接受放置。
  • 区域内移动:dragover,用来确定给用户显示怎样的反馈信息
  • 完成拖拽(落下)drop:,允许放置对象。

这四个事件并存,才能阻止Web浏览器默认行为和形成反馈。

3.阻止默认行为

代码很简单:

1
2
e.preventDefault() //阻止事件的默认行为(如在浏览器打开文件)
e.stopPropagation() // 阻止事件冒泡

每个事件阶段都需要阻止,为啥呢举个🌰栗子?

1
2
3
4
const handleDragOver = (e) => {
    // e.preventDefault();
    // e.stopPropagation();
};

实现一个交互完整的拖拽上传组件 仅需120行代码
不阻止的话,就会触发打开文件的行为,这显然不是我们想看到的。
实现一个交互完整的拖拽上传组件 仅需120行代码
4.组件内部状态:useState

拖拽上传组件,除了基础的拖拽状态控制,还应有成功上传文件或未通过验证时的消息提醒。

状态组成应为:

1
2
3
4
5
6
7
8
state = {
    dragging: false,
    message: {
        show: false,
        text: null,
        type: null,
    },
};

对应写成useState前先回归下写法:

1
const [属性, 操作属性的方法] = useState(默认值);

于是便成了:

1
2
const [dragging, setDragging] = useState(false);
const [message, setMessage] = useState({ show: false, text: null, type: null });

5.需要第二个叠加层

除了drop事件,另外三个事件都是动态变化的,而在拖动元素时,每隔350毫秒会触发dragover事件。

就此时需要第二ref来统一控制。

所以全部的裁判为:

1
2
const drop = useRef(); // 落下层
const drag = useRef(); // 拖拽活动层

6.文件类型,数量控制

我们在应用组件时,prop需要传入类型和数量来控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<FilesDragAndDrop
    onUpload={this.onUpload}
    count={1}
    formats={['jpg', 'png']}
>
    <div className={classList['FilesDragAndDrop__area']}>
        传下文件试试?
<span
            role='img'
            aria-label='emoji'
            className={classList['area__icon']}
        >
            &#128526;
</span>
    </div>
</FilesDragAndDrop>
  • onUpload:拖拽完成处理事件
  • count:数量控制
  • formats:文件类型。

对应的组件Drop内部事件handleDrop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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则是控制显示文本:

1
2
3
4
5
const showMessage = (text, type, timeout) => {
    setMessage({ show: true, text, type, })
    setTimeout(() =>
        setMessage({ show: false, text: null, type: null, },), timeout);
};

需要触发定时器来回到初始状态

7.事件在生命周期里的触发与销毁

原本EventListener的事件需要在componentDidMount添加,在componentWillUnmount中销毁:

1
2
3
4
5
6
7
componentDidMount () {
    this.drop.addEventListener('dragover', this.handleDragOver);
}

componentWillUnmount () {
    this.drop.removeEventListener('dragover', this.handleDragOver);
}

但Hooks中有内部操作方法状语从句:对应useEffect来取代上述两个生命周期

useEffect示例:

1
2
3
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

而每个effect都可以返回一个清除函数。如此可以将添加(componentDidMount)和移除(componentWillUnmount)订阅的逻辑放在一起。

于是上述就可以写成:

1
2
3
4
5
6
useEffect(() => {
    drop.current.addEventListener('dragover', handleDragOver);
    return () => {
        drop.current.removeEventListener('dragover', handleDragOver);
    }
})

useEffect示例

完整代码

FilesDragAndDropHook.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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' ? <>&#128546;</> : <>&#128536;</>}
                   </span>
                </div>
            )}
            {dragging && (
                <div
                    ref={drag}
                    className={classList['FilesDragAndDrop__placeholder']}
                >
                    请放手
                    <span
                        role='img'
                        aria-label='emoji'
                        className={classList['area__icon']}
                    >
                        &#128541;
                   </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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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']}
                    >
                        &#128526;
           </span>
                </div>
            </FilesDragAndDrop>
        )
    }
}

FilesDragAndDrop.scss:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
.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;
  }
}

然后你就可以拿到文件慢慢耍了

文章转载自公众号:前端劝退师

「除特别注明外,本站所有文章均为码云笔记原创,转载请保留出处!」

赞(9) 打赏

觉得文章有用就打赏一下文章作者

支付宝
微信
9

觉得文章有用就打赏一下文章作者

支付宝
微信

上一篇:

下一篇:

你可能感兴趣

共有 0 条评论 - React 实现一个交互完整的拖拽上传组件 仅需120行代码

博客简介

码云笔记 mybj123.com,一个专注Web前端开发技术的博客,主要记录和总结博主在前端开发工作中常用的实战技能及前端资源分享,分享各种科普知识和实用优秀的代码,以及分享些热门的互联网资讯和福利!码云笔记有你更精彩!
更多博客详情请看关于博客

精彩评论

站点统计

  • 文章总数: 472 篇
  • 分类数目: 13 个
  • 独立页面: 8 个
  • 评论总数: 228 条
  • 链接总数: 15 个
  • 标签总数: 1036 个
  • 建站时间: 522 天
  • 访问总量: 8681301 次
  • 最近更新: 2019年11月18日