前端大文件上传

设置axios的统一配置信息

let instance = axios.create({
  baseURL: "http://127.0.0.1:8888",
  headers: { "Content-Type": "multipart/form-data" },
  transformRequest: (data, headers) => {
    // data就是请求主体数据
    // headers就是设置的请求头Content-Type
    if (headers["Content-Type"] === "application/x-www-form-urlencoded") {
      //   是该请求头就将请求主体的对象格式转换为url格式
      return Qs.stringify(data);
    }
    // 否则不进行处理
    return data;
  },
});

//上面代码等价于
// axios.defaults.baseURL = "http://127.0.0.1:8888";
// // 默认传输文件的请求头
// axios.defaults.headers["Content-Type"] = "mutipart/form-data";
// // `transformRequest` 允许在向服务器发送前,修改请求数据
// axios.defaults.transformRequest = (data, headers) => {
//   // data就是请求主体数据
//   // headers就是设置的请求头Content-Type
//   if (headers["Content-Type"] === "application/x-www-form-urlencoded") {
//     //   是该请求头就将请求主体的对象格式转换为url格式
//     return Qs.stringify(data);
//   }
//   // 否则不进行处理
//   return data;
// };

instance.interceptors.response.use(
  function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    return response.data;
  },
  function (error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error);
  }
);

FormData上传

文件上次使用的是input标签中的type=file属性,但是原生的文件按钮工具难以设置样式等,所以都将原生的文件工具隐藏,自定义一个按钮,在点击自定义按钮的时候触发文件按钮。
第一个文件上次案例的结构代码如下

      <div class="item">
        <h3>单一文件上传「FORM-DATA</h3>
        <section class="upload_box" id="upload1">
          <!-- accept=".png" 限定上传文件的格式 -->
          <input type="file" class="upload_inp"  />
          <div class="upload_button_box">
            <button class="upload_button select">选择文件</button>
            <button class="upload_button upload">上传到服务器</button>
          </div>
          <div class="upload_tip">
            只能上传 PNG/JPG/JPEG 格式图片,且大小不能超过2MB
          </div>
          <ul class="upload_list">
            <!-- <li>
                        <span>文件:...</span>
                        <span><em>移除</em></span>
                    </li> -->
          </ul>
        </section>
      </div>

在这里插入图片描述
通过闭包处理不同案例的文件上次代码,以下基本代码中,提供了如何获取选取的文件信息。通过文件元素身上的files属性获取,该值是一个类数组

(function () {
  let upload1 = document.querySelector("#upload1"),
    upload_inp = upload1.querySelector(".upload_inp"),
    upload_inp_select = upload1.querySelector(".upload_button.select"),
    upload_inp_upload = upload1.querySelector(".upload_button.upload"),
    upload_tip = upload1.querySelector(".upload_tip"),
    upload_list = upload1.querySelector(".upload_list");

  // 给选择文件按钮绑定时间事件
  upload_inp_select.addEventListener("click", () => {
    //   触发自带的点击方法选取文件
    upload_inp.click();
  });

  upload_inp.addEventListener("change", () => {
    //   文件选取需要基于change事件完成
    console.log(upload_inp.files);
    //文件校验等
  });
})();

在这里插入图片描述
如上图所示,一个文件对象包含的熟悉中type对应文件的类型,size单位是字节对于文件的大小,name对象文件的名字,这些都可以作为文件校验的工具使用。

// 校验文件的类型和大小
    let file = upload_inp.files[0];
    if (!/(png|jpe?g)/i.test(file.type)) {
      alert("请选择png或者jpg或者jpeg格式的图片");
      return;
    }
     if (file.size > 2 * 1024 * 1024) {
      alert("图片大小不能超过2M");
      return;
    }

当然也可以使用file文件标签类型提供的accept属性自定义设置accept=".png,.jpg,.jpeg"
当选取文件的校验通过后,需要设置已选取文件的列表显示。和删除已选确定文件列表。因为删除按钮设计到元素的动态添加和删除,因此采用事件委托的方式处理。

  upload_inp.addEventListener("change", () => {
    ....
    upload_tip.style.display = "none";
    upload_list.style.display = "block";
    upload_list.innerHTML = `
        <li>
            <span>文件:${file.name}</span>
            <span><em>移除</em></span>
        </li>
      `;
  });
  upload_list.addEventListener("click", (e) => {
    //   点击的是文件的删除按钮
    if (e.target.tagName === "EM") {
      // 显示提示
      upload_tip.style.display = "block";
      // 删除文件列表显示
      upload_list.style.display = "none";
      upload_list.innerHTML = ``;
    }
  });

