当前位置 博文首页 > Python基于React-Dropzone实现上传组件的示例代码

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

    作者:DisonTangor 时间:2021-09-18 17:47

    目录
    • 实例演示
      • 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的方法每一个用户做文件种子,提供文件传输的辅助,目前国内并没有普及使用。

      jsjbwy
      下一篇:没有了