一、前言

最近chatGPT十分火爆,我也抽暇运用OpenAI的接口搭建了个自用的AI谈天网站Onlyy Bot(运用需求key)。今天,突发奇想让他帮我写代码,以便看看自己被替代的或许性有多高。下面是给他的指令。

完成一个大文件上传功用的代码,以下是整体思路和要求:语言为js,运用axiosvue的组合式API,大文件进行分片上传,分片时记载每个片段的顺序索引,运用spark-md5经过文件内容核算hash值作为文件名并调用后端接口exsitFile,传入hash值以及chunk总数量来判别该文件此前是否上传过。注意,exsitFile接口回来的数据包含done和existChunks两个字段:done是一个布尔值,用于标识整个大文件是否悉数上传完结;existChunks是一个数组,存储各个chunk对应的索引。上传每个文件片段时都需求将大文件的文件名、文件chunk、以及该chunk对应的索引一起经过接口uploadFileChunk发送给后端,uploadFileChunk接口回来根据后端现已接纳保存的chunk数量和大文件的chunk总数核算得到的当前上传总进展百分比。如果某个chunk发送失利,则自动重试。

需求满意以下基本功用:

  1. 若该文件此前现已上传,且上传完结,则本次一键秒传;
  2. 若该文件此前现已上传过,但是并未悉数上传完结,则本次持续上传剩下的部分;
  3. 若该文件此前未上传过,则本次重新开端分片上传。

额外要求:

  1. 需求有进展显现;
  2. 运用useWorker优化
  3. 将其封装为一个hook,以供多处复用;
  4. 供给前端运用示例。

二、gpt == 生产力

以下是chatGPT生成的内容,不确保完全正确:

让`chatGPT`写完大文件切片上传,感觉自己离失业不远了


这是一个比较复杂的需求,需求对前端的文件上传和后端的文件处理都有比较深入的了解。以下是JS代码的完成,其中包含了文件分片、文件上传重试、文件进展核算等功用:

import axios from 'axios';
import SparkMD5 from 'spark-md5';
​
/**
 * 核算文件的 MD5 值
 * @param {File} file
 * @returns {Promise<String>}
 */
function calculateMD5(file) {
 return new Promise((resolve) => {
  const fileReader = new FileReader();
  const spark = new SparkMD5();
  const CHUNK_SIZE = 2 * 1024 * 1024; // 每次读取文件片段的巨细
  let currentChunk = 0;
​
  fileReader.onload = function (e) {
   spark.appendBinary(e.target.result);
   currentChunk++;
   // 递归读取文件片段,直到读取完整个文件
   if (currentChunk < chunks) {
    loadNext();
    } else {
    const result = spark.end();
    resolve(result);
    }
   };
​
  function loadNext() {
   const start = currentChunk * CHUNK_SIZE;
   const end = Math.min(file.size, start + CHUNK_SIZE);
   fileReader.readAsBinaryString(file.slice(start, end));
   }
​
  const chunks = Math.ceil(file.size / CHUNK_SIZE);
  loadNext();
  });
}
​
/**
 * 上传文件片段操作
 * @param {String} url
 * @param {FormData} chunkFormData
 * @returns {Promise}
 */
function uploadChunk(url, chunkFormData) {
 return axios.post(url, chunkFormData, {
  headers: {
   'Content-Type': 'multipart/form-data',
   },
  // 装备请求超时时刻和最大重试次数
  timeout: 10000,
  retry: 3,
  retryDelay: 2000,
  onRetry: (err) => {
   console.log(`重试上传文件片段:${err}`);
   },
  });
}
​
/**
 * 根据文件 MD5 判别文件是否上传过
 * @param {String} url
 * @param {String} md5
 * @param {Number} chunks
 * @returns {Promise<Object>} { done: Boolean, existChunks: Array }
 */
function checkFileExist(url, md5, chunks) {
 return axios.post(url, {
  md5: md5,
  chunks: chunks,
  });
}
​
/**
 * 上传文件的 Hook
 * @param {String} uploadUrl
 * @param {String} existUrl
 * @returns {Object}
 */