然后针对文件上传按钮进行处理

   // 全局创建file变量
   let _file = null;
  upload_inp.addEventListener("change", () => {
    //   文件选取需要基于change事件完成
    let file = upload_inp.files[0];
    _file = file;
    .....
   }
  upload_list.addEventListener("click", (e) => {
    //   点击的是文件的删除按钮
    if (e.target.tagName === "EM") {
      // 移除文件了删除公共状态中保存的file对象
      _file = null;
      .....
    }
  });
  
  // 文件上传按钮
  upload_inp_upload.addEventListener("click", async () => {
    // 需要获取文件对象
    // 当没有选择文件的时候或者选择了文件又删除的情况下,不允许上传,_file=null
    if (!_file) {
      alert("请选择文件");
      return;
    }
    let fm = new FormData();
    // 后端服务器需要字段file和filename
    fm.append("file", _file);
    fm.append("filename", _file.name);
    try {
      let { code, servicePath } = await instance.post("/upload_single", fm);
      if (+code === 0) {
        alert(`上传成功:文件服务器地址${servicePath}`);
        return;
      }
    } catch (error) {
      console.log(error);
      alert("请重新上传!");
    }
  });

出现如图代表成功,这样子上传的文件都会出现在服务端的upload文件夹,然后上传文件成功之后,需要做一些事情,比如将上传的文件列表清空等。最主要的是给正在上传中的按钮设置样式,还有节流防抖效果
在这里插入图片描述
提取公共的代码部分

  const clearHandle = () => {
    // 移除文件了删除公共状态中保存的file对象
    _file = null;
    // 显示提示
    upload_tip.style.display = "block";
    // 删除文件列表显示
    upload_list.style.display = "none";
    upload_list.innerHTML = ``;
  };

单独编写处理一个样式类的代码

  const handleStyle = (flag) => {
    // flag为真代表选择文件按钮需要添加disable样式,上传服务器按钮需要添加loading样式
    if (flag) {
      upload_inp_select.classList.add("disable");
      upload_inp_upload.classList.add("loading");
    } else {
      upload_inp_select.classList.remove("disable");
      upload_inp_upload.classList.remove("loading");
    }
  };
  upload_inp_upload.addEventListener("click", async () => {
    // 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
    if (
      upload_inp_select.classList.contains("disable") ||
      upload_inp_upload.classList.contains("loading")
    ) {
      return;
    }
    // 需要获取文件对象
    // 当没有选择文件的时候或者选择了文件又删除的情况下,不允许上传,_file=null
    if (!_file) {
      alert("请选择文件");
      return;
    }
    // 设置按钮样式
    handleStyle(true);
    let fm = new FormData();
    // 后端服务器需要字段file和filename
    fm.append("file", _file);
    fm.append("filename", _file.name);
    try {
      let { code, servicePath } = await instance.post("/upload_single", fm);
      if (+code === 0) {
        alert(`上传成功:文件服务器地址${servicePath}`);
        return;
      }
    } catch (error) {
      console.log(error);
      alert("请重新上传!");
    } finally {
      // 统一调用公共逻辑代码部分
      clearHandle();
      // 设置按钮样式
      handleStyle(false);
    }
  });

其次选择文件按钮还需要做节流防抖处理

  upload_inp_select.addEventListener("click", () => {
    // 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
    if (
      upload_inp_select.classList.contains("disable") ||
      upload_inp_upload.classList.contains("loading")
    ) {
      return;
    }

    //   触发自带的点击方法选取文件
    upload_inp.click();
  });

Base64上传

      <div class="item">
        <h3>单一文件上传「BASE64」,只适合图片</h3>
        <section class="upload_box" id="upload2">
          <input type="file" class="upload_inp" accept=".jpg,.jpeg,.png" />
          <div class="upload_button_box">
            <button class="upload_button select">上传图片</button>
          </div>
          <div class="upload_tip">
            只能上传jpg/png格式图片,且大小不能超过2mb
          </div>
        </section>
      </div>

基本的js代码

// Base64文件上传
(function () {
  let upload2 = document.querySelector("#upload2"),
    upload_inp = upload2.querySelector(".upload_inp"),
    upload_inp_select = upload2.querySelector(".upload_button.select");
  let _file = null;

  const isGo = (ele) => {
    return (
      ele.classList.contains("disable") || ele.classList.contains("loading")
    );
  };
  // 给选择文件按钮绑定时间事件
  upload_inp_select.addEventListener("click", function () {
    // 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
    if (isGo(this)) return;
    //   触发自带的点击方法选取文件
    upload_inp.click();
  });

  upload_inp.addEventListener("change", () => {
    //   文件选取需要基于change事件完成
    let file = upload_inp.files[0];
    _file = file;
    if (!/(png|jpe?g)/i.test(file.type)) {
      alert("请选择png或者jpg或者jpeg格式的图片");
      return;
    }
    if (file.size > 2 * 1024 * 1024) {
      alert("图片大小不能超过2M");
      return;
    }
  });
})();

将文件转换为Base64借助FileReader实例创建。然后通过实例身上的readAsDataURL(文件)方法异步创建base64文件,该实例身上还有其他方法可以创建二进制等文件信息。区分URL.createObjectURL(blob)可以获取当前文件的一个内存URL。FileReader创建文件操作是异步的,如果下面代码底部有一个同步输出语句,希望文件操作输出base64文件后再输出,会发现结果和预想的不一样,会先输出111后输出文件内容。因此大部分时候,都是将FileReader有关的封装再一个promise函数中使用。

    let reader = new FileReader();
    reader.readAsDataURL(file); //异步操作
    reader.onload = (res) => {
      //需要基于load事件获取异步操作的结果
      console.log(res); // base64信息存储在:结果.target.result中保存
    };
    console.log('1111')

在这里插入图片描述

  const changeBase64 = (file) => {
    return new Promise((resolve) => {
      let reader = new FileReader();
      reader.readAsDataURL(file); //异步操作
      reader.onload = (res) => {
        //需要基于load事件获取异步操作的结果
        resolve(res.target.result);
      };
    });
  };
  upload_inp.addEventListener("change", async () => {
    ....
    let base64 = await changeBase64(file);
    console.log(base64);
  });

在这里插入图片描述
以下是点击按钮时候处理base64文件的部分代码,其中用到了encodeURIComponent()用于将一些特殊字符进行转码如#,$等,然后客户端采用decodeURIComponent()进行解码处理

  upload_inp.addEventListener("change", async () => {
    //   文件选取需要基于change事件完成
    let file = upload_inp.files[0];
    _file = file;
    if (!/(png|jpe?g)/i.test(file.type)) {
      alert("请选择png或者jpg或者jpeg格式的图片");
      return;
    }
    if (file.size > 2 * 1024 * 1024) {
      alert("图片大小不能超过2M");
      return;
    }
    // 设置按钮样式
    upload_inp_select.classList.add("loading");
    let base64 = await changeBase64(file);
    try {
      let { code, servicePath, codeText } = await instance.post(
        "/upload_single_base64",
        {
          //将特殊字符进行编码,主要服务端进行了解码处理,因此这里需要进行处理
          file: encodeURIComponent(base64),
          filename: _file.name,
        },
        {
          // 服务器对base64的Content-Type类型进行了限制,需要是urlencodede
          headers: {
            "Content-Type": "application/x-www-form-urlencoded",
          },
        }
      );
      if (+code === 0) {
        alert(`上传成功:${servicePath}`);
        return;
      }
      throw codeText;
    } catch (error) {
      console.log(error);
      alert("请重新上传!");
    } finally {
      // 移除样式类
      upload_inp_select.classList.remove("loading");
    }
  });

在针对base64的接口中,后台进行了处理,如果上传的文件是同一个,则不会进行重复上传(这里不仅是文件名一样,包括文件的内在属性,例如一个文件拷贝一份,两个文件之间只有文件名不同,服务器依旧能区分是同一个文件,从而不进行处理。)以下是后台处理

// 单文件上传处理「BASE64」
app.post("/upload_single_base64", async (req, res) => {
  let file = req.body.file,
    filename = req.body.filename,
    // SparkMD5库会根据文件的内容生成hash名字,如果文件内容一致则不再处理
    spark = new SparkMD5.ArrayBuffer(),
    // 获取文件后缀名
    suffix = /\.([0-9a-zA-Z]+)$/.exec(filename)[1],
    isExists = false,
    path;
  // 服务端解码
  file = decodeURIComponent(file);
  // 将base64的格式去掉
  file = file.replace(/^data:image\/\w+;base64,/, "");
  // Node内置模块,指定编码格式为base64,将file字符串转换为Buffer
  file = Buffer.from(file, "base64");
  // 将Buffer写入spark中,之后根据文件的内容生成名字
  spark.append(file);
  // spark.end()用于生成hash名字
  path = `${uploadDir}/${spark.end()}.${suffix}`;
  await delay();
  // 检测是否存在
  isExists = await exists(path);
  if (isExists) {
    res.send({
      code: 0,
      codeText: "file is exists",
      originalFilename: filename,
      servicePath: path.replace(__dirname, HOSTNAME),
    });
    return;
  }
  writeFile(res, path, file, filename, false);
});

文件上传缩略图&客户端自己生成文件hash名

在之前的文件上传中,文件传给服务器后,都是由服务器处理文件名,而这次是由客户端处理文件名同时添加客户端图片预览效果。

      <div class="item">
        <h3>单一文件上传「缩略图处理」</h3>
        <section class="upload_box" id="upload3">
          <input type="file" class="upload_inp" accept=".jpg,.jpeg,.png" />
          <div class="upload_button_box">
            <button class="upload_button select">选择文件</button>
            <button class="upload_button upload">上传到服务器</button>
          </div>
          <div class="upload_abbre">
            <img src="" alt="" />
          </div>
        </section>
      </div>
    </div>

基础js代码

(function () {
  let upload3 = document.querySelector("#upload3"),
    upload_inp = upload3.querySelector(".upload_inp"),
    upload_inp_select = upload3.querySelector(".upload_button.select"),
    upload_inp_upload = upload3.querySelector(".upload_button.upload"),
    upload_abbre = upload3.querySelector(".upload_abbre"),
    upload_abbre_img = upload_abbre.querySelector("img");
  let _file = null;

  const isGo = (ele) => {
    return (
      ele.classList.contains("disable") || ele.classList.contains("loading")
    );
  };
  // 给选择文件按钮绑定时间事件
  upload_inp_select.addEventListener("click", function () {
    // 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
    if (isGo(this)) return;
    //   触发自带的点击方法选取文件
    upload_inp.click();
  });

  const changeBase64 = (file) => {
    return new Promise((resolve) => {
      let reader = new FileReader();
      reader.readAsDataURL(file); //异步操作
      reader.onload = (res) => {
        //需要基于load事件获取异步操作的结果
        resolve(res.target.result);
      };
    });
  };

  upload_inp.addEventListener("change", async () => {
    //   文件选取需要基于change事件完成
    let file = upload_inp.files[0];
    _file = file;
    if (!/(png|jpe?g)/i.test(file.type)) {
      alert("请选择png或者jpg或者jpeg格式的图片");
      return;
    }
    // 设置按钮样式
    upload_inp_select.classList.add("disable");
    let base64 = await changeBase64(file); //图片本地预览采用base64格式
    upload_abbre.style.display = "block";
    upload_abbre_img.src = base64;
    upload_inp_select.classList.remove("disable");
  });
})();

在这里插入图片描述
图片预览成功后就需要处理上传服务器,将之前的代码复制修改使用。经过测试就会发现一个问题,如果自己在客户端没有处理文件名,那么有两个文件,仅仅是文件名相同,文件内容完全不同,上传服务器,服务器会提示已经存在该文件了,只会保留旧的文件使用,这是预想的不一样,本意是上次的文件都保留。(也可以新的覆盖旧的)

  const handleStyle = (flag) => {
    // flag为真代表选择文件按钮需要添加disable样式,上传服务器按钮需要添加loading样式
    if (flag) {
      upload_inp_select.classList.add("disable");
      upload_inp_upload.classList.add("loading");
    } else {
      upload_inp_select.classList.remove("disable");
      upload_inp_upload.classList.remove("loading");
    }
  };
  // 文件上传按钮
  upload_inp_upload.addEventListener("click", async () => {
    // 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
    if (isGo(upload_inp_upload)) return;
    // 需要获取文件对象
    // 当没有选择文件的时候或者选择了文件又删除的情况下,不允许上传,_file=null
    if (!_file) {
      alert("请选择文件");
      return;
    }
    // 设置按钮样式
    handleStyle(true);
    let fm = new FormData();
    // 后端服务器需要字段file和filename
    fm.append("file", _file);
    fm.append("filename", _file.name); // 未自己处理生成hash文件名
    try {
      let { code, servicePath } = await instance.post(
        "/upload_single_name",
        fm
      );
      if (+code === 0) {
        alert(`上传成功:文件服务器地址${servicePath}`);
        return;
      }
    } catch (error) {
      console.log(error);
      alert("请重新上传!");
    } finally {
      // 设置按钮样式
      handleStyle(false);
    }
  });

在客户端需要借助spark-md5库处理文件名的问题
编写一个函数,处理生成hash名,然后再上传按钮中调用,只不过在这里的函数中,调用FileReader实例身上的readAsArrayBuffer(文件)方法,生成一段文件的buffer信息,该信息传入spark-md5提供的函数中可以根据文件内容生成名字

  // 处理hash名字
  const changeBuffer = (file) => {
    return new Promise((resolve) => {
      let reader = new FileReader();
      reader.readAsArrayBuffer(file); //异步操作
      reader.onload = (res) => {
        //需要基于load事件获取异步操作的结果
        let buffer = res.target.result;
        console.log(buffer); // 查看
        resolve();
      };
    });
  };

在这里插入图片描述
继续添加如下代码,实例化new SparkMD5.ArrayBuffer()该插件提供的方法,然后调用append方法将刚才生成的文件二进制信息放进去,最后调用end方法生成hash名。如果两个文件是一模一样的(文件内容)则生成的hash文件名是一样的。
以下代码用于生成文件的名字,但是文件的后缀名还没有处理,文件的后缀可以根据传入的文件信息中的name字段进行提取

        let spark = new SparkMD5.ArrayBuffer(); //创建md5对象,用于生成hash值
        spark.append(buffer);
        let hash = spark.end(); //自动根据文件二进制内容生成名字
        console.log(hash);

在这里插入图片描述
编写一段正则,然后通过exec方法进行搜索匹配,返回的是一个数组,数组中会返回匹配的结果

        let suffix = /\.[a-zA-Z0-9]+/.exec(file.name); // +代表一个或多个,文件的后缀必须存在
        console.log(suffix);

在这里插入图片描述
完整处理如下

  const changeBuffer = (file) => {
    return new Promise((resolve) => {
      let reader = new FileReader();
      reader.readAsArrayBuffer(file); //异步操作
      reader.onload = (res) => {
        //需要基于load事件获取异步操作的结果
        let buffer = res.target.result;
        let spark = new SparkMD5.ArrayBuffer(); //创建md5对象,用于生成hash值
        spark.append(buffer);
        let hash = spark.end(); //自动根据文件二进制内容生成名字
        let suffix = /\.[a-zA-Z0-9]+/.exec(file.name)[0]; // +代表一个或多个,文件的后缀必须存在
        resolve({
          buffer,
          hash,
          suffix,
          filename: `${hash}${suffix}`, //手动拼接文件和后缀
        });
      };
    });
  };

然后再上传服务器按钮中修改部分代码

    let { filename } = await changeBuffer(_file);
    fm.append("filename", filename); // 手动处理后的文件名

    finally {
      // 设置按钮样式
      handleStyle(false);
      upload_abbre.style.display = "none"; // 清除缩略图等
      upload_abbre_img.src = "";
      _file = null;
    }

这样子就完成的缩略图在客户端手动处理文件名上传服务器了。

单文件上传进度管理

      <div class="item">
        <h3>单一文件上传「进度管控」</h3>
        <section class="upload_box" id="upload4">
          <input type="file" class="upload_inp" />
          <div class="upload_button_box">
            <button class="upload_button select">上传文件</button>
          </div>
          <div class="upload_progress">
            /* 通过指定value进度条的width宽度属性设置进度条 */
            <div class="value"></div>
          </div>
        </section>
      </div>
// 进度条管控
(function () {
  let upload4 = document.querySelector("#upload4"),
    upload_inp = upload4.querySelector(".upload_inp"),
    upload_inp_select = upload4.querySelector(".upload_button.select"),
    upload_progress = upload4.querySelector(".upload_progress"),
    upload_progress_value = upload_progress.querySelector(".value");
  let _file = null;

  const isGo = (ele) => {
    return (
      ele.classList.contains("disable") || ele.classList.contains("loading")
    );
  };
  // 给选择文件按钮绑定时间事件
  upload_inp_select.addEventListener("click", async function () {
    // 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
    if (isGo(this)) return;
    //   触发自带的点击方法选取文件
    upload_inp.click();
  });

  upload_inp.addEventListener("change", async () => {
    //   文件选取需要基于change事件完成
    let file = upload_inp.files[0];
    _file = file;
    if (!/(png|jpe?g)/i.test(file.type)) {
      alert("请选择png或者jpg或者jpeg格式的图片");
      return;
    }
    // 设置按钮样式
    upload_inp_select.classList.add("loading");
    try {
      let fm = new FormData();
      fm.append("file", _file);
      fm.append("filename", _file.name);
      let { code, codeText, servicePath } = await instance.post(
        "/upload_single",
        fm
      );
      if (+code === 0) {
        alert("上传成功:文件服务器地址" + servicePath);
        return;
      }
      throw codeText;
    } catch (error) {
      console.log(error);
    } finally {
      upload_inp_select.classList.remove("loading");
    }
  });
})();

目前功能就是上次一个FormData文件,处理完成后接着处理进度条问题
在这里插入图片描述
这里需要了解在axios中的请求配置中提供了 onUploadProgress: function (progressEvent) {// 处理原生进度事件},方法处理进度条事件。其本质是基于XHR中的属性(xhr.upload.onprogress)完成的。
在这里插入图片描述
将请求部分的代码修改如下,查看进度条事件对象提供的属性,其中loaded代表当前进度,total代表当前进度总数,通过计算比例设置进度条样式

      let { code, codeText, servicePath } = await instance.post(
        "/upload_single",
        fm,
        {
          onUploadProgress: (e) => {
            console.log(e);
          },
        }
      );

在这里插入图片描述
完善后代码如下,基本效果进度条递增是完成了。但是存在一个小bug,在代码的code===0中,针对服务器返回结果了就代表上传成功,于是我们设置百分百进度,但是样式中针对width变化设置了300毫秒的过度transition,但是alert会阻塞页面渲染,因此在过度动画过程中,如果没有完成的时候,alert弹出,则过度不会继续执行停在那里。

  upload_inp.addEventListener("change", async () => {
    //   文件选取需要基于change事件完成
    let file = upload_inp.files[0];
    _file = file;
    // 设置按钮样式
    upload_inp_select.classList.add("loading");
    try {
      let fm = new FormData();
      fm.append("file", _file);
      fm.append("filename", _file.name);
      let { code, codeText, servicePath } = await instance.post(
        "/upload_single",
        fm,
        {
          onUploadProgress: (e) => {
            let { total, loaded } = e; //当前进度总数和进度现有位置
            upload_progress.style.display = "block"; //开启显示容器
            upload_progress_value.style.width = `${(loaded / total) * 100}%`;
          },
        }
      );
      if (+code === 0) {
        upload_progress_value.style.width = `100%`; //成功直接显示100%
        alert("上传成功:文件服务器地址" + servicePath);
        return;
      }
      throw codeText;
    } catch (error) {
      console.log(error);
    } finally {
      upload_inp_select.classList.remove("loading");
      upload_progress.style.display = "none";
      upload_progress_value.style.width = 0;
    }
  });

在这里插入图片描述
解决方法在两者之间写一个延迟函数,确保过度执行完毕了再往下执行。

  const delay = (time) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve();
      }, time);
    });
  };

        upload_progress_value.style.width = `100%`; //成功直接显示100%
        await delay(300);
        alert(`上传成功:文件服务器地址${servicePath}`);

