当前位置:   article > 正文

前端-JavaScript 大文件分片上传 multipartUpload

multipartupload

主要流程: 

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

例子:

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

multipartUpload  上传文件方法  

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

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

  1. /**
  2. * @param {*} file 上传文件
  3. * @param {*} onProgress 切片上传进度
  4. * @param {*} splitSize 每个切片文件的大小
  5. * @param {*} splitCount 一次发送多少个切片请求
  6. * @returns
  7. */
  8. // 文件切片上传
  9. export const multipartUpload = async (
  10. file, // 切片原文件
  11. onProgress = () => {}, // 切片回调参数
  12. splitSize = 5, // 切片大小 默认5 MB
  13. splitCount = 5 // 一次性发送切片请求的个数 默认 5
  14. ) => {
  15. // 简单检查一下file 参数
  16. if (!file || typeof file != "object" || Array.isArray(file))
  17. throw new Error(" a required parameter 'file' missing .");
  18. // 简单判断一下 splitSize 必须是大于或等于 1 的 数字
  19. if (typeof splitSize != "number" || splitSize < 1)
  20. throw new Error(
  21. " the type of 'splitSize' must be 'number' and greater than 1 ."
  22. );
  23. // 简单判断一下 splitCount 必须是大于或等于 1 的 数字
  24. if (typeof splitCount !== "number" || splitCount < 1)
  25. throw new Error(
  26. " the type of 'splitCount' must be 'number' and greater than 1 ."
  27. );
  28. let _file = file.file || file;
  29. const { size, name, type, lastModified } = _file; // 获取文件大小 和名称
  30. let identifier = await getIdentifier(_file); // 根据文件md5 和当前时间戳生成的唯一标识符
  31. const totalChunks = Math.ceil(size / splitSize / 1024 / 1024); // 切片总块数 文件大小/ 切片大小向上取整
  32. const splitFileList = splitFile(_file, splitSize); // 返回切片数组
  33. let groupFileList = groupFileListByCount(splitFileList, splitCount); // 将 切片数组根据count 分组
  34. let splitInfo = { totalChunks, identifier, fileName: name, type }; // 文件切片信息
  35. // 上传分片文件件 告诉服务器准备上传的切片文件信息
  36. let beforeUploadFlag = await beforeUpload(splitInfo);
  37. if (!beforeUploadFlag) return false; // 当服务器没准备好切片上传
  38. // 切片上传的结果
  39. const uploadFlag = await uploadGroupFileList(
  40. groupFileList,
  41. splitInfo,
  42. onProgress
  43. );
  44. if (!uploadFlag) return false;
  45. // 请求服务器合并切片文件
  46. const mergeResult = await mergeFileFlag(identifier);
  47. if (!mergeResult) return false;
  48. // 请求服务器合并切片成功后 调一次上传进度回调接口 让上传进度100%
  49. onProgress({
  50. total: totalChunks + 1,
  51. loaded: totalChunks + 1,
  52. });
  53. return mergeResult;
  54. };

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

  1. // 根据文件MD5 + timestamp 生成的唯一标识
  2. export const getIdentifier = (file) => {
  3. return new Promise((resolve, reject) => {
  4. let reader = new FileReader();
  5. let spark = new SparkMD5();
  6. reader.readAsBinaryString(file);
  7. reader.onload = (e) => {
  8. spark.appendBinary(e.target.result);
  9. const md5 = spark.end();
  10. resolve(md5 + Date.now());
  11. };
  12. });
  13. };

splitFile 根据splitSize 文件进行分片 

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

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

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

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

  1. export const beforeUpload = async (splitInfo) => {
  2. try {
  3. const res = await that.$httpRequest({
  4. url: "/sys-file/prepareUpload",
  5. method: "post",
  6. data: splitInfo,
  7. isForm: true,
  8. });
  9. if (res.code == 0) return true;
  10. return false;
  11. } catch (err) {
  12. return false;
  13. }
  14. };

