一、前言
最近chatGPT
十分火爆,我也抽空利用OpenAI
的接口搭建了个自用的AI
聊天网站Onlyy Bot
(使用需要key
)。今天,突发奇想让他帮我写代码,以便看看自己被取代的可能性有多高。下面是给他的指令。
实现一个大文件上传功能的代码,以下是整体思路和要求:语言为js
,使用axios
和vue
的组合式API
,大文件进行分片上传,分片时记录每个片段的顺序索引,使用spark-md5
通过文件内容计算hash值作为文件名并调用后端接口exsitFile
,传入hash值以及chunk总数量来判断该文件此前是否上传过。注意,exsitFile
接口返回的数据包含done和existChunks
两个字段:done是一个布尔值,用于标识整个大文件是否全部上传完成;existChunks
是一个数组,存储各个chunk
对应的索引。上传每个文件片段时都需要将大文件的文件名、文件chunk
、以及该chunk
对应的索引一并通过接口uploadFileChunk
发送给后端,uploadFileChunk
接口返回根据后端已经接收保存的chunk
数量和大文件的chunk总数计算得到的当前上传总进度百分比。如果某个chunk发送失败,则自动重试。
需要满足以下基本功能:
- 若该文件此前已经上传,且上传完成,则本次一键秒传;
- 若该文件此前已经上传过,但是并未全部上传完成,则本次继续上传剩余的部分;
- 若该文件此前未上传过,则本次重新开始分片上传。
额外要求:
- 需要有进度显示;
- 使用
useWorker
优化; - 将其封装为一个hook,以供多处复用;
- 提供前端使用示例。
二、gpt == 生产力
?
以下是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('https://b2.7b2.com/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('https://b2.7b2.com/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,
};
}
以上是一个比较完整的大文件上传的实现方案,其中包括文件分片、断点续传、上传进度计算、上传重试、分布式上传等功能。部分代码实现可能需要根据具体业务场景进行调整和完善。
(继续)
最后,对于大文件上传,还需要注意以下几个问题:
- 文件分片大小应该合适,一般采用
1MB - 10MB
之间的大小比较适合,具体大小可以根据网络环境以及文件大小进行调整。 - 上传进度的计算应该准确,可以通过累加每个分片上传成功的进度来计算总进度。
- 断点续传和上传重试功能应该完善,可以通过在服务端记录已上传的分片信息以便续传,并对出错的分片进行重试操作。
- 分布式上传可以使用多个不同的服务器提高上传速度和稳定性,可以利用后端负载均衡技术实现。
- 对于用户体验,还可以增加文件大小、文件格式的限制,以及上传速度的显示等功能,提高用户的操作体验。
以上是关于大文件上传的一些实现方案和注意事项,希望可以对你有所帮助。
(继续)
作为 Web
前端工程师,我们还需要掌握一些前端框架或者库中大文件上传的使用方法,比如 React
和 Vue
等。
下面是一个使用 React
和 antd
的大文件上传示例代码:
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
已经不想写了,强行继续了一波)
对于大文件上传,还有一些常见的问题和注意事项,需要我们在开发中进行避免和解决。
- 网络不稳定:在上传大文件时,网络不稳定很容易导致上传失败或者上传速度变慢。可以通过增加上传重试次数、调整网络参数等方式进行解决。
- 服务器资源消耗:大文件上传也会占用服务器的资源,特别是在分布式上传时,需要考虑服务器负载均衡等问题,并且合理利用缓存技术,减少服务器压力。
- 安全问题:如果数据传输过程中出现安全问题,可能会导致数据泄露或者攻击。因此,在上传过程中需要采用加密、签名等技术,保证数据的安全性。
- 兼容性问题:在不同的浏览器和操作系统上,可能会存在兼容性问题,需要进行充分的测试和兼容性处理。
- 上传速度慢:当文件较大时,上传速度可能会很慢,影响用户的体验。可以采用分片上传和分布式上传等方式提高上传速度。
总之,大文件上传是一个比较复杂的后端开发问题,需要充分考虑数据安全性、上传速度、网络稳定性、兼容性等方面的因素,同时也需要前端工程师结合具体业务需求,进行灵活的实现方案。
尽管有的地方逻辑不正确,不能开箱即用。但随着继续进化,这些问题也许最后都不再是问题。话说回来,关于gpt
写的这份代码,能打几分呢?