多文件上传&进度条控制

多文件上传第一步,给标签设置multiple 属性

 <input type="file" class="upload_inp" multiple />
      <div class="item">
        <h3>多文件上传</h3>
        <section class="upload_box" id="upload5">
          <input type="file" class="upload_inp" multiple />
          <div class="upload_button_box">
            <button class="upload_button select">选择文件</button>
            <button class="upload_button upload">上传到服务器</button>
          </div>
          <ul class="upload_list">
            <!-- <li key='xx'>
                        <span>文件:xxxxx</span>
                        <span><em>移除</em></span>
                    </li> -->
          </ul>
        </section>
      </div>
(function () {
  let upload = document.querySelector("#upload5"),
    upload_inp = upload.querySelector(".upload_inp"),
    upload_inp_select = upload.querySelector(".upload_button.select"),
    upload_inp_upload = upload.querySelector(".upload_button.upload"),
    upload_list = upload.querySelector(".upload_list");
  let _files = [];

  const isGo = (ele) => {
    return (
      ele.classList.contains("disable") || ele.classList.contains("loading")
    );
  };
  const handleStyle = (flag) => {
    // flag为真代表选择文件按钮需要添加disable样式,上传服务器按钮需要添加loading样式
    if (flag) {
      upload_inp_select.classList.add("disable");
      upload_inp_upload.classList.add("loading");
    } else {
      upload_inp_select.classList.remove("disable");
      upload_inp_upload.classList.remove("loading");
    }
  };
  // 给选择文件按钮绑定时间事件
  upload_inp_select.addEventListener("click", function () {
    // 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
    if (isGo(this)) return;
    //   触发自带的点击方法选取文件
    upload_inp.click();
  });

  upload_inp.addEventListener("change", async () => {
    //   文件选取需要基于change事件完成
    _files = Array.from(upload_inp.files); //FileList 类数组
    //  显示已选文件
    let str = "";
    _files.forEach((item, index) => {
      str += `
          <li>
            <span>文件:${item.name}</span>
            <span><em>移除</em></span>
          </li>
      `;
    });
    // 插入页面
    upload_list.style.display = "block";
    upload_list.innerHTML = str;
  });
})();

