前端oss实现批量上传文件夹

2,054 阅读3分钟

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

前言

上传文件功能以前都是由前端直接调取后端接口,直接上传的,但是根据测试反馈,会有慢和上传大文件容易崩溃的弊端,于是提出了让前端来实现上传功能,直接上传到阿里oss去,然后再将oss中的存储信息传送给服务端进行处理,这样的优势有,减少了服务端的压力,减少带宽,提高上传速度(根据测试反馈,优化后的确没有奔溃的情况出现,且速度大大提高)。

实现思路

1 获取oss签名

前端传文件名给后端,获取对应的oss签名信息

image.png

image.png 格式如下

    res.data=[{
          bucket_name: ""
          endpoint: ""
          file_name: "xx.png"
          file_url: ""
          key: ""
          oss_access_key_id: ""
          oss_host: "",
          policy: "",
          signature: ""
        }]

根据这些返回,前端调用接口进行文件上传

2 文件上传

      const endpoint = ossToken.endpoint.replace('https://', '');
      let url = `https://${ossToken.bucket_name}.${endpoint}`;
      console.log('url:', url);
      let formData = new FormData();
      //注意formData里append添加的键的大小写
      formData.append('key', ossToken.key); //存储在oss的文件路径
      formData.append('OSSAccessKeyId', ossToken.oss_access_key_id); //accessKeyId
      formData.append('policy', ossToken.policy); //policy
      formData.append('Signature', ossToken.signature); //签名
      formData.append('callback', ossToken.callback); //回调  可有可无 ,根据后端返回
      formData.append('success_action_status', 200); //成功后返回的操作码
      formData.append('file', file);
      axios({
        url: url, //这里的地址就是oss的地址
        method: 'POST',
        data: formData,
        withCredentials: false,
        headers: {
          'Content-Type': 'multipart/form-data',
        },
      }).then(res => {
       if (res.status === 200) {
          //上传成功
       }
      })

具体实现

这个功能笔者实现了三种情况的文件上传:

  • 拖拽上传 --支持拖拽文件,文件夹,同时拖拽多个文件/文件夹
  • 点击上传文件---支持选择多个文件
  • 点击上传文件夹---支持选择多个文件夹上传(文件夹里面必须有文件才会被识别到)
  • 其他,上传文件、文件夹进度,文件、文件夹删除
 <section className={styles['upload_box']}>
      <div
        onDrop={fileDrop}
        className={styles['upload_box-main']}
        onDragOver={fileDragover}
        onClick={changeFiles}
      >
        <Loading loading={loading} />
        <p>点击这里或者把文件拖到这里进行上传</p>
        <form ref={formRef}>
          {selectType == 1 && (
            <input
              type="file"
              multiple={true}
              accept="*"
              onChange={handleInputChange}
              style={{ display: 'none' }}
              ref={inputRef}
            />
          )}
          {selectType == 2 && (
            <input
              type="file"
              multiple
              accept="*/*"
              style={{ display: 'none' }}
              webkitdirectory="true"
              onChange={handleInputChange}
              ref={inputRef2}
            />
          )}
        </form>
      </div>
      <div className={styles['files_list']}>
        {uploadMenuList.map((ele, index) => {
          let icon;
          if (ele.status == 1) {
            icon = success;
          } else if (ele.status == -1) {
            icon = warning;
          }
          let typeicon =
            ele.type == 'pdf'
              ? pdf
              : ele.type == 'audio'
              ? audio
              : ele.type == 'doc'
              ? doc
              : ele.type == 'folder'
              ? folder
              : ele.type == 'image'
              ? image
              : ele.type == 'video'
              ? video
              : ele.type == 'xls'
              ? xls
              : ele.type == 'ppt'
              ? ppt
              : unknown;
          return (
            <div key={index} className={styles['files_list_item']}>
              <div
                className={styles['delete-icon']}
                onClick={() => deleteFile(index)}
              ></div>
              <div className={styles['icon']}>
                <img src={typeicon} alt="" />
              </div>
              <div className={styles['file_detail']}>
                <div className={styles['file_detail_text']}>
                  <div className={styles['file_detail_name']}>{ele.name}</div>
                  <div className={styles['file_detail_size']}>
                    {(ele.size / 1024 / 1024).toFixed(2)}M
                  </div>
                </div>

                <div className={styles['file_detail_progress']}>
                  <Progress
                    percent={ele.progress}
                    showInfo={false}
                    trailColor="#D8D8D8"
                    strokeColor="#5EB4FF"
                  />
                </div>
              </div>
              <div className={styles['file_status']}>
                {icon && <img src={icon} alt="" />}
                {!icon && ele.progress + '%'}
              </div>
            </div>
          );
        })}
      </div>
    </section>

