切片上传(也称为分片上传)是一种处理大文件上传的有效方法,它通过将大文件分割成多个较小的部分(即切片或分片),然后分别上传这些切片到服务器,最后在服务器上将这些切片合并成原始文件。
1. 文件切片
- 确定切片大小:首先,需要确定每个切片的大小。这个大小可以根据实际情况和网络条件来设定,比如常见的切片大小可以是1MB(1024 * 1024字节)或50KB(51200字节)等。
-
切片操作:使用
File.slice()方法(或在某些浏览器中的File.prototype.mozSlice、File.prototype.webkitSlice)对文件进行切片。通过循环遍历文件的每个部分,并使用切片方法获取每个切片。 -
// 假设文件对象存储在file变量中,切片大小为1MB const chunkSize = 1024 * 1024; // 1MB const fileChunks = []; let start = 0; while (start < file.size) { const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); fileChunks.push(chunk); start = end; }
2. 切片上传
-
创建FormData:为每个切片创建一个
FormData对象,并将切片和其他必要的信息(如文件名、切片索引、文件唯一标识等)添加到FormData中。 -
并发控制:控制同时上传的切片数量,以防止过多的并发请求导致浏览器内存溢出或服务器压力过大。可以使用
Promise.race或异步函数结合并发池来实现。 -
上传请求:使用
XMLHttpRequest、fetch或axios等HTTP客户端库发送POST请求,将FormData作为请求体发送到服务器。服务器需要有一个接口来接收这些切片,并保存它们到临时位置。 -
const uploadChunks = async (chunks, fileKey) => { for (let i = 0; i < chunks.length; i++) { const formData = new FormData(); formData.append('file', chunks[i]); formData.append('index', i); formData.append('fileKey', fileKey); try { await axios.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); console.log(`Chunk ${i + 1} uploaded su***essfully.`); } catch (error) { console.error(`Failed to upload chunk ${i + 1}:`, error); // 实现重试机制 } } };
3. 切片验证与重试
- 验证切片是否存在:在上传每个切片之前,可以先向服务器发送一个请求来验证该切片是否已存在。这有助于避免重复上传相同的切片,提高上传效率。
-
// 假设你有一个函数可以发送请求来验证切片 async function checkChunkExists(fileKey, index) { try { const response = await axios.get(`/check-chunk/${fileKey}/${index}`); return response.data.exists; // 假设服务器返回了一个对象,其中exists字段表示切片是否存在 } catch (error) { console.error('Failed to check if chunk exists:', error); return false; } } // 在上传切片之前调用 if (!(await checkChunkExists(fileKey, i))) { // 如果切片不存在,则继续上传 // ... 上传切片的代码 ... } - 重试机制:如果某个切片的上传失败(例如由于网络问题),则需要实现重试机制来重新上传该切片。可以设置重试次数和重试间隔。
-
async function uploadChunk(chunk, fileKey, index, retries = 3) { try { const formData = new FormData(); formData.append('file', chunk); formData.append('index', index); formData.append('fileKey', fileKey); await axios.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); console.log(`Chunk ${index + 1} uploaded su***essfully.`); } catch (error) { if (retries > 0) { console.log(`Failed to upload chunk ${index + 1}, retrying...`); // 等待一段时间后重试 await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒 // 递归调用,减少重试次数 await uploadChunk(chunk, fileKey, index, retries - 1); } else { console.error(`Failed to upload chunk ${index + 1} after ${retries} retries.`); // 可以选择抛出错误或进行其他错误处理 } } } // 调用函数上传切片 uploadChunk(chunks[i], fileKey, i);
4. 合并切片
- 通知服务器合并:在所有切片都成功上传后,向服务器发送一个请求来通知它合并这些切片。这个请求可以包含文件的唯一标识和切片数量等信息。
- 服务器合并:服务器接收到合并请求后,根据提供的信息找到所有切片,并将它们合并成原始文件。合并完成后,可以进行后续的处理(如保存到数据库、通知用户等)。
-
const express = require('express'); const fs = require('fs'); const path = require('path'); const app = express(); const PORT = 3000; // 假设我们使用文件名和索引来保存切片 // 例如: uploaded_file_0.part, uploaded_file_1.part, ... // 模拟的切片保存位置 const uploadDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } // 接收切片的路由(这里只是模拟,实际中应该更复杂) app.post('/upload', (req, res) => { const file = req.files.file; // 假设你使用了中间件来处理multipart/form-data const fileKey = req.body.fileKey; const index = req.body.index; const filePath = path.join(uploadDir, `${fileKey}_${index}.part`); file.mv(filePath, (err) => { if (err) { return res.status(500).send('Failed to save the chunk'); } res.send('Chunk uploaded su***essfully'); }); }); // 合并切片的路由 app.post('/merge', async (req, res) => { const fileKey = req.body.fileKey; const totalChunks = req.body.totalChunks; // 总切片数,应该由前端或数据库提供 let outputPath = path.join(uploadDir, fileKey); let writeStream = fs.createWriteStream(outputPath); for (let i = 0; i < totalChunks; i++) { const chunkPath = path.join(uploadDir, `${fileKey}_${i}.part`); const readStream = fs.createReadStream(chunkPath); readStream.on('error', (err) => { console.error('Error reading chunk:', err); res.status(500).send('Failed to merge chunks'); writeStream.close(); }); readStream.pipe(writeStream, { end: false }); // 注意:不要在这里结束写入流 readStream.on('end', () => { // 当当前切片读取完毕时,不做任何操作 // 所有切片都读取完毕后,在外部逻辑中结束写入流 }); } // 等待所有切片都被读取并写入 // 这里为了简化,我们假设所有切片都立即可用 // 在实际应用中,你可能需要使用Promise.all或async/await来等待所有读取流结束 // 但由于Node.js的流是异步的,直接等待所有流结束可能比较复杂 // 一种简单的方法是监听'finish'事件,但在这里我们假设直接调用 // 模拟所有切片都已写入 writeStream.end(() => { res.send('File merged su***essfully'); }); // 注意:上面的代码存在逻辑问题,因为writeStream.end()会被立即调用 // 而不会等待所有readStream都完成。这里只是为了说明思路。 // 在实际中,你可能需要更复杂的状态管理或使用其他库来处理流。 // 更好的做法是使用流队列或类似的机制来确保切片按顺序写入 }); app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });
5. 进度监控与错误处理
- 上传进度监控:在上传过程中,可以监控每个切片的上传进度,并将这些信息展示给用户。这有助于提高用户体验,让用户了解上传的实时状态。
- 错误处理:对于上传过程中出现的错误(如网络错误、文件损坏等),需要进行适当的处理。可以显示错误消息给用户,并允许用户重新尝试上传或选择其他文件。