在这里插入图片描述
然后就是处理每一个文件的删除按钮逻辑,设计多个元素的动态添加,使用事件委托处理。但是如下代码仅仅是删除了结构样式,保存的file文件中的数据并没有删除,因此还需要删除文件中的数据。

  // 删除按钮的逻辑
  upload_list.addEventListener("click", (e) => {
    let target = e.target;
    let curLi = null;
    if (target.tagName === "EM") {
      // 从结构中删除该元素的整体内容(删除li节点及其子节点)
      let curLi = target.parentNode.parentNode;
      if (!curLi) return; //不存在直接退出,防止出错
      upload_list.removeChild(curLi);
      console.log(_files);
    }
  });

在这里插入图片描述
这个时候可以换位思考,给li元素绑定一个唯一标识,每次获取的时候拿着该标识到file数组中进行查找,这里又涉及到了自定义设置的标识如何在file数组中查找,这里可以重构file数组。重构完file数组后,需要修改原forEach中的代码

  // 生成唯一id函数,利用时间戳+随机数
  const createRandom = () => {
    let key = Date.now() * Math.random(); //因为每次的数都足够大都会带小数点不用后面进行判断
    return String(key.toString(16)).replace(".", ""); // 转换为16进制后从数字转换为字符串,然后将点去除
  };
    _files = Array.from(upload_inp.files); //FileList 类数组
    // 重构数组,并设置唯一标识
    _files = _files.map((file, index) => {
      return {
        file,
        filename: file.name,
        key: createRandom(),
      };
    });
    console.log(_files);

