主要流程: 

  • 首先,上传分片文件前,将文件分片信息发送给服务器 。
  • 其次, 服务器返回成功后,上传所有的分片文件
  • 最后,当所有分片都上传成功之后,请求服务器合并上传的分片文件。

例子:

let uploadFile = async (file)=> {
      let onProgress = (progress) => {
        let percent = Math.round((progress.loaded / progress.total) * 100); // 文件上传进度回调
        //  do something here ...
      };
        // @required file 文件 ,onProgress 进度回调 ,splitSize 分片大小 默认5MB, splitCount 一次上传分片个数 默认5
      const res = await multipartUpload(file, onProgress, 5, 5);
    },

multipartUpload  上传文件方法  

        multipartUpload  方法中,首先会简单检查一下参数,防止因参数导致程序报错。除了 file ( 文件)参数是必填 ,onProgress (上传进度回调),splitSize( 分片大小),splitCount( 一次性发送多少个分片上传请求) ,都是可选参数 。    为什么是简单检查一下参数呢,因为就目前的条件判断还是不能够避免所有出现异常的情况,比如file 参数传个null ,splitCount 传个小数......在这里参数条件判断写得太多会影响代码的可读性,抽离成一个方法出去的话,我个人觉得是可以有,但是没必要。因为按照正常人的正常逻辑来使用的话,目前的判断还是可以避免编码中的参数错误的。

        检查参数之后 ,对文件进行分片,获取文件分片信息。 将分片信息发送给服务器,服务返回成功后,上传分片文件,为了避免上传文件过大,会分批次,一次性发送指定个(splitCount 默认为 5) 分片上传请求。在每一个切片上传成功之后,则回调用一次 onProgress 文件上传进度回调函数,模拟出一个文件上传进度。 等所有的分片文件都上传成功之后,请求服务器合并分片文件,服务器合并成功则返回合并成功的文件路径 。 

/**
 * @param {*} file 上传文件
 * @param {*} onProgress  切片上传进度
 * @param {*} splitSize 每个切片文件的大小
 * @param {*} splitCount  一次发送多少个切片请求
 * @returns
 */

// 文件切片上传
export const multipartUpload = async (
  file, // 切片原文件
  onProgress = () => {}, // 切片回调参数
  splitSize = 5, // 切片大小 默认5 MB
  splitCount = 5 // 一次性发送切片请求的个数 默认 5
) => {
  //  简单检查一下file 参数
  if (!file || typeof file != "object" || Array.isArray(file))
    throw new Error(" a required  parameter 'file' missing .");
  // 简单判断一下 splitSize 必须是大于或等于 1 的 数字
  if (typeof splitSize != "number" || splitSize < 1)
    throw new Error(
      " the type of 'splitSize' must be 'number' and greater than 1 ."
    );
  // 简单判断一下   splitCount 必须是大于或等于 1 的 数字
  if (typeof splitCount !== "number" || splitCount < 1)
    throw new Error(
      " the type of 'splitCount' must be 'number' and greater than 1 ."
    );
  let _file = file.file || file;
  const { size, name, type, lastModified } = _file; // 获取文件大小 和名称
  let identifier = await getIdentifier(_file); //  根据文件md5 和当前时间戳生成的唯一标识符
  const totalChunks = Math.ceil(size / splitSize / 1024 / 1024); // 切片总块数 文件大小/ 切片大小向上取整
  const splitFileList = splitFile(_file, splitSize); //  返回切片数组
  let groupFileList = groupFileListByCount(splitFileList, splitCount); //  将 切片数组根据count 分组
  let splitInfo = { totalChunks, identifier, fileName: name, type }; // 文件切片信息
  // 上传分片文件件 告诉服务器准备上传的切片文件信息
  let beforeUploadFlag = await beforeUpload(splitInfo);
  if (!beforeUploadFlag) return false; //  当服务器没准备好切片上传
  // 切片上传的结果
  const uploadFlag = await uploadGroupFileList(
    groupFileList,
    splitInfo,
    onProgress
  );
  if (!uploadFlag) return false;
  // 请求服务器合并切片文件
  const mergeResult = await mergeFileFlag(identifier);
  if (!mergeResult) return false;
  // 请求服务器合并切片成功后  调一次上传进度回调接口  让上传进度100%
  onProgress({
    total: totalChunks + 1,
    loaded: totalChunks + 1,
  });
  return mergeResult;
};

  getIdentifier 根据文件MD5 + timestamp 生成一个上传文件的唯一标识 