uploadGroupFileList  上传分组中的分片 

  1. // 上传分组的切片文件
  2. export const uploadGroupFileList = async (
  3. list = [],
  4. splitInfo = {},
  5. onProgress // 每一个切片上传成功之后 调一次上传回调 模拟一个进度条
  6. ) => {
  7. /**
  8. * 模拟一个进度条
  9. * total+1 避免当所有的切片都上传完成后进度100% 。而还没有请求服务器合并切片
  10. * 或者请求服务器合并分片失败 而上传进度为100% 对用户不友好
  11. */
  12. let progress = {
  13. total: splitInfo.totalChunks + 1,
  14. loaded: 0,
  15. };
  16. for (let i = 0; i < list.length; i++) {
  17. let itemList = Array.isArray(list[i]) ? list[i] : [];
  18. let itemHttpUpload = itemList.map((item, index) => {
  19. return new Promise(async (resolve, reject) => {
  20. let chunkNumber = i * list[i].length + index + 1; // 当前是所有文件切片中的第几个
  21. let { totalChunks, identifier, fileName } = splitInfo;
  22. let params = { totalChunks, chunkNumber, identifier, fileName };
  23. uploadFile(item, params, "/sys-file/multiUpload")
  24. .then((res) => {
  25. // 上传进度回调
  26. progress.loaded++;
  27. onProgress(progress);
  28. resolve(chunkNumber);
  29. })
  30. .catch((err) => {
  31. reject(chunkNumber);
  32. });
  33. });
  34. });
  35. // Promise.allSettled 等待上一组 upload http请求所有都完成后 返回结果
  36. const lastGroupUpload = await Promise.allSettled(itemHttpUpload)
  37. .then((res) => {
  38. return Promise.resolve(res);
  39. })
  40. .catch((err) => {
  41. return Promise.resolve(err);
  42. });
  43. // 将上传失败的分片文件存储起来
  44. let errorChunkList = lastGroupUpload
  45. .filter((item) => item.status === "rejected")
  46. .map((item) => item.reason);
  47. // 判断上一组的http 中是否有上传失败的分片 存在则不继续上传剩余的分片 返回 false
  48. if (errorChunkList.length > 0) return false;
  49. }
  50. return true;
  51. };

mergeFileFlag  请求合并分片 

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

  1. // 分片全部上传完成之后 请求服务器合并 上传的分片文件
  2. export const mergeFileFlag = async (identifier) => {
  3. try {
  4. const params = { identifier, convert: 0 };
  5. const res = await that.$httpRequest({
  6. url: "/sys-file/complete",
  7. method: "post",
  8. isForm: true,
  9. data: params,
  10. });
  11. if (res.code == 0 && res.data) {
  12. return res.data;
  13. }
  14. return false;
  15. } catch (err) {
  16. return false;
  17. }
  18. };