在这里插入图片描述

  // 删除按钮的逻辑
  upload_list.addEventListener("click", (e) => {
    let target = e.target;
    let curLi = null;
    if (target.tagName === "EM") {
      // 从结构中删除该元素的整体内容(删除li节点及其子节点)
      let curLi = target.parentNode.parentNode;
      if (!curLi) return; //不存在直接退出,防止出错
      upload_list.removeChild(curLi);
      // 删除数据
      let key = curLi.getAttribute("key"); //通过自定义属性获取唯一标识
      _files = _files.filter((file) => {
        return key !== file.key;
      });
      if (_files.length === 0) {
        upload_list.style.display = "none";
        upload_list.innerHTML = "";
      }
    }
  });

这里不推荐使用forEach中提供的index作为li的索引因为:索引赋值给元素后就固定了,当删除的时候,用索引去file数组中查找的时候会出现问题。

然后处理提交服务器按钮的代码

  // 服务器上传按钮
  upload_inp_upload.addEventListener("click", () => {
    if (isGo(upload_inp_upload)) return;
    if (_files.length === 0) {
      alert("请先选择文件");
      return;
    }
    handleStyle(true);
    // 获取所有的li元素,已经开始上传服务器,代表li元素存在
    let upload_list_arr = Array.from(upload_list.querySelectorAll("li"));
    // 这里有多个请求需要发送
    _files = _files.map((item) => {
      let fm = new FormData();
      fm.append("file", item.file);
      fm.append("filename", item.filename);
      // 进行当前li元素匹配,返回li元素
      let curli = upload_list_arr.find((li) => {
        return li.getAttribute("key") === item.key;
      });
      // 获取该li元素下的最后一个span元素
      let curspan = curli?.querySelector("span:last-child");
      return instance
        .post("/upload_single", fm, {
          // 给每一个文件设置进度条样式
          onUploadProgress(ev) {
            /*  这里设计让li元素的最后一个span元素中的内容变为进度百分比显示即可,因此在这里需要获取对于元素
              只需要获取到当前的li元素即可,因为li元素身上绑定了唯一标识,因此可以获取所有的li元素和当map中item.key进行匹配
              <li key='xx'>
                    <span>文件:xxxxx</span>
                    <span>进度显示:10%</span>
              </li> 
          */
            if (curspan) {
              curspan.innerHTML = `进度显示:${(
                (ev.loaded / ev.total) *
                100
              ).toFixed(2)}%`;
            }
          },
        })
        .then((res) => {
          if (+res.code === 0) {
            if (curspan) {
              curspan.innerHTML = `进度显示:100%`;
              return;
            }
          }
          Promise.reject(res.codeText);
        });
    });
    Promise.all(_files) //只要有一个请求失败了整体失败
      .then((res) => {
        alert("上传成功");
      })
      .catch((err) => {
        console.log(err);
      })
      .finally(() => {
        handleStyle(false);
        _files = [];
        upload_list.style.display = "none";
        upload_list.innerHTML = "";
      });
  });