// 根据文件MD5 + timestamp 生成的唯一标识
export const getIdentifier = (file) => {
  return new Promise((resolve, reject) => {
    let reader = new FileReader();
    let spark = new SparkMD5();
    reader.readAsBinaryString(file);
    reader.onload = (e) => {
      spark.appendBinary(e.target.result);
      const md5 = spark.end();
      resolve(md5 + Date.now());
    };
  });
};

splitFile 根据splitSize 文件进行分片 

export const splitFile = (file, splitSize = 5) => {
  const _5M = 1024 * 1024 * splitSize; //  默认分割成5M
  const { size, type } = file;
  let tempFileList = []; // 盛放切割后的切片
  for (let index = 0; index < size; index += _5M) {
    let start = index; // 切片开始位置
    let end = start + _5M < size ? index + _5M : size; // 切片结束位置
    let item = file.slice(start, end, type); // 切割的文件
    tempFileList.push(item);
  }
  return tempFileList;
};

groupFileListByCount根据splitCount 对分片文件数组进行分组

// 将fileList 分组
export const groupFileListByCount = (fileList, count) => {
  let group = Math.ceil(fileList.length / count); // 将 fileList 分为多少个组
  let resultList = new Array(group); // 根据组数创建一个空的 list
  fileList.map((item, index) => {
    let i = Math.floor(index / count); // index / count 向下取整 为组数编号
    if (Array.isArray(resultList[i])) resultList[i].push(item);
    else resultList[i] = [item];
  });
  return resultList;
};

beforeUpload 上传分片文件前 将文件的分片信息发送给服务器 服务器返回成功上传分片

export const beforeUpload = async (splitInfo) => {

  try {
    const res = await that.$httpRequest({
      url: "/sys-file/prepareUpload",
      method: "post",
      data: splitInfo,
      isForm: true,
    });
    if (res.code == 0) return true;
    return false;
  } catch (err) {
    return false;
  }
};

uploadGroupFileList  上传分组中的分片 

// 上传分组的切片文件
export const uploadGroupFileList = async (
  list = [],
  splitInfo = {},
  onProgress //  每一个切片上传成功之后 调一次上传回调  模拟一个进度条
) => {
  /**
   * 模拟一个进度条
   * total+1  避免当所有的切片都上传完成后进度100% 。而还没有请求服务器合并切片
   * 或者请求服务器合并分片失败 而上传进度为100%  对用户不友好
   */
  let progress = {
    total: splitInfo.totalChunks + 1,
    loaded: 0,
  };
  for (let i = 0; i < list.length; i++) {
    let itemList = Array.isArray(list[i]) ? list[i] : [];
    let itemHttpUpload = itemList.map((item, index) => {
      return new Promise(async (resolve, reject) => {
        let chunkNumber = i * list[i].length + index + 1; // 当前是所有文件切片中的第几个
        let { totalChunks, identifier, fileName } = splitInfo;
        let params = { totalChunks, chunkNumber, identifier, fileName };
        uploadFile(item, params, "/sys-file/multiUpload")
          .then((res) => {
            //  上传进度回调
            progress.loaded++;
            onProgress(progress);
            resolve(chunkNumber);
          })
          .catch((err) => {
            reject(chunkNumber);
          });
      });
    });
    // Promise.allSettled  等待上一组 upload http请求所有都完成后 返回结果
    const lastGroupUpload = await Promise.allSettled(itemHttpUpload)
      .then((res) => {
        return Promise.resolve(res);
      })
      .catch((err) => {
        return Promise.resolve(err);
      });
    // 将上传失败的分片文件存储起来
    let errorChunkList = lastGroupUpload
      .filter((item) => item.status === "rejected")
      .map((item) => item.reason);
    // 判断上一组的http 中是否有上传失败的分片 存在则不继续上传剩余的分片 返回 false
    if (errorChunkList.length > 0) return false;
  }

  return true;
};

mergeFileFlag  请求合并分片 

合并成功 则文件上传完成 。

// 分片全部上传完成之后 请求服务器合并 上传的分片文件
export const mergeFileFlag = async (identifier) => {
  try {
    const params = { identifier, convert: 0 };
    const res = await that.$httpRequest({
      url: "/sys-file/complete",
      method: "post",
      isForm: true,
      data: params,
    });
    if (res.code == 0 && res.data) {
      return res.data;
    }
    return false;
  } catch (err) {
    return false;
  }
};

