一、上传
1.上传数据的封装
在上传文件时,最常用的方式是使用 FormData
对象,它会自动将请求头中的 Content-Type
请求头指定为multipart/form-data
const formData = new FormData(); formData.append("file", fileInput.files[0]); // fileInput 是 <input type="file"> const xhr = new XMLHttpRequest(); xhr.open("POST", "/upload"); // 不要设置 Content-Type,浏览器会自动生成类似于 multipart/form-data 的头 xhr.send(formData);
2.上传进度条
XMLHttpRequest
对象的 upload
属性有一个 progress
事件,它可以获取文件上传的进度。每当文件上传进度更新时,都会触发该事件,提供已上传字节数和文件总大小的信息
const xhr = new XMLHttpRequest(); xhr.open("POST", "/upload"); // 替换为实际的上传 URL // 监听上传进度事件 xhr.upload.addEventListener("progress", (event) => { if (event.lengthComputable) { const percentComplete = (event.loaded / event.total) * 100; const progressBar = document.getElementById("progress-bar"); progressBar.style.width = percentComplete + "%"; progressBar.textContent = Math.round(percentComplete) + "%"; } }); // 上传完成事件 xhr.onload = () => { if (xhr.status === 200) { alert("上传成功!"); } else { alert("上传失败!"); } }; // 发送表单数据 xhr.send(formData);
3.大文件上传
文件分片上传的关键技术主要涉及文件分片、分片上传、分片管理和服务器端的分片合并。这些技术通过优化传输和可靠性,确保大文件在网络不稳定或需要续传的场景下能够顺利完成上传。以下是文件分片上传的关键技术点:
文件分片(Chunking)
- 技术点:文件分片是将大文件按固定大小(通常为 1 MB 或 5 MB)划分为若干小块(Chunk),这些分片可以单独处理和上传。通过 JavaScript 的
Blob.slice
方法可以轻松实现分片。 - 实现细节:在前端使用
Blob.slice(start, end)
方法获取每个分片,通过循环逐片读取并存储到FormData
中。
分片上传与进度管理
- 技术点:每个分片单独发起上传请求(如使用
XMLHttpRequest
或fetch
),通过监听上传进度事件来实时更新上传进度。同时,可以记录每个分片的状态,便于上传失败时进行重试。 - 并发上传:对于大文件,可以并发上传多个分片(Promise.all),提高上传速度。例如,前端可以控制同时上传 3 个分片,并在其中一个分片完成后再上传下一个分片。
- 并发控制:通过控制并发数,既保证上传速度,又防止过多并发导致服务器压力过大。
分片标识与校验
- 分片标识:每个分片通常使用唯一标识(如文件名、分片序号、总分片数)来标识,可以在请求中带上这些信息,方便服务器识别每个分片的位置。
- 分片校验:为了确保每个分片正确上传,可以对每个分片计算哈希值(如 MD5、SHA-256),服务器上传后进行校验。这样可以在上传失败或分片出错时重试,保证数据完整性。
服务端分片接收与校验
- 接收分片:服务器根据上传请求中的分片标识(如
chunkIndex
和totalChunks
)存储每个分片,通常放在临时文件夹中。 - 校验分片:每个分片上传后可以计算哈希并与客户端传来的哈希比对,确保分片未损坏。校验通过后才认为分片上传成功,失败则需要重新上传该分片。
服务端合并分片
- 合并触发:在服务器端收到所有分片后,可以触发文件合并。通常通过
chunkIndex
和totalChunks
判断是否收到完整文件。 - 合并顺序:合并时需要按照
chunkIndex
顺序将所有分片按序读入并写入完整文件。 - 并发与异步:合并过程可以使用多线程或异步方法,尤其是当文件较大时,可以提升合并效率。
- 清理临时文件:合并完成后需要删除分片的临时文件,释放服务器存储空间。
断点续传支持
- 断点记录:断点续传的关键在于记录已上传的分片。前端可以在本地存储(如
localStorage
)中记录已成功上传的分片序号。 - 续传实现:当上传中断时,用户可以从记录的进度重新开始上传,无需重复上传已完成的分片。这可以减少带宽浪费和上传时间。
7.实现代码
客户端
const CHUNK_SIZE = 1024 * 1024; // 每片1MB async function uploadFile() { const fileInput = document.getElementById("fileInput"); const file = fileInput.files[0]; if (!file) { alert("请选择文件后再上传!"); return; } const totalChunks = Math.ceil(file.size / CHUNK_SIZE); // 计算分片数量 let uploadedChunks = 0; for (let start = 0; start < file.size; start += CHUNK_SIZE) { const end = Math.min(start + CHUNK_SIZE, file.size); const chunk = file.slice(start, end); // 获取文件分片 const chunkIndex = start / CHUNK_SIZE; // 当前分片序号 // 使用 FormData 上传每个分片 const formData = new FormData(); formData.append("file", chunk); // 添加分片数据 formData.append("filename", file.name); // 添加文件名 formData.append("chunkIndex", chunkIndex); // 添加分片序号 formData.append("totalChunks", totalChunks); // 添加总分片数 // 上传当前分片 try {
//等待当前task完成后再进入下一个循环 await uploadChunk(formData); uploadedChunks++; updateProgress(uploadedChunks, totalChunks); // 更新进度条 } catch (error) { console.error(`分片 ${chunkIndex} 上传失败`, error); return; // 上传失败退出 } } alert("文件上传成功!"); } function uploadChunk(formData) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("POST", "/upload-chunk"); // 替换为实际的上传 URL xhr.onload = () => { if (xhr.status === 200) { resolve(); } else { reject(new Error("分片上传失败")); } }; xhr.onerror = () => reject(new Error("网络错误")); xhr.send(formData); }); } // 更新进度条 function updateProgress(uploadedChunks, totalChunks) { const progressBar = document.getElementById("progress-bar"); const percentComplete = (uploadedChunks / totalChunks) * 100; progressBar.textContent = `上传进度: ${Math.round(percentComplete)}%`; }
服务端
在所有分片上传完毕后,服务器端会收到所有分片并合并。服务器可以根据 filename
和 chunkIndex
来识别每个分片,并按顺序合并它们。
const fs = require("fs"); const path = require("path"); const express = require("express"); const app = express(); const uploadDir = "uploads/"; // 保存分片的临时目录 app.post("/upload-chunk", (req, res) => { const { filename, chunkIndex, totalChunks } = req.body; const chunk = req.files.file; // 获取上传的分片文件 // 将每个分片存储到临时文件夹 const chunkPath = path.join(uploadDir, `${filename}.part${chunkIndex}`); fs.writeFileSync(chunkPath, chunk.data); // 检查是否所有分片都上传完成 if (parseInt(chunkIndex) === totalChunks - 1) { // 合并所有分片 const filePath = path.join(uploadDir, filename); const writeStream = fs.createWriteStream(filePath); for (let i = 0; i < totalChunks; i++) { const partPath = path.join(uploadDir, `${filename}.part${i}`); const data = fs.readFileSync(partPath); writeStream.write(data); fs.unlinkSync(partPath); // 删除已合并的分片 } writeStream.end(); res.send("文件上传成功并合并完成!"); } else { res.send("分片上传成功!"); } }); app.listen(3000, () => { console.log("Server started on http://localhost:3000"); });
二、下载
1. 使用 <a>
标签的 download
属性
这是实现文件下载最简单的方式。<a>
标签的 download
属性可以强制浏览器下载文件而不是直接打开文件。
<a href="path/to/file.pdf" download="filename.pdf">下载文件</a>当需要下载的数据是由 JavaScript 动态生成时,可以将数据转换为
Blob
对象,并生成一个 URL 来进行下载。使用 Blob 和 URL.createObjectURL 生成下载链接
function downloadBlob(data, filename, mimeType) { const blob = new Blob([data], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); // 释放 URL } // 使用示例 const data = "Hello, this is a text file!"; downloadBlob(data, "example.txt", "text/plain");
2.使用ajax和 Blob 实现文件下载
不推荐使用该方法。下载速度比方法1要慢,要等ajax接收完文件再调用浏览器的文件保存api,文件较大时点击后可能需要很长时间才有反应,并且下载传输过程中出现问题后,无法断点续传。
async function downloadFile(url, filename) { const response = await fetch(url); const blob = await response.blob(); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); } // 使用示例 downloadFile("path/to/file.pdf", "downloaded_file.pdf");
标签:文件,const,xhr,ajax,file,分片,上传,下载 From: https://www.cnblogs.com/94pm/p/18531157