文件拖拽上传

      <div class="item">
        <h3>拖拽上传</h3>
        <section class="upload_box" id="upload6">
          <input type="file" class="upload_inp" />
          <div class="upload_drag">
            <i class="icon"></i>
            <span class="text"
              >将文件拖到此处,或<a href="javascript:;" class="upload_submit"
                >点击上传</a
              ></span
            >
          </div>
          <div class="upload_mark">正在上传中,请稍等...</div>
        </section>
      </div>
// 拖拽上传
(function () {
  let upload = document.querySelector("#upload6"),
    upload_inp = upload.querySelector(".upload_inp"),
    upload_submit = upload.querySelector(".upload_submit"),
    upload_mark = upload.querySelector(".upload_mark");

  let _file = null;

  const isGo = (ele) => {
    return (
      ele.classList.contains("disable") || ele.classList.contains("loading")
    );
  };
  // 给选择文件按钮绑定时间事件
  upload_submit.addEventListener("click", async function () {
    // 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
    if (isGo(this)) return;
    //   触发自带的点击方法选取文件
    upload_inp.click();
  });

  upload_inp.addEventListener("change", async () => {
    //   文件选取需要基于change事件完成
    _file = upload_inp.files[0];
    console.log(_file);
  });
})();

在这里插入图片描述

