跳到主要内容前端文件上传处理:优化体验与性能 | 极客日志JavaScript大前端
前端文件上传处理:优化体验与性能
探讨前端文件上传的常见问题与解决方案,涵盖基础验证、进度显示、拖拽上传及大文件分块上传。通过对比错误示例与正确实现,强调用户体验、性能优化、错误处理及安全验证的重要性。提供 FormData、XMLHttpRequest 及 fetch API 的使用示例,包含图片预览、压缩及管理类实现。
花里胡哨4 浏览 前端文件上传处理:优化体验与性能
常见误区
文件上传?听起来就像是前端工程师为了显得自己很专业而特意搞的一套复杂流程。你以为随便加个 input[type=file] 就能实现文件上传?别做梦了!到时候你会发现,大文件上传会导致页面崩溃,用户体验极差。
你以为 FormData 就能解决所有问题?别天真了!FormData 在处理大文件时会导致内存溢出,而且无法显示上传进度。还有那些所谓的文件上传库,看起来高大上,用起来却各种问题。
为什么你需要这个
- 用户体验:良好的文件上传处理可以提高用户体验,减少用户等待时间。
- 性能优化:合理的文件上传策略可以减少服务器负担,提高上传速度。
- 错误处理:完善的错误处理可以避免上传失败时的用户困惑。
- 安全保障:安全的文件上传处理可以防止恶意文件上传,保障系统安全。
- 功能丰富:支持多文件上传、拖拽上传、进度显示等功能,满足不同场景的需求。
反面教材
<input type="file">
<button onclick="uploadFile()">Upload</button>
function uploadFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const formData = new FormData();
formData.append('file', file);
fetch('/api/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => console.(data))
.( .(error));
}
() {
fileInput = .();
file = fileInput.[];
(file. > * * ) {
();
;
}
}
() {
fileInput = .();
file = fileInput.[];
allowedTypes = [, , ];
(!allowedTypes.(file.)) {
();
;
}
}
() {
fileInput = .();
file = fileInput.[];
formData = ();
formData.(, file);
(, {
: ,
: formData
})
.( response.())
.( .(data))
.( .(error));
}
() {
fileInput = .();
file = fileInput.[];
formData = ();
formData.(, file);
(, {
: ,
: formData
})
.( response.())
.( .(data));
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
log
catch
error =>
console
error
function
uploadFile
const
document
getElementById
'fileInput'
const
files
0
if
size
10
1024
1024
alert
'File too large'
return
function
uploadFile
const
document
getElementById
'fileInput'
const
files
0
const
'image/jpeg'
'image/png'
'image/gif'
if
includes
type
alert
'Invalid file type'
return
function
uploadFile
const
document
getElementById
'fileInput'
const
files
0
const
new
FormData
append
'file'
fetch
'/api/upload'
method
'POST'
body
then
response =>
json
then
data =>
console
log
catch
error =>
console
error
function
uploadFile
const
document
getElementById
'fileInput'
const
files
0
const
new
FormData
append
'file'
fetch
'/api/upload'
method
'POST'
body
then
response =>
json
then
data =>
console
log
- 简单文件上传,无法处理大文件
- 忽略文件大小限制,导致服务器负担过重
- 忽略文件类型限制,可能上传恶意文件
- 缺少进度显示,用户体验差
- 忽略错误处理,上传失败时用户不知道原因
正确的做法
基本文件上传
function uploadFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (file.size > 10 * 1024 * 1024) {
alert('File too large');
return;
}
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(file.type)) {
alert('Invalid file type');
return;
}
const formData = new FormData();
formData.append('file', file);
fetch('/api/upload', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error('Upload failed');
}
return response.json();
})
.then(data => {
console.log('Upload successful:', data);
alert('File uploaded successfully');
})
.catch(error => {
console.error('Upload error:', error);
alert('Upload failed: ' + error.message);
});
}
function uploadFiles() {
const fileInput = document.getElementById('fileInput');
const files = fileInput.files;
if (files.length === 0) {
alert('Please select files');
return;
}
for (const file of files) {
if (file.size > 10 * 1024 * 1024) {
alert(`File ${file.name} is too large`);
return;
}
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(file.type)) {
alert(`File ${file.name} has invalid type`);
return;
}
}
const formData = new FormData();
for (const file of files) {
formData.append('files', file);
}
fetch('/api/upload-multiple', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error('Upload failed');
}
return response.json();
})
.then(data => {
console.log('Upload successful:', data);
alert('Files uploaded successfully');
})
.catch(error => {
console.error('Upload error:', error);
alert('Upload failed: ' + error.message);
});
}
带进度显示的文件上传
function uploadFileWithProgress() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const progressBar = document.getElementById('progressBar');
if (!file) {
alert('Please select a file');
return;
}
const formData = new FormData();
formData.append('file', file);
fetch('/api/upload', {
method: 'POST',
body: formData,
onUploadProgress: function(progressEvent) {
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
progressBar.style.width = percentCompleted + '%';
progressBar.textContent = percentCompleted + '%';
}
})
.then(response => {
if (!response.ok) {
throw new Error('Upload failed');
}
return response.json();
})
.then(data => {
console.log('Upload successful:', data);
alert('File uploaded successfully');
})
.catch(error => {
console.error('Upload error:', error);
alert('Upload failed: ' + error.message);
});
}
function uploadFileWithProgressXHR() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const progressBar = document.getElementById('progressBar');
if (!file) {
alert('Please select a file');
return;
}
const formData = new FormData();
formData.append('file', file);
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function(event) {
if (event.lengthComputable) {
const percentCompleted = Math.round((event.loaded * 100) / event.total);
progressBar.style.width = percentCompleted + '%';
progressBar.textContent = percentCompleted + '%';
}
});
xhr.addEventListener('load', function() {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log('Upload successful:', data);
alert('File uploaded successfully');
} else {
console.error('Upload error:', xhr.statusText);
alert('Upload failed: ' + xhr.statusText);
}
});
xhr.addEventListener('error', function() {
console.error('Upload error');
alert('Upload failed');
});
xhr.open('POST', '/api/upload');
xhr.send(formData);
}
拖拽上传
function setupDragAndDrop() {
const dropArea = document.getElementById('dropArea');
dropArea.addEventListener('dragover', function(event) {
event.preventDefault();
dropArea.classList.add('drag-over');
});
dropArea.addEventListener('dragleave', function() {
dropArea.classList.remove('drag-over');
});
dropArea.addEventListener('drop', function(event) {
event.preventDefault();
dropArea.classList.remove('drag-over');
const files = event.dataTransfer.files;
if (files.length > 0) {
uploadFiles(files);
}
});
dropArea.addEventListener('click', function() {
document.getElementById('fileInput').click();
});
document.getElementById('fileInput').addEventListener('change', function() {
const files = this.files;
if (files.length > 0) {
uploadFiles(files);
}
});
}
function uploadFiles(files) {
for (const file of files) {
if (file.size > 10 * 1024 * 1024) {
alert(`File ${file.name} is too large`);
return;
}
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(file.type)) {
alert(`File ${file.name} has invalid type`);
return;
}
}
const formData = new FormData();
for (const file of files) {
formData.append('files', file);
}
}
大文件分块上传
async function uploadLargeFile(file) {
const chunkSize = 1024 * 1024;
const totalChunks = Math.ceil(file.size / chunkSize);
const fileId = generateFileId();
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('fileId', fileId);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
formData.append('fileName', file.name);
try {
const response = await fetch('/api/upload-chunk', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Upload failed');
}
const data = await response.json();
console.log(`Chunk ${i + 1}/${totalChunks} uploaded:`, data);
} catch (error) {
console.error('Upload error:', error);
throw error;
}
}
const response = await fetch('/api/merge-chunks', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ fileId, fileName: file.name, totalChunks })
});
if (!response.ok) {
throw new Error('Merge failed');
}
const data = await response.json();
console.log('File uploaded successfully:', data);
return data;
}
function generateFileId() {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}
最佳实践
function previewImage(file) {
const reader = new FileReader();
reader.onload = function(e) {
const img = document.createElement('img');
img.src = e.target.result;
img.style.maxWidth = '200px';
document.getElementById('preview').appendChild(img);
};
reader.readAsDataURL(file);
}
function compressImage(file, maxWidth = 800, maxHeight = 800, quality = 0.8) {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = function() {
let width = img.width;
let height = img.height;
if (width > height) {
if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}
} else {
if (height > maxHeight) {
width = (width * maxHeight) / height;
height = maxHeight;
}
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(function(blob) {
resolve(blob);
}, file.type, quality);
};
img.src = URL.createObjectURL(file);
});
}
function validateFile(file) {
if (file.size > 10 * 1024 * 1024) {
return { valid: false, message: 'File too large' };
}
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
if (!allowedTypes.includes(file.type)) {
return { valid: false, message: 'Invalid file type' };
}
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.pdf', '.doc', '.docx'];
const extension = file.name.substring(file.name.lastIndexOf('.'));
if (!allowedExtensions.includes(extension.toLowerCase())) {
return { valid: false, message: 'Invalid file extension' };
}
return { valid: true, message: 'File is valid' };
}
class UploadManager {
constructor() {
this.uploads = new Map();
}
async upload(file) {
const id = generateFileId();
const upload = { id, file, status: 'pending', progress: 0, error: null };
this.uploads.set(id, upload);
try {
upload.status = 'uploading';
upload.status = 'completed';
upload.progress = 100;
} catch (error) {
upload.status = 'failed';
upload.error = error.message;
}
return upload;
}
getUpload(id) {
return this.uploads.get(id);
}
getAllUploads() {
return Array.from(this.uploads.values());
}
cancelUpload(id) {
const upload = this.uploads.get(id);
if (upload) {
upload.status = 'cancelled';
}
}
}
const uploadManager = new UploadManager();
const file = document.getElementById('fileInput').files[0];
uploadManager.upload(file).then(upload => {
console.log('Upload result:', upload);
});
总结与建议
文件上传确实很重要,但我见过太多开发者滥用这个特性,导致应用变得过于复杂。
想象一下,当你为了实现大文件上传,使用了分块上传技术,结果导致代码变得非常复杂,这真的值得吗?
还有那些过度使用文件上传库的开发者,为了使用某个库,而忽略了项目的实际需求,结果导致代码变得过于复杂。
所以,在实现文件上传时,一定要根据实际需求来决定。不要为了实现所有功能而实现,要选择最适合的方案。
当然,对于需要上传大文件的应用来说,分块上传是必要的。但对于普通的文件上传需求,使用简单的 FormData 可能更加合适。
最后,记住一句话:文件上传的目的是为了方便用户上传文件,而不是为了炫技。如果你的文件上传实现导致用户体验变得更差,那你就失败了。