大致原理就是将大文件分割成好几个部分(根据固定数量/固定大小方式),每个切片都有自己的数据和各自的名字,每一部分都发起一次ajax
请求,将切片传递到服务器端。服务器端根据文件创建一个文件夹,用来存放大文件的切片,当客户端将全部切片传递到服务器端的时候,再发起一次请求告知服务器端,前端将数据全部传递完成了,服务器端接收到传递完成的通知的时候,将刚刚文件夹里面的文件全部合并成一个文件,最后将该文件夹删除。简短概括:大文件-->拆成很多小文件-->发起很多ajax请求发送小文件-->服务器端接收小文件-->组装成大文件
-
1、将大文件拆分成很多小文件来上传
... // 根据文件内容生成唯一的hash import SparkMD5 from "spark-md5"; ... // 开始上传 async confirmUpload () { let formData = new FormData(); const currentFile = this.currentFile; // 上传前的钩子函数 const flag = this.beforeUpload(currentFile); if (!flag) { return false; }; const fileBuffer = await this.fileParse(currentFile, 'buffer'); let spark = new SparkMD5.ArrayBuffer(); spark.append(fileBuffer); const hash = spark.end(); const suffix = /\.([0-9a-zA-Z]+)$/i.exec(currentFile.name)[1]; // 将文件切割为100份来上传 let partList = []; const partSize = currentFile.size / 100; let cur = 0; for (let i = 0; i < 100; i++) { let item = { chunk: currentFile.slice(cur, cur + partSize), filename: `${hash}_${i}.${suffix}`, } cur += partSize; partList.push(item); } this.partList = partList; this.hash = hash; // 发送ajax请求到服务器端 this.sendRequest(); },
根据文件切片发起ajax
请求
async sendRequest () { // 根据多少切片来创建多少请求 let requestList = []; // 设置请求头 const headers = { // "Content-Type": "multipart/form-data", } this.partList.forEach((item, index) => { const fn = () => { let formData = new FormData(); formData.append('chunk', item.chunk); formData.append('filename', item.filename); // 发送ajax请求 axios.post('/upload3', formData, { headers }).then(res => { const data = res.data; if (data.code == 0) { this.total += 1; // 传完的切片我们把它移除掉 this.partList.splice(index, 1); } }) } requestList.push(fn); }); let currentIndex = 0; const send = async () => { // 如果中断上传就不在发送请求 if (this.abort) return; if (currentIndex >= requestList.length) { // 调用上传完成的按钮,告诉后端合并文件 this.complete(); return; } await requestList[currentIndex](); currentIndex++; send(); } send(); },
全部切片上传完成后通知后端上传完成
// 文件上传,需要后端合并文件 complete () { axios.get('/merge', { params: { hash: this.hash, } }).then(res => { console.log(res, '上传完成'); }) },
模拟暂停与开始
// 暂停和开始 handleBtn () { if (this.btn) { //断点续传 this.abort = false; this.btn = false; this.sendRequest(); return; } //暂停上传 this.btn = true; this.abort = true; }
大文件上传后端部分代码
接收文件切片
// 切片上传 app.post('/upload3', async (req, res) => { const {fields,files} = await handleMultiparty(req, res, true); const [chunk] = files.chunk; const [filename] = fields.filename; // 获取上传文件的hash const hash = /([0-9a-zA-Z]+)_\d+/.exec(filename)[1]; const dir = `${uploadDir}/${hash}`; if (!fs.existsSync(dir)) { fs.mkdirSync(dir); } const path = `${dir}/${filename}`; fs.access(path, async err => { // 如果已经存在了就不做任何处理 if (!err) { res.send({ code: 0, path: path.replace(__dirname, `http://127.0.0.1:${PORT}`) }) } // 测试上传需要时间,手动延迟 await new Promise(resolve => { setTimeout(() => { resolve(); }, 100); }); // 不存在的时候就创建 const readStream = fs.createReadStream(chunk.path); const writeStream = fs.createWriteStream(path); readStream.pipe(writeStream); readStream.on('end', function() { fs.unlinkSync(chunk.path); res.send({ code: 0, path: path.replace(__dirname, `http://127.0.0.1:${PORT}`) }); }) }) });
合并多个切片文件
// 大文件上传后 app.get('/merge',(req, res) => { const { hash } = req.query; const path = `${uploadDir}/${hash}`; const fileList = fs.readdirSync(path); let suffix = null; fileList.sort((a, b) => { const reg = /_(\d+)/; return reg.exec(a)[1] - reg.exec(b)[1]; }).forEach(item => { !suffix ? suffix = /\.([0-9a-zA-Z]+)$/.exec(item)[1] : null; // 写入文件 fs.appendFileSync(`${uploadDir}/${hash}.${suffix}`, fs.readFileSync(`${path}/${item}`)); // 删除文件 fs.unlinkSync(`${path}/${item}`); }); fs.rmdirSync(path); res.send({ code: 0, path: `http://127.0.0.1:${PORT}/upload/${hash}.${suffix}` }); })