完整代码

import Vue from "vue";
import $ from "jquery";
import SparkMD5 from "spark-md5"; //  获取文件MD5
import store from "@/store";
let that = new Vue(); //  that 指向一个新Vue 实例 ,用于调用 Vue prototype 的一些一些方法 例如 deepClone 、httpRequest 等
const is_Dev = process.env.NODE_ENV == "development" ? true : false;
let baseUrl = is_Dev
  ? window.globalConfig.DEV_BASE_API
  : window.globalConfig.PRO_BASE_API;
// 单独封装一个分片上传的请求
export const uploadFile = (file, config = {}, url = "/sys-file/upload") => {
  if (!file || typeof file != "object" || Array.isArray(file))
    throw new Error(" a required  parameter 'file' missing . ");
  let _file = file.file || file;
  let _para = new FormData();
  _para.append("file", _file);
  // 除文件外的其他参数
  if (typeof config == "object" && !Array.isArray(config)) {
    const keyList = Object.keys(config);
    keyList.map((item) => {
      _para.append(item, config[item]);
    });
  }
  return new Promise((resolve, reject) => {
    let options = {
      url: baseUrl + url,
      method: "POST",
      data: _para,
      timeout: 60000,
      contentType: false,
      processData: false,
      headers: {
        Authorization: "Bearer " + store.state.user.access_token,
      },
    };
    $.ajax(options)
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        if (err.statusText == "timeout") err.abort();
        reject(err);
      });
  });
};
// 根据splitSize 对文件进行切片
export const splitFile = (file, splitSize = 5) => {
  const _5M = 1024 * 1024 * splitSize; //  默认分割成5M
  const { size, type } = file;
  let tempFileList = []; // 盛放切割后的切片
  for (let index = 0; index < size; index += _5M) {
    let start = index; // 切片开始位置
    let end = start + _5M < size ? index + _5M : size; // 切片结束位置
    let item = file.slice(start, end, type); // 切割的文件
    tempFileList.push(item);
  }
  return tempFileList;
};
// 根据文件MD5 + timestamp 生成的唯一标识
export const getIdentifier = (file) => {
  return new Promise((resolve, reject) => {
    let reader = new FileReader();
    let spark = new SparkMD5();
    reader.readAsBinaryString(file);
    reader.onload = (e) => {
      spark.appendBinary(e.target.result);
      const md5 = spark.end();
      resolve(md5 + Date.now());
    };
  });
};
//  分片上传前 告诉服务器 分片文件信息
export const beforeUpload = async (splitInfo) => {
  try {
    const res = await that.$httpRequest({
      url: "/sys-file/prepareUpload",
      method: "post",
      data: splitInfo,
      isForm: true,
    });
    if (res.code == 0) return true;
    return false;
  } catch (err) {
    return false;
  }
};
// 分片全部上传完成之后 请求服务器合并 上传的分片文件
export const mergeFileFlag = async (identifier) => {
  try {
    const params = { identifier, convert: 0 };
    const res = await that.$httpRequest({
      url: "/sys-file/complete",
      method: "post",
      isForm: true,
      data: params,
    });
    if (res.code == 0 && res.data) {
      return res.data;
    }
    return false;
  } catch (err) {
    return false;
  }
};

// 将fileList 分组
export const groupFileListByCount = (fileList, count) => {
  let group = Math.ceil(fileList.length / count); // 将 fileList 分为多少个组
  let resultList = new Array(group); // 根据组数创建一个空的 list
  fileList.map((item, index) => {
    let i = Math.floor(index / count); // index / count 向下取整 为组数编号
    if (Array.isArray(resultList[i])) resultList[i].push(item);
    else resultList[i] = [item];
  });
  return resultList;
};
// 上传分组的切片文件
export const uploadGroupFileList = async (
  list = [],
  splitInfo = {},
  onProgress //  每一个切片上传成功之后 调一次上传回调  模拟一个进度条
) => {
  /**
   * 模拟一个进度条
   * total+1  避免当所有的切片都上传完成后进度100% 。而还没有请求服务器合并切片
   * 或者请求服务器合并分片失败 而上传进度为100%  对用户不友好
   */
  let progress = {
    total: splitInfo.totalChunks + 1,
    loaded: 0,
  };
  for (let i = 0; i < list.length; i++) {
    let itemList = Array.isArray(list[i]) ? list[i] : [];
    let itemHttpUpload = itemList.map((item, index) => {
      return new Promise(async (resolve, reject) => {
        let chunkNumber = i * list[i].length + index + 1; // 当前是所有文件切片中的第几个
        let { totalChunks, identifier, fileName } = splitInfo;
        let params = { totalChunks, chunkNumber, identifier, fileName };
        uploadFile(item, params, "/sys-file/multiUpload")
          .then((res) => {
            //  上传进度回调
            progress.loaded++;
            onProgress(progress);
            resolve(chunkNumber);
          })
          .catch((err) => {
            reject(chunkNumber);
          });
      });
    });
    // Promise.allSettled  等待上一组 upload http请求所有都完成后 返回结果
    const lastGroupUpload = await Promise.allSettled(itemHttpUpload)
      .then((res) => {
        return Promise.resolve(res);
      })
      .catch((err) => {
        return Promise.resolve(err);
      });
    // 将上传失败的分片文件存储起来
    let errorChunkList = lastGroupUpload
      .filter((item) => item.status === "rejected")
      .map((item) => item.reason);
    // 判断上一组的http 中是否有上传失败的分片 存在则不继续上传剩余的分片 返回 false
    if (errorChunkList.length > 0) return false;
  }

  return true;
};

