前端文件上传处理:别再让用户等待了!

前端文件上传处理:别再让用户等待了!

毒舌时刻

文件上传?听起来就像是前端工程师为了显得自己很专业而特意搞的一套复杂流程。你以为随便加个input[type=file]就能实现文件上传?别做梦了!到时候你会发现,大文件上传会导致页面崩溃,用户体验极差。

你以为FormData就能解决所有问题?别天真了!FormData在处理大文件时会导致内存溢出,而且无法显示上传进度。还有那些所谓的文件上传库,看起来高大上,用起来却各种问题。

为什么你需要这个

  1. 用户体验:良好的文件上传处理可以提高用户体验,减少用户等待时间。
  2. 性能优化:合理的文件上传策略可以减少服务器负担,提高上传速度。
  3. 错误处理:完善的错误处理可以避免上传失败时的用户困惑。
  4. 安全保障:安全的文件上传处理可以防止恶意文件上传,保障系统安全。
  5. 功能丰富:支持多文件上传、拖拽上传、进度显示等功能,满足不同场景的需求。

反面教材

// 1. 简单文件上传 <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.log(data)) .catch(error => console.error(error)); } // 2. 忽略文件大小限制 function uploadFile() { const fileInput = document.getElementById('fileInput'); const file = fileInput.files[0]; if (file.size > 10 * 1024 * 1024) { // 10MB alert('File too large'); return; } // 上传逻辑 } // 3. 忽略文件类型限制 function uploadFile() { const fileInput = document.getElementById('fileInput'); const file = fileInput.files[0]; const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']; if (!allowedTypes.includes(file.type)) { alert('Invalid file type'); return; } // 上传逻辑 } // 4. 缺少进度显示 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.log(data)) .catch(error => console.error(error)); } // 5. 忽略错误处理 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.log(data)); } 

问题

  • 简单文件上传,无法处理大文件
  • 忽略文件大小限制,导致服务器负担过重
  • 忽略文件类型限制,可能上传恶意文件
  • 缺少进度显示,用户体验差
  • 忽略错误处理,上传失败时用户不知道原因

正确的做法

基本文件上传

// 1. 单文件上传 function uploadFile() { const fileInput = document.getElementById('fileInput'); const file = fileInput.files[0]; // 验证文件大小 if (file.size > 10 * 1024 * 1024) { // 10MB 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); }); } // 2. 多文件上传 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) { // 10MB 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); }); } // 使用XMLHttpRequest实现进度显示 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) { // 10MB 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; // 1MB 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; } } // 通知服务器合并 chunks 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); } 

最佳实践

// 1. 使用FileReader预览图片 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); } // 2. 压缩图片 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); }); } // 3. 安全验证 function validateFile(file) { // 验证文件大小 if (file.size > 10 * 1024 * 1024) { // 10MB 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' }; } // 4. 上传状态管理 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可能更加合适。

最后,记住一句话:文件上传的目的是为了方便用户上传文件,而不是为了炫技。如果你的文件上传实现导致用户体验变得更差,那你就失败了。

Read more

GPT-OSS-20B多用户并发:WEBUI压力测试案例

GPT-OSS-20B多用户并发:WEBUI压力测试案例 1. 引言:为什么我们需要关注多用户并发下的AI推理表现? 你有没有遇到过这种情况:团队里好几个人同时用同一个大模型做内容生成,结果页面卡住、响应变慢,甚至直接报错?这其实不是网络问题,而是推理服务扛不住并发请求。 今天我们来聊一个非常实用的场景——基于 GPT-OSS-20B 模型的 WebUI 多用户并发压力测试。这个模型是 OpenAI 最新开源项目的一部分(注:仅为模拟设定),专为高效推理优化,配合 vLLM 加速框架,在双卡 4090D 上实现了接近生产级的响应能力。 本文将带你从零开始部署镜像,并通过真实压力测试数据告诉你:这套组合在 5 人、10 人同时提问时,到底能不能稳住?延迟多少?吞吐量如何?适合哪些实际应用场景? 无论你是想搭建团队内部的知识助手、客服机器人,还是用于内容批量生成平台,这篇文章都能给你提供可落地的参考依据。 2. 环境准备与快速部署 2.1 硬件要求说明

AI时代营销变革核心:GEO战略全景解析与落地指南

AI时代营销变革核心:GEO战略全景解析与落地指南

一、GEO解读:AI时代的营销新路径 生成式AI浪潮下,GEO引领营销新变革      以大型语言模型(LLMs)为核心的生成式AI(如ChatGPT、Gemini、豆包等)加速迭代,推动信息获取与决策范式变革。其快速渗透信息检索、决策辅助等领域,重塑用户交互逻辑——传统“搜索引擎链接筛选”模式渐退,“AI对话式获答”成主流。传统SEO难以适配这一变革,企业仍需全新优化路径。      在此背景下,GEO(生成引擎优化)应运而生。它并非SEO的简单延伸,而是聚焦“语义深度、数据支持、权威来源”与“AI交互逻辑”的全新优化体系,为企业抢占AI信息生态先机提供核心方案。 GEO:超越传统SEO的全新优化哲学      作为新兴战略优化领域,GEO的核心目标是:优化企业/品牌内容及信号,使其易被生成式AI理解、采信,并在生成答案时优先正面呈现。相较于传统SEO聚焦排名的逻辑,GEO更侧重与AI的语义适配和信任构建,是适配AI生态的全新优化理念。 白皮书核心观点:解码GEO的战略价值与实践逻辑

一个 skill ,增加大模型前端的审美能力

上周,我让 AI 帮我做个落地页。 十分钟过去了,生成出来的东西—— 白色背景,紫色渐变,Inter 字体。 我直接关了。 你也遇到过吧? 用 AI 生前端,出来的东西都长一个样。 背景非白即黑,标题栏永远是紫色渐变,字体不是 Inter 就是 Roboto,配色永远是那套蓝绿红黄。 不是说不能用,但—— 太像 AI 了。 一眼看过去就是"机器生成",没有灵魂,没有个性。 直到昨天,我发现了一个东西。 Anthropic 官方出的一个 skill,叫 frontend-design。 让我再试一次。 这次不一样了 同样的提示词,同样的模型。 我只加了一句话: “使用 frontend-design skill” 结果呢?

前端异常捕获与统一格式化:从 console.log(error) 到服务端上报

前端异常捕获与统一格式化:从 console.log(error) 到服务端上报

🧑 博主简介:ZEEKLOG博客专家,「历代文学网」(公益文学网,PC端可以访问:https://lidaiwenxue.com/#/?__c=1000,移动端可关注公众号 “ 心海云图 ” 微信小程序搜索“历代文学”)总架构师,首席架构师,也是联合创始人!16年工作经验,精通Java编程,高并发设计,分布式系统架构设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。 🤝商务合作:请搜索或扫码关注微信公众号 “ 心海云图 ” 前端异常捕获与统一格式化:从 console.log(error) 到服务端上报 引言 在前端开发中,异常监控是保证应用稳定性的重要一环。当用户遇到页面白屏、功能不可用等问题时,如果能及时收集到详细的错误信息(包括堆栈、