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

编辑: admin 分类: python 发布时间: 2021-12-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的方法每一个用户做文件种子,提供文件传输的辅助,目前国内并没有普及使用。

    到此这篇关于Python基于React-Dropzone实现上传组件的示例代码的文章就介绍到这了,更多相关Python React-Dropzone上传组件内容请搜索hwidc以前的文章或继续浏览下面的相关文章希望大家以后多多支持hwidc!

    【本文来源:高防服务器 转载请保留连接】