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

一、前言

最近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生成的内容,不保证完全正确:

GPT回复


这是一个比较复杂的需求,需要对前端的文件上传和后端的文件处理都有比较深刻的理解。以下是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写的这份代码,能打几分呢?

猜你喜欢

转载自juejin.im/post/7220365570456551481