function useFileUploader(uploadUrl, existUrl) {
 const progressRef = ref(0); // 文件上传进展的呼应引证
 const isCompleteRef = ref(false); // 文件上传是否完结的呼应引证
 const isSuccessRef = ref(false); // 文件上传是否成功的呼应引证
 const isUploadingRef = ref(false); // 文件上传是否正在进行的呼应引证/**
  * 上传文件操作
  * @param {File} file
  * @returns {Promise}
  */
 async function uploadFile(file) {
  isUploadingRef.value = true;
  const md5 = await calculateMD5(file);
  const existFile = await checkFileExist(existUrl, md5, Math.ceil(file.size / chunkSize));
  if (existFile.done && existFile.existChunks.length === 0) {
   // 一键秒传
   progressRef.value = 100;
   isCompleteRef.value = true;
   isSuccessRef.value = true;
   return Promise.resolve();
   } else if (existFile.existChunks.length === Math.ceil(file.size / chunkSize)) {
   // 文件现已上传完结
   progressRef.value = 100;
   isCompleteRef.value = true;
   isSuccessRef.value = true;
   return Promise.resolve();
   } else {
   // 持续上传剩下的部分或重新开端上传
   const startChunk = existFile.existChunks.length; // 断点方位
   const formData = new FormData();
   formData.append('filename', md5);
   formData.append('totalChunks', Math.ceil(file.size / chunkSize));
   for (let i = startChunk; i < Math.ceil(file.size / chunkSize); i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);
    formData.append('chunk', chunk);
       formData.append('index', i.toString());
       try {
        await uploadChunk(uploadUrl, formData);
        progressRef.value = ((i + 1) / Math.ceil(file.size / chunkSize)) * 100;
       } catch (error) {
        // 产生过错,重试
        console.log(error);
        i--;
       }
      }
      isCompleteRef.value = true;
      isSuccessRef.value = true;
      return Promise.resolve();
    }
  }
  return {
  progressRef,
  isCompleteRef,
  isSuccessRef,
  isUploadingRef,
  uploadFile,
  };
}

在此基础上,能够根据自己的需求进行调整和完善。一起供给一个简略的前端运用示例:

import { useFileUploader } from './useFileUploader';
​
export default {
 setup() {
  const {
   progressRef,
   isCompleteRef,
   isSuccessRef,
   isUploadingRef,
   uploadFile,
   } = useFileUploader('/upload', '/existFile');
​
  function handleFileChange(event) {
   const fileList = event.target.files;
   if (fileList) {
    const file = fileList[0];
    uploadFile(file)
      .then(() => {
      console.log('文件上传成功!');
      })
      .catch(() => {
      console.log('文件上传失利!');
      });
    }
   }
​
  return {
   progressRef,
   isCompleteRef,
   isSuccessRef,
   isUploadingRef,
   handleFileChange,
   };
  },
};

(持续)

上面的代码还能够进一步优化,比方能够运用 Web Worker 进行文件分片的核算,以防止堵塞主线程。详细完成能够参阅以下代码:

// 核算文件 MD5 值的 Worker 线程代码,独立于主线程运作
self.importScripts('/spark-md5.min.js');
self.onmessage = function (e) {
 const fileReader = new FileReader();
 const spark = new SparkMD5();
 const CHUNK_SIZE = 2 * 1024 * 1024;
 let currentChunk = 0;
​
 fileReader.onload = function (e) {
  spark.appendBinary(e.target.result);
  currentChunk++;
  if (currentChunk < e.data.chunks) {
   loadNext();
   } else {
   const result = spark.end();
   self.postMessage(result);
   }
  };
​
 function loadNext() {
  const start = currentChunk * CHUNK_SIZE;
  const end = Math.min(e.data.file.size, start + CHUNK_SIZE);
  fileReader.readAsBinaryString(e.data.file.slice(start, end));
  }
​
 const chunks = Math.ceil(e.data.file.size / CHUNK_SIZE);
 loadNext();
};
​
/**
 * 核算文件的 MD5 值
 * @param {File} file
 * @returns {Promise<String>}
 */
function calculateMD5(file) {
 return new Promise((resolve) => {
  // 创建 Worker 目标
  const worker = new Worker('/md5Worker.js');
  worker.postMessage({
   file: file,
   chunks: Math.ceil(file.size / chunkSize),
   });
  worker.onmessage = function (e) {
   resolve(e.data);
   worker.terminate(); // 核算完结后毁掉 Worker 目标
   };
  });
}

关于文件分片的操作,也能够运用 Worker 线程来处理。代码如下:

self.onmessage = function (e) {
 const chunkFormData = e.data.chunkFormData;
 const url = e.data.url;
 const retryTimes = e.data.retryTimes;
​
 /**
  * 上传文件片段操作
  * @param {Number} retryTimes
  * @returns {Promise}
  */
 async function doUpload(retryTimes) {
  try {
   const response = await axios.post(url, chunkFormData, {
    headers: {
     'Content-Type': 'multipart/form-data',
     },
    // 装备请求超时时刻和最大重试次数
    timeout: 10000,
    retry: retryTimes, // 重试次数
    retryDelay: 2000, // 重试延时
    onRetry: (err) => {
     console.log(`重试上传文件片段:${err}`);
     },
    });
   self.postMessage(response.data);
   } catch (error) {
   if (retryTimes > 0) {
    // 产生过错,持续重试
    doUpload(retryTimes - 1);
    } else {
    self.postMessage(error);
    }
   }
  }
​
 doUpload(retryTimes);
};