完整代码

  1. import Vue from "vue";
  2. import $ from "jquery";
  3. import SparkMD5 from "spark-md5"; // 获取文件MD5
  4. import store from "@/store";
  5. let that = new Vue(); // that 指向一个新Vue 实例 ,用于调用 Vue prototype 的一些一些方法 例如 deepClone 、httpRequest 等
  6. const is_Dev = process.env.NODE_ENV == "development" ? true : false;
  7. let baseUrl = is_Dev
  8. ? window.globalConfig.DEV_BASE_API
  9. : window.globalConfig.PRO_BASE_API;
  10. // 单独封装一个分片上传的请求
  11. export const uploadFile = (file, config = {}, url = "/sys-file/upload") => {
  12. if (!file || typeof file != "object" || Array.isArray(file))
  13. throw new Error(" a required parameter 'file' missing . ");
  14. let _file = file.file || file;
  15. let _para = new FormData();
  16. _para.append("file", _file);
  17. // 除文件外的其他参数
  18. if (typeof config == "object" && !Array.isArray(config)) {
  19. const keyList = Object.keys(config);
  20. keyList.map((item) => {
  21. _para.append(item, config[item]);
  22. });
  23. }
  24. return new Promise((resolve, reject) => {
  25. let options = {
  26. url: baseUrl + url,
  27. method: "POST",
  28. data: _para,
  29. timeout: 60000,
  30. contentType: false,
  31. processData: false,
  32. headers: {
  33. Authorization: "Bearer " + store.state.user.access_token,
  34. },
  35. };
  36. $.ajax(options)
  37. .then((res) => {
  38. resolve(res);
  39. })
  40. .catch((err) => {
  41. if (err.statusText == "timeout") err.abort();
  42. reject(err);
  43. });
  44. });
  45. };
  46. // 根据splitSize 对文件进行切片
  47. export const splitFile = (file, splitSize = 5) => {
  48. const _5M = 1024 * 1024 * splitSize; // 默认分割成5M
  49. const { size, type } = file;
  50. let tempFileList = []; // 盛放切割后的切片
  51. for (let index = 0; index < size; index += _5M) {
  52. let start = index; // 切片开始位置
  53. let end = start + _5M < size ? index + _5M : size; // 切片结束位置
  54. let item = file.slice(start, end, type); // 切割的文件
  55. tempFileList.push(item);
  56. }
  57. return tempFileList;
  58. };
  59. // 根据文件MD5 + timestamp 生成的唯一标识
  60. export const getIdentifier = (file) => {
  61. return new Promise((resolve, reject) => {
  62. let reader = new FileReader();
  63. let spark = new SparkMD5();
  64. reader.readAsBinaryString(file);
  65. reader.onload = (e) => {
  66. spark.appendBinary(e.target.result);
  67. const md5 = spark.end();
  68. resolve(md5 + Date.now());
  69. };
  70. });
  71. };
  72. // 分片上传前 告诉服务器 分片文件信息
  73. export const beforeUpload = async (splitInfo) => {
  74. try {
  75. const res = await that.$httpRequest({
  76. url: "/sys-file/prepareUpload",
  77. method: "post",
  78. data: splitInfo,
  79. isForm: true,
  80. });
  81. if (res.code == 0) return true;
  82. return false;
  83. } catch (err) {
  84. return false;
  85. }
  86. };
  87. // 分片全部上传完成之后 请求服务器合并 上传的分片文件
  88. export const mergeFileFlag = async (identifier) => {
  89. try {
  90. const params = { identifier, convert: 0 };
  91. const res = await that.$httpRequest({
  92. url: "/sys-file/complete",
  93. method: "post",
  94. isForm: true,
  95. data: params,
  96. });
  97. if (res.code == 0 && res.data) {
  98. return res.data;
  99. }
  100. return false;
  101. } catch (err) {
  102. return false;
  103. }
  104. };
  105. // 将fileList 分组
  106. export const groupFileListByCount = (fileList, count) => {
  107. let group = Math.ceil(fileList.length / count); // 将 fileList 分为多少个组
  108. let resultList = new Array(group); // 根据组数创建一个空的 list
  109. fileList.map((item, index) => {
  110. let i = Math.floor(index / count); // index / count 向下取整 为组数编号
  111. if (Array.isArray(resultList[i])) resultList[i].push(item);
  112. else resultList[i] = [item];
  113. });
  114. return resultList;
  115. };
  116. // 上传分组的切片文件
  117. export const uploadGroupFileList = async (
  118. list = [],
  119. splitInfo = {},
  120. onProgress // 每一个切片上传成功之后 调一次上传回调 模拟一个进度条
  121. ) => {
  122. /**
  123. * 模拟一个进度条
  124. * total+1 避免当所有的切片都上传完成后进度100% 。而还没有请求服务器合并切片
  125. * 或者请求服务器合并分片失败 而上传进度为100% 对用户不友好
  126. */
  127. let progress = {
  128. total: splitInfo.totalChunks + 1,
  129. loaded: 0,
  130. };
  131. for (let i = 0; i < list.length; i++) {
  132. let itemList = Array.isArray(list[i]) ? list[i] : [];
  133. let itemHttpUpload = itemList.map((item, index) => {
  134. return new Promise(async (resolve, reject) => {
  135. let chunkNumber = i * list[i].length + index + 1; // 当前是所有文件切片中的第几个
  136. let { totalChunks, identifier, fileName } = splitInfo;
  137. let params = { totalChunks, chunkNumber, identifier, fileName };
  138. uploadFile(item, params, "/sys-file/multiUpload")
  139. .then((res) => {
  140. // 上传进度回调
  141. progress.loaded++;
  142. onProgress(progress);
  143. resolve(chunkNumber);
  144. })
  145. .catch((err) => {
  146. reject(chunkNumber);
  147. });
  148. });
  149. });
  150. // Promise.allSettled 等待上一组 upload http请求所有都完成后 返回结果
  151. const lastGroupUpload = await Promise.allSettled(itemHttpUpload)
  152. .then((res) => {
  153. return Promise.resolve(res);
  154. })
  155. .catch((err) => {
  156. return Promise.resolve(err);
  157. });
  158. // 将上传失败的分片文件存储起来
  159. let errorChunkList = lastGroupUpload
  160. .filter((item) => item.status === "rejected")
  161. .map((item) => item.reason);
  162. // 判断上一组的http 中是否有上传失败的分片 存在则不继续上传剩余的分片 返回 false
  163. if (errorChunkList.length > 0) return false;
  164. }
  165. return true;
  166. };
  167. /**
  168. * @param {*} file 上传文件
  169. * @param {*} onProgress 切片上传进度
  170. * @param {*} splitSize 每个切片文件的大小
  171. * @param {*} splitCount 一次发送多少个切片请求
  172. * @returns
  173. */
  174. // 文件切片上传
  175. export const multipartUpload = async (
  176. file, // 切片原文件
  177. onProgress = () => {}, // 切片回调参数
  178. splitSize = 5, // 切片大小 默认5 MB
  179. splitCount = 5 // 一次性发送切片请求的个数 默认 5
  180. ) => {
  181. // 简单检查一下file 参数
  182. if (!file || typeof file != "object" || Array.isArray(file))
  183. throw new Error(" a required parameter 'file' missing .");
  184. // 简单判断一下 splitSize 必须是大于或等于 1 的 数字
  185. if (typeof splitSize != "number" || splitSize < 1)
  186. throw new Error(
  187. " the type of 'splitSize' must be 'number' and greater than 1 ."
  188. );
  189. // 简单判断一下 splitCount 必须是大于或等于 1 的 数字
  190. if (typeof splitCount !== "number" || splitCount < 1)
  191. throw new Error(
  192. " the type of 'splitCount' must be 'number' and greater than 1 ."
  193. );
  194. let _file = file.file || file;
  195. const { size, name, type, lastModified } = _file; // 获取文件大小 和名称
  196. let identifier = await getIdentifier(_file); // 根据文件md5 和当前时间戳生成的唯一标识符
  197. const totalChunks = Math.ceil(size / splitSize / 1024 / 1024); // 切片总块数 文件大小/ 切片大小向上取整
  198. const splitFileList = splitFile(_file, splitSize); // 返回切片数组
  199. let groupFileList = groupFileListByCount(splitFileList, splitCount); // 将 切片数组根据count 分组
  200. let splitInfo = { totalChunks, identifier, fileName: name, type }; // 文件切片信息
  201. // 上传分片文件件 告诉服务器准备上传的切片文件信息
  202. let beforeUploadFlag = await beforeUpload(splitInfo);
  203. if (!beforeUploadFlag) return false; // 当服务器没准备好切片上传
  204. // 切片上传的结果
  205. const uploadFlag = await uploadGroupFileList(
  206. groupFileList,
  207. splitInfo,
  208. onProgress
  209. );
  210. if (!uploadFlag) return false;
  211. // 请求服务器合并切片文件
  212. const mergeResult = await mergeFileFlag(identifier);
  213. if (!mergeResult) return false;
  214. // 请求服务器合并切片成功后 调一次上传进度回调接口 让上传进度100%
  215. onProgress({
  216. total: totalChunks + 1,
  217. loaded: totalChunks + 1,
  218. });
  219. return mergeResult;
  220. };

分组上传分片文件

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

分组上传效果图

 

 

上传进度及效果图

 

完, 请各位大佬批评指正 ! 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/222648
推荐阅读
相关标签
  

闽ICP备14008679号