Python基于React-Dropzone实现上传组件的示例代码

作者:DisonTangor 时间:2021-04-01 19:53:03 

目录
  • 实例演示

    • 1. axios上传普通文件:

    • 2. 大文件导入:

  • 结语

    这次我要讲述的是在React-Flask框架上开发上传组件的技巧。我目前主要以React开发前端,在这个过程中认识到了许多有趣的前端UI框架——React-Bootstrap、Ant Design、Material UI、Bulma等。而比较流行的上传组件也不少,而目前用户比较多的是jQuery-File-Upload和Dropzone,而成长速度快的新晋有Uppy和filepond。比较惋惜的是Fine-Uploader的作者自2018年后就决定不再维护了,原因作为后来者的我就不多过问了,但请各位尊重每一位开源作者的劳动成果。

    这里我选择React-Dropzone,原因如下:

    • 基于React开发,契合度高

    • 网上推荐度高,连Material UI都用他开发上传组件

    • 主要以 Drag 和 Drop 为主,但是对于传输逻辑可以由开发者自行设计。例如尝试用socket-io来传输file chunks。对于node全栈估计可行,但是我这里使用的是Flask,需要将Blob转ArrayBuffer。但是如何将其在Python中读写,我就没进行下去了。

    实例演示

    1. axios上传普通文件:

    通过yarn将react-dropzone和引入:


    yarn add react-dropzone axios

    前端js如下(如有缺失,请自行修改):


    import React, {
       useState,
       useCallback,
       useEffect,
    } from 'react';
    import {useDropzone} from 'react-dropzone';
    import "./dropzone.styles.css"
    import InfiniteScroll from 'react-infinite-scroller';
    import {
       List,
       message,
       // Avatar,
       Spin,
    } from 'antd';
    import axios from 'axios';

    /**
    * 计算文件大小
    * @param {*} bytes
    * @param {*} decimals
    * @returns
    */
    function formatBytes(bytes, decimals = 2) {
       if (bytes === 0) return '0 Bytes';

    const k = 1024;
       const dm = decimals < 0 ? 0 : decimals;
       const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];

    const i = Math.floor(Math.log(bytes) / Math.log(k));

    return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
    }

    /**
    * Dropzone 上传文件
    * @param {*} props
    * @returns
    */
    function DropzoneUpload(props) {
       const [files, setFiles] = useState([])
       const [loading, setLoading] = useState(false);
       const [hasMore, setHasMore] = useState(true);

    const onDrop = useCallback(acceptedFiles => {
           setLoading(true);
           const formData = new FormData();
           smallFiles.forEach(file => {
               formData.append("files", file);
           });
           axios({
               method: 'POST',
               url: '/api/files/multiplefiles',
               data: formData,
               headers: {
                   "Content-Type": "multipart/form-data",
               }
           })
           then(resp => {
               addFiles(acceptedFiles);
               setLoading(false);
           });
       }, [files]);

    // Dropzone setting
       const { getRootProps, getInputProps } = useDropzone({
           multiple:true,
           onDrop,
       });

    // 删除附件
       const removeFile = file => {
           const newFiles = [...files]
           newFiles.splice(newFiles.indexOf(file), 1)
           setFiles(newFiles)
       }

    useEffect(() => {
           // init uploader files
           setFiles([])
       },[])

    return (
           <section className="container">
           <div {...getRootProps({className: 'dropzone'})}>
               <input {...getInputProps()} />
               <p>拖动文件或点击选择文件😊</p>
           </div>

    <div className="demo-infinite-container">
               <InfiniteScroll
                   initialLoad={false}
                   pageStart={0}
                   loadMore={handleInfiniteOnLoad}
                   hasMore={!loading && hasMore}
                   useWindow= {false}
               >
                   <List
                       dataSource={files}
                       renderItem={item=> (
                           <List.Item
                               actions={[
                                   // <a key="list-loadmore-edit">编辑</a>,
                                   <a key="list-loadmore-delete" onClick={removeFile}>删除</a>
                               ]}
                               // extra={

    // }
                               key={item.path}>
                               <List.Item.Meta
                                   avatar={
                                       <>
                                       {
                                           !!item.type && ['image/gif', 'image/jpeg', 'image/png'].includes(item.type) &&
                                           <img
                                               width={100}
                                               alt='logo'
                                               src={item.preview}
                                           />
                                       }
                                       </>
                                   }
                                   title={item.path}
                                   description={formatBytes(item.size)}
                               />
                           </List.Item>
                       )}
                   >
                       {loading && hasMore && (
                           <div className="demo-loading-container">
                               <Spin />
                           </div>
                       )}
                   </List>
               </InfiniteScroll>
           </div>
           </section>
       );
    }

    flask代码:


    def multiplefiles():
    if 'files' not in request.files:
       return jsonify({'message': '没有文件!'}), 200
    files = request.files.getlist('files')

    for file in files:
       if file:
           # 通过拼音解决secure_filename中文问题
           filename = secure_filename(''.join(lazy_pinyin(file.filename))
           Path(UPLOAD_FOLDER + '/' + file_info['dir_path']).mkdir(parents=True, exist_ok=True)
           file.save(os.path.join(UPLOAD_FOLDER + '/' + file_info['dir_path'], filename))

    return jsonify({'message': '保存成功!!'})

    2. 大文件导入:

    通过file.slice()方法生成文件的chunks。不要用Promise.all容易产生非顺序型的请求,导致文件损坏。

    js代码:


    const promiseArray = largeFiles.map(file => new Promise((resolve, reject) => {

    const chunkSize = CHUNK_SIZE;
       const chunks = Math.ceil(file.size / chunkSize);
       let chunk = 0;
       let chunkArray = new Array();
       while (chunk <= chunks) {
           let offset = chunk * chunkSize;
           let slice = file.slice(offset, offset+chunkSize)
           chunkArray.push([slice, offset])
           ++chunk;
       }
       const chunkUploadPromises = (slice, offset) => {
           const largeFileData = new FormData();
           largeFileData.append('largeFileData', slice)
           return new Promise((resolve, reject) => {
               axios({
                   method: 'POST',
                   url: '/api/files/largefile',
                   data: largeFileData,
                   headers: {
                       "Content-Type": "multipart/form-data"
                   }
               })
               .then(resp => {
                   console.log(resp);
                   resolve(resp);
               })
               .catch(err => {
                   reject(err);
               })
           })
       };

    chunkArray.reduce( (previousPromise, [nextChunk, nextOffset]) => {
           return previousPromise.then(() => {
               return chunkUploadPromises(nextChunk, nextOffset);
           });
       }, Promise.resolve());
       resolve();
    }))

    flask代码:


    filename = secure_filename(''.join(lazy_pinyin(filename)))
    Path(UPLOAD_FOLDER + '/' + file_info['dir_path']).mkdir(parents=True, exist_ok=True)
    save_path = os.path.join(UPLOAD_FOLDER + '/' + file_info['dir_path'], filename)
    # rm file if exists
    if offset == 0 and save_path.exists(filename):
       os.remove(filename)
    try:
       with open(save_path, 'ab') as f:
           f.seek(offset)
           f.write(file.stream.read())
           print("time: "+ str(datetime.now())+" offset: " + str(offset))
    except  OSError:
       return jsonify({'Could not write to file'}), 500

    结语

    文件传输一直都是HTTP的痛点,尤其是大文件传输。最好的方式是自己做个Client,通过FTP和FTPS的协议进行传输。第二种来自于大厂很中心化的方法,通过文件的checksum来确定文件是否已经上传了,来营造秒传的效果。第三种来自去中心化的Bittorrent的方法每一个用户做文件种子,提供文件传输的辅助,目前国内并没有普及使用。

    来源:https://www.cnblogs.com/DisonTangor/p/15143516.html

    标签:Python,React-Dropzone,上传组件
    0
    投稿

    猜你喜欢

  • 用javascript实现的汉字简繁转换功能

    2008-05-04 13:15:00
  • 解决用CSS控制DIV居中失效的问题

    2010-04-05 21:53:00
  • python解析xml文件方式(解析、更新、写入)

    2022-03-07 05:19:01
  • python私有属性和方法实例分析

    2023-11-21 06:16:13
  • Python压缩和解压缩zip文件

    2023-09-16 21:20:10
  • 网马解密大讲堂——网马解密初级篇

    2009-09-16 14:45:00
  • Python读取文件内容的三种常用方式及效率比较

    2023-08-29 23:46:00
  • asp最简单的生成验证码代码

    2011-03-07 11:05:00
  • 如何处理好网页色彩搭配

    2007-08-10 13:22:00
  • 关于ThinkPhp 框架表单验证及ajax验证问题

    2023-11-15 06:33:05
  • Python格式化日期时间操作示例

    2022-04-23 23:07:19
  • SWF FLASH的param属性参数详解

    2008-10-25 15:12:00
  • 利用Python实现简单的相似图片搜索的教程

    2023-10-24 18:45:36
  • javascript让浏览器实现复读机的功能

    2008-10-10 11:49:00
  • PHP制作3D扇形统计图以及对图片进行缩放操作实例

    2023-11-17 19:31:47
  • php的RSA加密解密算法原理与用法分析

    2023-07-13 11:27:14
  • 一篇文章带你学习Python3的高级特性(1)

    2021-09-24 04:39:01
  • 两个css郁闷的发现

    2007-12-16 15:31:00
  • python实现获取单向链表倒数第k个结点的值示例

    2022-10-12 17:38:10
  • 另外一种斜体的导航条

    2008-11-05 12:24:00
  • asp之家 网络编程 m.aspxhome.com