原生js(promise+ reduce)实现图片链式上传功能!

80 阅读2分钟

1、应用场景

在实现多张图片上传的需求中,例如相册上传、发帖传照片等,实现照片多选以及链式上传效果。

2、实现思路

例如你有一组接口需要串行执行,首先你可能会想到使用await

const requestAry = [() => api.request1(), () => api.request2(), () => api.request3()];
for (const requestItem of requestAry) {
  await requestItem();
}

但是如果使用promise的写法,那么你可以使用then函数来串联多个promise,从而实现串行执行。

const requestAry = [() => api.request1(), () => api.request2(), () => api.request3()];
const finallyPromise = requestAry.reduce(
    (currentPromise, nextRequest) => currentPromise.then(() => nextRequest()),
    Promise.resolve() // 创建一个初始promise,用于链接数组内的promise
);

由此操作为基础,实现所需UI效果

3、具体实现代码(vue2)

  import tiny from "./tinyImage";
  import { mkQiniuPublicUrl } from "@/api/common";
  export default {
    props: {
      fileList: {
        type: Array,
        default() {
          return [];
        }
      },
      limit: Number,
      disabled: {
        type: Boolean,
        default: false
      }
    },
    data() {
      return {
        images: [],
        mime: ["image/*"],
        uploadLength: 0,
        isUploading: false
      };
    },
    watch: {
      fileList: {
        handler(){
          if (!this.images.length) {
            this.images = this.fileList;
          }
        },
        immediate: true
      }
    },
    beforeDestroy() {
      // 需要在销毁这个组件之前,释放掉图片占用的内容
      this.images.forEach(img => img.src && URL.revokeObjectURL(img.src));
    },
    methods: {
      // 删除图片的操作
      actClose(index) {
        // 禁用状态不可删除
        if (this.disabled) return;
        // 上传状态不可删除
        if (this.isUploading) {
          this.$toast("正在上传,请勿操作");
          return;
        }
        this.images[index].src && URL.revokeObjectURL(this.images[index].src);
        this.images.splice(index, 1);
        this.triggerChange();
      },
      // 接口上传的方法
      apiUpload(file, local) {
        return async () => {
          let formData = new FormData();
          const compressFile = await tiny.compress(file);
          formData.append("img", compressFile);
          return new Promise((resolve, reject) => {
            mkQiniuPublicUrl(formData, (cur, total) => {
            // 七牛云API
              local.percent = Math.ceil((cur / total) * 100);
            })
              .then(({ errorCode, errorMsg, responseData }) => {
                if (errorCode === 0) {
                  const { url, width, height } = responseData;
                  local.u = url;
                  local.w = width;
                  local.h = height;
                  resolve();
                } else {
                  resolve();
                  this.$toast(errorMsg);
                }
              })
              .catch(() => {
                reject("网络请求失败");
              })
              .finally(() => {
                local.percent = 100
                if (0 === --this.uploadLength) {
                  this.triggerChange();
                  setTimeout(() => {
                    this.isUploading = false;
                    this.target.value = "";
                  }, 0);
                }
              });
          });
        };
      },
      // 触发变更
      triggerChange() {
        this.$emit(
          "change",
          this.images.map(item => ({
            u: item.u,
            h: item.h,
            w: item.w
          }))
        );
      },
      // 本地图片change后立马预览
      imageViewLocal(file) {
        const tmp = {
          src: URL.createObjectURL(file),
          percent: 0,
          u: "",
          h: 0,
          w: 0
        };
        this.images.push(tmp);
        return tmp;
      },
      // 修建修改之后
      fileChangeHandler(e) {
        const target = (this.target = e.target);
        if (!target.files || target.files.length == 0) return;
        this.uploadLength = target.files.length;
        // 上传的文件长度 + 目前已经有的文件长度 总和
        if (this.uploadLength + this.images.length > this.limit) {
          this.$toast(`不能超过${this.limit}张图片`);
          this.uploadLength = 0;
          return;
        }
        const promiseArr = [];
        Object.keys(target.files).forEach(k => {
          let file = target.files[k];
          promiseArr.push(this.apiUpload(file, this.imageViewLocal(file)));
        });

        // 开始上传
        this.isUploading = true;
        promiseArr.reduce((acc, cur) => {
          return acc.finally(() => cur());
        }, Promise.resolve());
      }
    },
    computed: {
      enabledUpload() {
        return this.images.length < this.limit;
      }
    }
  };

html和css如下图 image.png

.upload-wrap {
    position: relative;
    display: flex;
    justify-content: flex-start;
    flex-wrap: wrap;
    overflow: hidden;

    .upload-item {
      position: relative;
      margin-right: 8px;
      margin-bottom: 10px;
      width: 80px;
      height: 80px;
      background: #efefef;
      border-radius: 8px;
      overflow: hidden;
      &:nth-child(4n) {
        margin-right: 0;
      }
      .image {
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        width: 100%;
        z-index: 1;
      }
      .close {
        position: absolute;
        right: 0;
        top: 0;
        width: 22px;
        height: 22px;
        z-index: 3;
      }
      .loading {
        position: absolute;
        left: 0;
        top: 0;
        right: 0;
        bottom: 0;
        z-index: 4;
        background: rgba(0, 0, 0, 0.65);
        .loading-progress {
          width: 40px;
          height: 6px;
          position: absolute;
          top: 50%;
          left: 50%;
          transform: translate3d(-50%, -50%, 0);
          background: #ccc;
          border-radius: 3px;
          .progress {
            width: 0;
            height: 100%;
            border-radius: 3px;
            background: #fff;
          }
        }
      }
    }
    .pub-upload {
      position: relative;
      width: 80px;
      height: 80px;
      margin-bottom: 10px;
      overflow: hidden;
      border-radius: 8px;
      input {
        opacity: 0;
        position: absolute;
        height: 100%;
        width: 100%;
        left: 0;
        top: 0;
        right: 0;
        bottom: 0;
        z-index: 2;
      }
      .img {
        position: absolute;
        height: 100%;
        width: 100%;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
      }
    }
  }

    /* prettier-ignore */
  @media screen and (min-width: 414px) and (orientation: landscape) {
 .upload-wrap {
    .upload-item {
      margin-right: 8PX;
      margin-bottom: 10PX;
      width: 80PX;
      height: 80PX;
      border-radius: 8PX;
      &:nth-child(4n) {
        margin-right: 8PX;
      }
      .close {
        width: 22PX;
        height: 22PX;
        cursor: pointer;
        transition: all .3s;
        &:hover{
          transform: scale(1.1,1.1);
        }
      }
      .loading {
        .loading-progress {
          width: 40PX;
          height: 6PX;
          border-radius: 3PX;
          .progress {
            border-radius: 3PX;
          }
        }
      }
    }
    .pub-upload {
      width: 80PX;
      height: 80PX;
      margin-bottom: 10PX;
      border-radius: 8PX;
      input{
        cursor: pointer;
      }
    }
  }
}