运用 Worker线程的优势在于能够防止主线程被堵塞,进步页面的呼应速度。但一起也需求注意 Worker线程与主线程的通讯方法和数据传输格局,以及线程的毁掉和反常处理等问题。

(持续)

针对大文件的上传,还能够运用分布式上传的计划,行将文件分片上传到多个不同的服务器上,并行进行上传,然后进步上传速度和稳定性。详细完成能够参阅以下代码:

/**
 * 根据文件名和分片序号生成分片文件名
 * @param {String} filename
 * @param {Number} index
 * @returns {String}
 */
function getChunkFilename(filename, index) {
 return `${filename}-${index}`;
}
​
/**
 * 上传文件的 Hook
 * @param {Array<String>} uploadUrls
 * @param {String} existUrl
 * @returns {Object}
 */
function useFileUploader(uploadUrls, existUrl) {
 const progressRef = ref(0);
 const isCompleteRef = ref(false);
 const isSuccessRef = ref(false);
 const isUploadingRef = ref(false);
​
 async function uploadFile(file) {
  isUploadingRef.value = true;
  const md5 = await calculateMD5(file);
  const existFile = await checkFileExist(existUrl, md5, Math.ceil(file.size / chunkSize));
  if (existFile.done && existFile.existChunks.length === 0) {
   progressRef.value = 100;
   isCompleteRef.value = true;
   isSuccessRef.value = true;
   return Promise.resolve();
   } else if (existFile.existChunks.length === Math.ceil(file.size / chunkSize)) {
   progressRef.value = 100;
   isCompleteRef.value = true;
   isSuccessRef.value = true;
   return Promise.resolve();
   } else {
   const startChunk = existFile.existChunks.length;
   const formDataList = []; // FormData 目标列表
   for (let i = startChunk; i < Math.ceil(file.size / chunkSize); i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);
    const chunkFormData = new FormData();
    const chunkFilename = getChunkFilename(md5, i);
    chunkFormData.append('filename', md5);
    chunkFormData.append('totalChunks', Math.ceil(file.size / chunkSize));
​
    // 将分片保存到不同的服务端上
    for (let i = 0; i < uploadUrls.length; i++) {
     const uploadUrl = uploadUrls[i];
     chunkFormData.append('chunk', chunk, chunkFilename);
     chunkFormData.append('index', i.toString());
     formDataList[i] = formDataList[i] || new FormData();
     formDataList[i].append('filename', md5);
     formDataList[i].append('totalChunks', Math.ceil(file.size / chunkSize));
     formDataList[i].append('chunk', chunk, chunkFilename);
     formDataList[i].append('index', i.toString());
     }
    }
​
   // 将分片传送到不同的服务器上并行上传
   const uploadPromises = formDataList.map((formData, index) =>
    uploadChunk(uploadUrls[index], formData)
    );
   try {
    const uploadResults = await Promise.all(uploadPromises);
    const progressList = uploadResults.map(({ data }) => data.progress);
    progressRef.value = (sum(progressList) / progressList.length) * 100;
    isCompleteRef.value = true;
    isSuccessRef.value = true;
    } catch (error) {
    console.log(error);
    isCompleteRef.value = true;
    isSuccessRef.value = false;
    }
   }
  }
​
 return {
  progressRef,
  isCompleteRef,
  isSuccessRef,
  isUploadingRef,
  uploadFile,
  };
}

以上是一个比较完整的大文件上传的完成计划,其中包含文件分片、断点续传、上传进展核算、上传重试、分布式上传等功用。部分代码完成或许需求根据详细事务场景进行调整和完善。

(持续)

最终,关于大文件上传,还需求注意以下几个问题:

  1. 文件分片巨细应该合适,一般选用 1MB - 10MB 之间的巨细比较适合,详细巨细能够根据网络环境以及文件巨细进行调整。
  2. 上传进展的核算应该准确,能够经过累加每个分片上传成功的进展来核算总进展。
  3. 断点续传和上传重试功用应该完善,能够经过在服务端记载已上传的分片信息以便续传,并对犯错的分片进行重试操作。
  4. 分布式上传能够运用多个不同的服务器进步上传速度和稳定性,能够运用后端负载均衡技能完成。
  5. 关于用户体会,还能够添加文件巨细、文件格局的约束,以及上传速度的显现等功用,进步用户的操作体会。

以上是关于大文件上传的一些完成计划和注意事项,期望能够对你有所帮助。

(持续)

作为 Web前端工程师,咱们还需求掌握一些前端框架或许库中大文件上传的运用方法,比方 ReactVue等。

下面是一个运用 Reactantd的大文件上传示例代码:

import { Upload, Button, message } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import { useRef, useState } from 'react';
import axios from 'axios';
import SparkMD5 from 'spark-md5';
​
function calculateMD5(file) {
 return new Promise((resolve) => {
  const spark = new SparkMD5.ArrayBuffer();
  const fileReader = new FileReader();
  const chunkSize = 2 * 1024 * 1024;
  let currentChunk = 0;
​
  fileReader.onload = function (e) {
   spark.append(e.target.result);
   currentChunk++;
   if (currentChunk < chunks) {
    loadNext();
    } else {
    const result = spark.end();
    resolve(result);
    }
   };
​
  function loadNext() {
   const start = currentChunk * chunkSize;
   const end = Math.min(file.size, start + chunkSize);
   const buffer = file.slice(start, end);
   fileReader.readAsArrayBuffer(buffer);
   }
​
  const chunks = Math.ceil(file.size / chunkSize);
  loadNext();
  });
}
​
function chunkFile(file, chunkSize) {
 const chunks = Math.ceil(file.size / chunkSize);
 const chunksList = [];
 let currentChunk = 0;
​
 while (currentChunk < chunks) {
  const start = currentChunk * chunkSize;
  const end = Math.min(file.size, start + chunkSize);
  const chunk = file.slice(start, end);
  chunksList.push(chunk);
  currentChunk++;
  }
​
 return chunksList;
}
​
function App() {
 const [uploading, setUploading] = useState(false);
 const chunkRefs = useRef([]);
 const md5Ref = useRef('');
​
 const handleFileChange = async ({ file }) => {
  setUploading(true);
  const md5 = await calculateMD5(file);
  md5Ref.current = md5;
​
  // 将文件分片并保存到 reference 目标中
  const chunksList = chunkFile(file, 2 * 1024 * 1024);
  chunkRefs.current = chunksList.map((chunk, index) => {
   const formData = new FormData();
   formData.append('chunk', chunk);
   formData.append('filename', file.name);
   formData.append('total', chunksList.length);
   formData.append('index', index.toString());
   return formData;
   });
​
  // 上传分片
  const uploadPromises = chunkRefs.current.map((formData) =>
   axios.post('http://localhost:3000/upload', formData)
   );
​
  try {
   await Promise.all(uploadPromises);
   message.success('文件上传成功!');
   } catch (error) {
   console.error(error);
   message.error('文件上传失利!');
   }
​
  setUploading(false);
  };
​
 return (
  <div>
   <Upload
    name="file"
    accept=".jpg,.jpeg,.png,.gif,.zip,.rar,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"
    multiple={false}
    showUploadList={false}
    beforeUpload={() => false}
    onChange={handleFileChange}
   >
    <Button loading={uploading} icon={<UploadOutlined />}>
      {uploading ? '上传中' : '开端上传'}
    </Button>
   </Upload>
  </div>
  );
}
​
export default App;

整体来说,在 antd中完成大文件上传功用比较简略,只需求调用 Upload组件,而且装备相关特点即可。需求注意的是,在 beforeUpload函数中回来 false,以防止 Upload组件默许的上传操作。

Vue中的大文件上传完成方法也比较相似,能够运用 ElementUI或许其他相关组件库,详细运用方法能够参阅官方文档。

整体来说,运用前端框架和组件库的大文件上传完成,能够有效地进步开发效率和代码可维护性,一起也具有丰厚的功用和良好的用户体会。

(持续)

(gpt现已不想写了,强行持续了一波)

关于大文件上传,还有一些常见的问题和注意事项,需求咱们在开发中进行防止和解决。

  1. 网络不稳定:在上传大文件时,网络不稳定很容易导致上传失利或许上传速度变慢。能够经过添加上传重试次数、调整网络参数等方法进行解决。
  2. 服务器资源耗费:大文件上传也会占用服务器的资源,特别是在分布式上传时,需求考虑服务器负载均衡等问题,而且合理运用缓存技能,削减服务器压力。
  3. 安全问题:如果数据传输过程中呈现安全问题,或许会导致数据走漏或许攻击。因而,在上传过程中需求选用加密、签名等技能,确保数据的安全性。
  4. 兼容性问题:在不同的浏览器和操作系统上,或许会存在兼容性问题,需求进行充分的测验和兼容性处理。
  5. 上传速度慢:当文件较大时,上传速度或许会很慢,影响用户的体会。能够选用分片上传和分布式上传等方法进步上传速度。

总之,大文件上传是一个比较复杂的后端开发问题,需求充分考虑数据安全性、上传速度、网络稳定性、兼容性等方面的因素,一起也需求前端工程师结合详细事务需求,进行灵敏的完成计划。


尽管有的当地逻辑不正确,不能开箱即用。但随着持续进化,这些问题也许最终都不再是问题。话说回来,关于gpt写的这份代码,能打几分呢?