拖拽

//拖拽的处理
const fileDrop = async e => {
    setLoading(true);
    //阻止事件冒泡
    e.stopPropagation();
    //阻止事件的默认行为
    e.preventDefault();
    //储存获取到的文件列表
    let fileList = [];
    let DirectoryEntryList = [];
    //清除样式
    e.target.classList.remove('dropBoxHover');

    if (e.dataTransfer.items) {
      // 拖拽对象列表转换成数组
      let items = new Array(...e.dataTransfer.items);
      // 获得DirectoryEntry对象列表
      for (let index = 0; index < items.length; index++) {
        let e = items[index];
        let item = null;
        //兼容不同内核的浏览器
        if (e.webkitGetAsEntry) {
          item = e.webkitGetAsEntry();
        } else if (e.getAsEntry) {
          item = e.getAsEntry();
        } else {
          console.log('浏览器不支持拖拽上传');
          return;
        }
        DirectoryEntryList.push(item);
      }
      if (DirectoryEntryList.length > 0) {
        for (let index = 0; index < DirectoryEntryList.length; index++) {
          let item = DirectoryEntryList[index];
          if (item) {
            //获取文件夹目录
            let FileTree = await getFileTree(item);

            // 拿到目录下的所有文件
            if (Array.isArray(FileTree)) {
              //展平文件夹
              flattenArray(FileTree, fileList);
            } else {
              //方便后续处理,单文件时也包装成数组
              fileList.push(FileTree);
            }
          }
        }
      }
    }
    console.log('fileList:', fileList);//此时会处理成上传文件的数组  见图3
    //  fileFactory(fileList); 此时可以根据自己的需求对数据做对应的处理 
  };
  
  /**
   * 获取文件
   */
  const fileSync = item => {
    return new Promise((resolve, reject) => {
      item.file(res => {
        resolve(res);
      });
    });
  };

  //读取文件夹下的文件
  const readEntriesSync = dirReader => {
    return new Promise((rel, rej) => {
      dirReader.readEntries(res => {
        rel(res);
      });
    });
  };

  /**
   * 展平数组
   * @param {Array} 需要展平的数组
   * @param {Array} 展平后的数组
   *
   */
  const flattenArray = (array, result) => {
    // console.log(array, flatArray);
    for (let i = 0; i < array.length; i++) {
      if (Array.isArray(array[i])) {
        flattenArray(array[i], result);
      } else {
        result.push(array[i]);
      }
    }
  };

  /**
   * 获取文件目录结构树
   *
   */
  const getFileTree = async item => {
    var path = item.fullPath || '';
    let dir = new Array();
    if (item.isFile) {
      let resFile = await fileSync(item);
      resFile.path = path;
      return resFile;
      // item为文件夹时
    } else if (item.isDirectory) {
      var dirReader = item.createReader();
      let entries = await readEntriesSync(dirReader);
      for (let i = 0; i < entries.length; i++) {
        let proItem = await getFileTree(entries[i]);
        dir.push(proItem);
      }
      return dir;
    }
  };
image.png (图3)

点击选择文件/文件夹上传

 //文件的上传
 <input type="file"  multiple={true} accept="*" onChange={handleInputChange} 
        style={{ display: 'none' }} ref={inputRef}/>
  //文件夹的上传  webkitdirectory 可以识别文件夹     
  <input type="file"  multiple  accept="*/*"
              style={{ display: 'none' }}
              webkitdirectory="true"
              onChange={handleInputChange}
              ref={inputRef2}
            />
  // 触发 input file
  const changeFiles = (e: any) => {
    if (selectType == 1) {
      inputRef.current.click();
    } else {
      inputRef2.current.click();
    }
  };

这三种上传的文件格式path参数有差异,可以对应处理一下,后续做统一上传处理

  • 拖拽后的file格式

image.png

  • 点击后上传file格式

image.png

  • 点击后上传文件夹file格式,里面一个文件返回一个file对象

image.png

处理后就可以回到开头的获取签名,上传成功后记录对应的文件列表 我处理之后的格式是这样,上传了两个图片一个文件夹

image.png image.png

后续对这个数据进行处理,文件夹调用后端的生成目录接口,对应下级文件调用生成文件素材的接口,此处不赘述了。(花了3,4天搞这个功能,后面也懒得优化了,暂时先这样了)