前端上传下载文件处理
文件的上传和下载是前端开发中常见的操作,本文将介绍如何实现文件的上传和下载,并提供一些实用的技巧和注意事项。包括小文件上传、大文件上传、断点续传、文件下载等功能。请求使用fetch作为示例,fetch是浏览器内置的API,可用于发送HTTP请求且支持formData格式,不需要设置Content-Type,也可使用axios等库。
基础文件上传实现
基础且最常用的文件上传实现,通过input[type=file]获取文件,然后使用FormData进行上传。单个文件大小限制为5MB,支持多文件上传。使用时,需要在input标签上添加accept属性,指定支持的文件类型。使用fileFormData函数对文件进行处理,包括验证文件大小和构建FormData对象,返回的FormData对象可以直接用于上传请求,可使用fetch或axios等库进行上传。
<input type="file" id="uploader" accept=".pdf,.docx,.png" multiple />
// 获取文件
const uploader = document.getElementById("uploader");
// 触发上传
uploader.addEventListener('change', async (e) => {
try {
const files = Array.from(e.target.files);
if (files.length === 0) {
throw new Error("请选择文件");
}
// 处理文件
const formData = fileFormData(files);
// 上传逻辑...
fetch('/api/upload', {
method: 'POST',
body: formData
});
} catch (error) {
// 错误处理
console.error('上传出错:', error);
alert(`上传失败: ${error.message}`);
}
});
// 文件处理函数
const fileFormData = (files) => {
// 基础验证
if (files.some((file) => file.size > 5 * 1024 * 1024)) {
throw new Error("单个文件不能超过5MB");
}
// 构建FormData对象
const formData = new FormData();
files.forEach((file) => formData.append("files[]", file));
return formData;
};
大文件上传实现
大文件分片上传
大文件上传通常需要进行分片处理,将大文件分成多个小文件进行上传,然后在服务端进行合并。这种方式可以有效避免大文件上传时的网络阻塞问题,提高上传效率。考虑网络中断,物理硬件出故障等问题需支持断点续传。使用时,需要在input标签上添加accept属性,指定支持的文件类型。使用chunkedUpload函数对文件进行分片处理,包括计算文件hash、获取已上传分片、上传分片、合并请求等操作。使用时,需要传入文件对象和进度回调函数。 使用checkUploadedChunks函数获取已上传分片,使用mergeChunks函数进行分片合并请求。
<input type="file" id="uploader" accept=".pdf,.docx,.png" multiple />
// 大文件分片上传
async function chunkedUpload(file, onProgress) {
const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB分片
const fileHash = await calculateFileHash(file); // 文件唯一标识
// 获取已上传分片(断点续传)
const uploaded = await checkUploadedChunks(fileHash);
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
for (let i = 0; i < totalChunks; i++) {
if (uploaded.includes(i)) {
onProgress?.((i / totalChunks) * 100); // 更新进度
continue; // 跳过已传分片
}
const chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
const formData = new FormData();
// 添加分片元数据
formData.append('file', chunk);
formData.append('chunkNumber', i);
formData.append('totalChunks', totalChunks);
formData.append('identifier', fileHash);
// 分片上传请求
await fetch('/api/upload-chunk', {
method: 'POST',
body: formData
});
onProgress?.(((i + 1) / totalChunks) * 100);
}
// 合并请求
await mergeChunks(file.name, fileHash);
}
计算文件hash是为了保证文件的唯一性,避免重复上传。使用Web Worker进行计算,可以避免阻塞主线程,提高用户体验。需要创建一个hash-worker.js文件,用于计算文件hash。
// 计算文件hash(使用Web Worker)
function calculateFileHash(file) {
return new Promise((resolve) => {
const worker = new Worker('/hash-worker.js');
worker.postMessage(file);
worker.onmessage = (e) => resolve(e.data);
worker.onerror = (e) => {
console.error('Hash计算错误:', e);
resolve('fallback-hash-' + Date.now());
};
});
}
hash-worker.js计算文件hash
// hash-worker.js
self.onmessage = function(e) {
const fileData = e.data;
const reader = new FileReader();
reader.onload = function(event) {
const buffer = event.target.result;
const hashBuffer = crypto.subtle.digest('SHA-256', buffer);
hashBuffer.then(function(hash) {
const hashArray = Array.from(new Uint8Array(hash));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
self.postMessage({hash: hashHex});
}).catch(function(err) {
self.postMessage({error: err});
});
};
reader.readAsArrayBuffer(fileData);
};
断点续传检查,获取已上传分片。
// 断点续传检查
async function checkUploadedChunks(fileHash) {
try {
const response = await fetch(`/api/upload-status?hash=${fileHash}`);
return await response.json();
} catch (error) {
console.error('获取上传状态失败:', error);
return [];
}
}
分片合并请求。
// 分片合并请求
async function mergeChunks(filename, fileHash) {
try {
await fetch('/api/merge', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
filename,
fileHash
})
});
} catch (error) {
console.error('分片合并失败:', error);
throw new Error('文件合并失败,请稍后重试');
}
}
文件下载实现
一、Blob方式下载(适合小文件)
Blob方式下载适合小文件,将文件内容转换为Blob对象,然后创建一个URL对象,将Blob对象作为URL的源,然后创建一个a标签,将URL设置为a标签的href属性,设置下载文件名,然后将a标签添加到文档中,最后触发a标签的click事件,完成下载。
// Blob方式下载(适合小文件)
function downloadByBlob(content, filename) {
try {
const blob = new Blob([content], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
// 清理
setTimeout(() => {
document.body.removeChild(link);
URL.revokeObjectURL(url);
}, 100);
} catch (error) {
console.error('下载失败:', error);
throw new Error('文件下载失败');
}
}
二、直连下载(适合大文件)
直连下载适合大文件,通过创建一个iframe元素,将文件URL设置为iframe的src属性,然后将iframe添加到文档中,完成下载。
// 直连下载(适合大文件)
function downloadDirect(url) {
return new Promise((resolve, reject) => {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
iframe.onload = () => {
setTimeout(() => {
document.body.removeChild(iframe);
resolve();
}, 5000);
};
iframe.onerror = () => {
document.body.removeChild(iframe);
reject(new Error('下载失败'));
};
document.body.appendChild(iframe);
});
}
文件验证
文件类型白名单验证是指在上传文件时,对文件的类型进行验证,只允许上传指定类型的文件。这种方式可以有效防止恶意文件的上传,提高系统的安全性。使用时,需要在input标签上添加accept属性,指定支持的文件类型。使用validateFile函数对文件进行验证,包括文件类型验证和文件头验证。
// 文件类型白名单验证
const ALLOWED_TYPES = {
'image/png': true,
'application/pdf': true
};
const FILE_SIGNATURES = {
'89504e47': 'image/png', // PNG
'25504446': 'application/pdf' // PDF
};
async function validateFile(file) {
// MIME类型验证
if (!ALLOWED_TYPES[file.type]) {
throw new Error('不支持的文件格式');
}
// 文件头验证(防御文件扩展名伪造)
const isValid = await checkFileSignature(file);
if (!isValid) {
throw new Error('文件格式与扩展名不符');
}
return true;
}
async function checkFileSignature(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
const arr = new Uint8Array(e.target.result).subarray(0, 4);
const header = Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
resolve(!!FILE_SIGNATURES[header]);
};
reader.onerror = () => resolve(false);
reader.readAsArrayBuffer(file.slice(0, 4));
});
}