拖拽事件

dragenter:拖拽的文件进入指定的容器触发
dragleave:拖拽的文件离开了指定的容器触发
dragover:拖拽的文件在指定的容器内会一直触发(因此需要进行节流防抖处理)
drop:拖拽的文件放置到指定的容器中触发
但是任意一个文件,拖拽到浏览器页面中都会进入默认的预览,因此我们需要将默认行为取消
在这个项目中,前面两个事件用不到可以删除,代码主体基本在drop事件中

  // 容器添加拖拽事件
  upload.addEventListener("dragenter", (e) => {
    console.log("进入了");
  });
  upload.addEventListener("dragleave", (e) => {
    console.log("离开了");
  });
  upload.addEventListener("dragover", (e) => {
    e.preventDefault();
    console.log("在里面了移动了");
  });
  upload.addEventListener("drop", (e) => {
    e.preventDefault();
    console.log("放置了");
  });

drop事件中,最终事件对象身上的event.dataTransfer.files字段保存拖拽上传的文件信息。
在这里插入图片描述
因为这里两个地方的逻辑代码上传服务器是一样的,因此封装为一个函数处理

  let isRun = false; //节流防抖标志,true代表节流防抖,代表正在运行
  // 上传文件方法
  const uploadFile = async (file) => {
    if (isRun) return;
    isRun = true;
    // 将遮罩层启动
    upload_mark.style.display = "block";
    try {
      let fm = new FormData();
      fm.append("file", file);
      fm.append("filename", file.name);
      let { code, codeText, servicePath } = await instance.post(
        "/upload_single",
        fm
      );
      if (+code === 0) {
        alert(`上传成功:文件服务器地址${servicePath}`);
        return;
      }
      throw codeText;
    } catch (error) {
      console.log(error);
    } finally {
      // 清除默认样式类
      upload_mark.style.display = "none";
      isRun = false;
    }
  };
  upload.addEventListener("drop", (e) => {
    e.preventDefault();
    let file = e.dataTransfer.files[0];
    uploadFile(file);
  });
    upload_inp.addEventListener("change", async () => {
    //   文件选取需要基于change事件完成
    _file = upload_inp.files[0];
    uploadFile(_file);
  });

切片上传

项目中如果存在一个很大体积的文件,如果不做任何处理直接上传给服务器,会造成服务器压力过大,且如果传输过程中失败了,还需要重新将文件上传。于是采用了切片上传,切片上传的思路就是在客户端选取文件即将上传的时候,将文件切成一个一个小切片进行传送,在服务端中,每次接受到一个切片后就将切片保存在一个地方收集,当客户端切片传送结束的时候,给服务端再次传送一个结束标志,服务端接受到该标志后就将刚才收集的切片进行组合,最终合并为一个文件。基于切片上传有一个好处是可以实现断点续传。 断点续传第一种是服务端提供接口,客户端切片上传部分后失败了,服务端会保存部分切片,然后客户端重新请求的时候会调用该接口查看已有服务端切片,然后存在的切片就不会继续上传了。第二中是不提供接口,客户端每次切片传送的时候,在服务端都进行校验查看该切片是否存在。

那么这么多切片如何判断哪些切片是一组的,并且切片之间的顺序是如何组合的。在客户端,可以基于SparkMD5库完成,该库中如果是同一个文件,那么生成的hash文件名是一样的,那么我们的切片可以是这种格式:hash_1,hash_2 ...这种格式的,同一个文件的hash是一样的。

      <div class="item">
        <h3>大文件上传</h3>
        <section class="upload_box" id="upload7">
          <input type="file" class="upload_inp" />
          <div class="upload_button_box">
            <button class="upload_button select">上传图片</button>
          </div>
          <div class="upload_progress">
            <div class="value"></div>
          </div>
        </section>
      </div>
// 切片上传&断电续传
(function () {
  let upload = document.querySelector("#upload7"),
    upload_inp = upload.querySelector(".upload_inp"),
    upload_inp_select = upload.querySelector(".upload_button.select"),
    upload_progress = upload.querySelector(".upload_progress"),
    upload_progress_value = upload_progress.querySelector(".value");
  let _file = null;

  const isGo = (ele) => {
    return (
      ele.classList.contains("disable") || ele.classList.contains("loading")
    );
  };
  // 处理hash名字
  const createBuffer = (file) => {
    return new Promise((resolve) => {
      let reader = new FileReader();
      reader.readAsArrayBuffer(file);
      reader.onload = (res) => {
        let buffer = res.target.result;
        let spark = new SparkMD5.ArrayBuffer();
        spark.append(buffer);
        let hash = spark.end();
        let suffix = /\.[a-zA-Z0-9]+/.exec(file.name)[0];
        resolve({
          buffer,
          hash,
          suffix,
          filename: `${hash}${suffix}`, //手动拼接文件和后缀
        });
      };
    });
  };

  // 给选择文件按钮绑定时间事件
  upload_inp_select.addEventListener("click", async function () {
    // 判断是否有disable或loading属性有就不继续往下走。类似节流防抖
    if (isGo(this)) return;
    //   触发自带的点击方法选取文件
    upload_inp.click();
  });

  upload_inp.addEventListener("change", async () => {
    //   文件选取需要基于change事件完成
    let file = upload_inp.files[0];
    _file = file;
    // 设置按钮样式
    upload_inp_select.classList.add("loading");
    await createBuffer(_file);
  });
})();