/**
 * @param {*} file 上传文件
 * @param {*} onProgress  切片上传进度
 * @param {*} splitSize 每个切片文件的大小
 * @param {*} splitCount  一次发送多少个切片请求
 * @returns
 */

// 文件切片上传
export const multipartUpload = async (
  file, // 切片原文件
  onProgress = () => {}, // 切片回调参数
  splitSize = 5, // 切片大小 默认5 MB
  splitCount = 5 // 一次性发送切片请求的个数 默认 5
) => {
  //  简单检查一下file 参数
  if (!file || typeof file != "object" || Array.isArray(file))
    throw new Error(" a required  parameter 'file' missing .");
  // 简单判断一下 splitSize 必须是大于或等于 1 的 数字
  if (typeof splitSize != "number" || splitSize < 1)
    throw new Error(
      " the type of 'splitSize' must be 'number' and greater than 1 ."
    );
  // 简单判断一下   splitCount 必须是大于或等于 1 的 数字
  if (typeof splitCount !== "number" || splitCount < 1)
    throw new Error(
      " the type of 'splitCount' must be 'number' and greater than 1 ."
    );
  let _file = file.file || file;
  const { size, name, type, lastModified } = _file; // 获取文件大小 和名称
  let identifier = await getIdentifier(_file); //  根据文件md5 和当前时间戳生成的唯一标识符
  const totalChunks = Math.ceil(size / splitSize / 1024 / 1024); // 切片总块数 文件大小/ 切片大小向上取整
  const splitFileList = splitFile(_file, splitSize); //  返回切片数组
  let groupFileList = groupFileListByCount(splitFileList, splitCount); //  将 切片数组根据count 分组
  let splitInfo = { totalChunks, identifier, fileName: name, type }; // 文件切片信息
  // 上传分片文件件 告诉服务器准备上传的切片文件信息
  let beforeUploadFlag = await beforeUpload(splitInfo);
  if (!beforeUploadFlag) return false; //  当服务器没准备好切片上传
  // 切片上传的结果
  const uploadFlag = await uploadGroupFileList(
    groupFileList,
    splitInfo,
    onProgress
  );
  if (!uploadFlag) return false;
  // 请求服务器合并切片文件
  const mergeResult = await mergeFileFlag(identifier);
  if (!mergeResult) return false;
  // 请求服务器合并切片成功后  调一次上传进度回调接口  让上传进度100%
  onProgress({
    total: totalChunks + 1,
    loaded: totalChunks + 1,
  });
  return mergeResult;
};

分组上传分片文件

将分片文件根据splitCount 分组,一次只发送一组请求,等待上一组请求全部完成,再发送下一组请求。1是因为如果文件过大的话,同时发送过多的请求,服务器可能会处理不了,服务异常导致上传文件失败。2是因为同时发送的请求,最后一个请求可能会等待很久才返回,可能会出现请求超时,导致上传文件失败。3.如果一个文件上传完,再上传后一个文件的话,不能够减少上传文件的时间,一次性,传多个请求可以减少上传时间 。 4.可以根据不同服务器的带宽能力,调试一次性上传多少个文件合适。


js axios上传文件 javascript文件上传_javascript

分组上传效果图

 

 


js axios上传文件 javascript文件上传_开发语言_02

上传进度及效果图