在这里插入图片描述
实现切片处理有两种方法:固定数量和固定大小。但是实际开发通常两者结合,先设置最大切片数量,以固定大小进行切片,如果当前切片超过了最大数量,则以固定数量处理。固定大小进行切片分割如果切片过小,会造成数量过多,传给服务器也会造成压力,因此添加最大切片数量加以控制。

    // 获取已有切片数量
    let already = [];
    try {
      // 该接口调用会返回已有切片数量,如果fileList长度为0或进入catch都可以理解为服务器没有切片,
      // 现有文件切片需要全部上传
      let { code, fileList } = await instance.get("/upload_already", {
        params: {
          hash,
        },
      });
      if (+code === 0) {
        already = fileList;
      }
    } catch (error) {}
    // 设置切片处理逻辑:固定数量&固定大小结合
    let max = 20 * 1024; // 切片最大20kb
    let count = Math.ceil(_file.size / max); //切片数量,同时向上取整
    // 判断数量是否大于了最大切片数量,大于则以固定数量为基准实现
    if (count > 100) {
      count = 100; //固定切片数量最大为100个
      max = _file.size / count;
    }

在一个文件对象的原型上slice(start,end)方法用于实现切片,不包含结束位置,且单位是字节
在这里插入图片描述
在这里插入图片描述

    let chunks = []; //保存每一个切片
    // 生成切片,调用file文件原型上提供的slice方法进行切片
    let index = 0; //当前切片的序号
    while (index < count) {
      chunks.push({
        file: _file.slice(max * index, max * (index + 1)), //每一个切片的大小
        filename: `${hash}_${index + 1}${suffix}`,
      });
      index++;
    }
    console.log(chunks);

在这里插入图片描述
断点续传主要代码,先传送小切片,当传送完毕的时候通知服务器合并切片组成文件

    // 无论上传切片失败还是切片合并后上传失败,或者最终成功都需要清空loading,因此封装为函数
    const clear = () => {
      upload_inp_select.classList.remove("loading");
      upload_progress.style.display = "none";
      upload_progress_value.style.width = "0%";
    };
    // 控制进度条,根据切片上传的情况控制
    index = 0; //当前已上传切片数量
    const complete = async () => {
      index++;
      // 设置进度条样式,当前切片的数量/总切片数量
      upload_progress_value.style.width = `${(index / count) * 100}%`;
      // 当切片的数量到达指定的数量的时候就开始合并切片
      if (index < count) return;
      // 切片合并
      upload_progress_value.style.width = `100%`;
      try {
        let { code, servicePath } = await instance.post(
          "/upload_merge",
          {
            HASH: hash,
            count,
          },
          {
            headers: {
              "Content-Type": "application/x-www-form-urlencoded",
            },
          }
        );
        if (+code === 0) {
          // 合并成功
          alert(`上传成功:文件服务器地址${servicePath}`);
          return;
        }
      } catch (error) {
        alert("合并失败");
      } finally {
        clear();
      }
    };

    // for (let i = 0; i < chunks.length; i++) {
    //   let chunk = chunks[i];
    //   // 进入该函数之前,判断该切片是否已经存在于服务器,服务器返回切片的名字
    //   if (already.length > 0 && already.includes(chunk.filename)) {
    //     complete(); //执行进度条控制
    //     return;
    //   }
    //   // 获取每一个切片信息上传服务器
    //   let fm = new FormData();
    //   fm.append("file", chunk.file);
    //   fm.append("filename", chunk.filename);
    //   instance
    //     .post("/upload_chunk", fm)
    //     .then((res) => {
    //       if (+res.code === 0) {
    //         // 上传成功,执行进度条控制模块
    //         complete();
    //         return;
    //       }
    //       Promise.reject(res.codeText);
    //     })
    //     .catch((err) => {
    //       alert("当前切片上传失败,请您稍后再试~~");
    //       clear();
    //     });
    // }

    chunks.forEach((chunk) => {
      // 进入该函数之前,判断该切片是否已经存在于服务器,服务器返回切片的名字
      if (already.length > 0 && already.includes(chunk.filename)) {
        complete(); //执行进度条控制
        return;
      }
      // 获取每一个切片信息上传服务器
      let fm = new FormData();
      fm.append("file", chunk.file);
      fm.append("filename", chunk.filename);
      instance
        .post("/upload_chunk", fm)
        .then((res) => {
          if (+res.code === 0) {
            // 上传成功,执行进度条控制模块
            complete();
            return;
          }
          Promise.reject(res.codeText);
        })
        .catch((err) => {
          alert("当前切片上传失败,请您稍后再试~~");
          clear();
        });
      // try {
      //   let { code, codeText } = await instance.post("/upload_chunk", fm);
      //   if (+code === 0) {
      //     // 上传成功,执行进度条控制模块
      //     complete();
      //     return;
      //   }
      //   throw codeText;
      // } catch (err) {
      //   console.log(err, "单个切片上传失败");
      //   clear();
      // }
    });
  });
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值