6.5 文件上传交互
文件上传是 Web 表单交互的核心场景之一,覆盖本地文件读取、预览、提交、传输的完整链路。
从基础的表单上传到拖拽交互,再到大文件分片传输,形成了覆盖不同文件体积、不同体验需求的完整上传体系,是附件提交、资源上传、文件分享等业务的核心技术支撑。
6.5.1 文件读取与预览
文件读取与预览是上传前的核心交互环节,基于浏览器原生 File API 与 FileReader 接口实现,能够在本地完成文件信息读取、内容预览与格式校验,无需上传到服务端,大幅提升交互效率与体验。
一、File 文件对象
用户通过文件控件选择文件后,input.files返回FileList对象,每个元素为一个File文件对象,继承自Blob二进制对象,包含文件的完整元信息。
属性 | 类型 | 含义说明 |
name | string | 文件名称,包含后缀扩展名 |
size | number | 文件大小,单位为字节(Byte) |
type | string | 文件的 MIME 类型,如image/jpeg、application/pdf |
lastModified | number | 文件最后修改时间的时间戳 |
二、FileReader 读取接口
FileReader是专门用于读取本地文件内容的异步接口,支持多种读取格式,读取过程通过事件回调通知状态与结果。
核心方法 | 作用说明 | 适用场景 |
readAsDataURL(file) | 将文件读取为 Base64 编码的 DataURL 字符串 | 图片、音视频等媒体文件本地预览 |
readAsText(file, encoding) | 将文件读取为文本字符串,可指定编码 | 文本文件、代码文件内容预览 |
readAsArrayBuffer(file) | 将文件读取为二进制 ArrayBuffer 对象 | 文件分片计算、二进制数据处理 |
三、核心事件
- onload:读取成功时触发,结果存储在reader.result中
- onerror:读取失败时触发
- onprogress:读取过程中周期性触发,可用于展示读取进度
四、图片预览实现示例
const fileInput = document.querySelector('#avatarInput'); const previewImg = document.querySelector('#previewImg'); fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; // 格式前置校验 if (!file.type.startsWith('image/')) { alert('请选择图片文件'); return; } const reader = new FileReader(); reader.onload = () => { // 读取完成,赋值给预览图 previewImg.src = reader.result; }; reader.onerror = () => { alert('文件读取失败'); }; // 启动读取 reader.readAsDataURL(file); });五、注意事项
- 所有读取操作均为异步,需通过事件回调获取结果,不可同步读取。
- 大文件读取会占用大量浏览器内存,超大文件不建议全量本地读取。
- 仅拥有文件读取权限,无法修改本地文件系统中的文件内容。
6.5.2 基础表单上传
基础表单上传是文件上传的原生实现,分为传统表单同步上传与 FormData 异步上传两种模式,前者是浏览器原生行为,后者是现代应用的主流方案。
一、传统表单同步上传
通过表单原生提交能力实现,是最原始的上传方式。
<form action="/api/upload" method="post" enctype="multipart/form-data"> <input type="file" name="avatar" accept="image/*"> <button type="submit">上传</button> </form>- 核心配置:enctype="multipart/form-data"是文件上传的必要配置,浏览器会将表单数据编码为多部分格式,支持二进制文件传输。
- 特点:实现简单、全浏览器兼容;但提交后页面会刷新,体验较差,无法获取上传进度。
二、FormData 异步无刷新上传
通过 FormData 封装文件与表单数据,配合 XMLHttpRequest 实现异步上传,页面无刷新,支持进度监听,是当前工业界的主流方案。
const form = document.querySelector('#uploadForm'); form.addEventListener('submit', (e) => { e.preventDefault(); const formData = new FormData(form); const xhr = new XMLHttpRequest(); // 监听上传进度 xhr.upload.onprogress = (e) => { if (e.lengthComputable) { const percent = Math.round(e.loaded / e.total * 100); console.log(`上传进度:${percent}%`); } }; xhr.onload = () => { if (xhr.status === 200) { console.log('上传成功'); } }; xhr.open('POST', '/api/upload'); xhr.send(formData); });注意:提交 FormData 时禁止手动设置
Content-Type请求头,浏览器会自动生成带边界标识的multipart/form-data类型,手动设置会导致服务端无法解析。
三、文件控件核心属性
属性 | 作用说明 |
multiple | 支持同时选择多个文件 |
accept | 限制可选择的文件类型,如image/*、.pdf、video/* |
capture | 移动端直接调用系统相机,取值user前置摄像头、environment后置摄像头 |
6.5.3 拖拽上传
拖拽上传基于 HTML5 拖放 API 实现,用户直接将本地文件拖拽到指定区域即可完成选择与上传,交互更直观高效,是桌面端上传的标准增强交互。
一、核心原理
利用拖放事件的dataTransfer.files属性获取拖拽的本地文件列表,后续的读取、校验、上传逻辑与表单上传完全一致,仅交互方式不同。
二、完整实现
<div class="drop-area" id="dropArea"> <p>拖拽文件到此处上传</p> </div> const dropArea = document.querySelector('#dropArea'); // 拖拽进入区域 dropArea.addEventListener('dragenter', (e) => { e.preventDefault(); dropArea.classList.add('drag-over'); }); // 拖拽悬停区域,必须阻止默认行为才能触发drop dropArea.addEventListener('dragover', (e) => { e.preventDefault(); dropArea.classList.add('drag-over'); }); // 拖拽离开区域 dropArea.addEventListener('dragleave', () => { dropArea.classList.remove('drag-over'); }); // 放置文件 dropArea.addEventListener('drop', (e) => { e.preventDefault(); dropArea.classList.remove('drag-over'); // 获取拖拽的文件列表 const files = e.dataTransfer.files; // 执行上传逻辑 uploadFiles(files); });三、注意事项
- dragover与drop事件必须调用preventDefault(),否则浏览器会执行默认行为(直接打开文件)。
- 支持多文件同时拖拽,处理逻辑与表单多选文件完全复用。
- 可扩展拖拽进入动效、文件数量校验、格式校验、缩略图展示等交互。
6.5.4 大文件分片上传与断点续传
对于百 MB 级以上的大文件,一次性上传存在请求超时、失败重传成本高、网络波动易中断等问题。分片上传将大文件切割为多个小分片逐个上传,断点续传在此基础上记录上传进度,失败后无需从头开始,大幅提升大文件上传的稳定性与体验。
一、分片上传核心原理
- 文件切片:利用Blob.slice()方法,将大文件按指定大小(通常 2~5MB)切割为多个连续的二进制分片。
- 逐个上传:每个分片作为独立请求上传,携带文件唯一标识、分片序号、总分片数等信息。
- 服务端合并:所有分片全部上传完成后,前端通知服务端,服务端按分片序号合并为完整文件。
二、基础分片实现
/** * 文件切片函数 * @param {File} file 原始文件 * @param {number} chunkSize 分片大小,单位字节 * @returns {Blob[]} 分片数组 */ function sliceFile(file, chunkSize = 2 * 1024 * 1024) { const chunks = []; let start = 0; while (start < file.size) { chunks.push(file.slice(start, start + chunkSize)); start += chunkSize; } return chunks; } // 分片上传流程 async function uploadBigFile(file) { const chunks = sliceFile(file); const fileId = generateFileId(file); // 生成文件唯一标识 // 逐个上传分片 for (let i = 0; i < chunks.length; i++) { const formData = new FormData(); formData.append('fileId', fileId); formData.append('chunkIndex', i); formData.append('totalChunks', chunks.length); formData.append('chunk', chunks[i]); await fetch('/api/upload/chunk', { method: 'POST', body: formData }); } // 所有分片上传完成,通知合并 await fetch('/api/upload/merge', { method: 'POST', body: JSON.stringify({ fileId, fileName: file.name }) }); }三、断点续传原理
断点续传是分片上传的增强,核心是记录已上传分片状态,重新上传时跳过已成功上传的分片,仅上传缺失的部分。
两种主流进度记录方式:
- 本地存储记录:将上传进度记录在 localStorage,页面刷新后可恢复,适合短时间续传场景。
- 服务端查询:上传前向服务端查询该文件已上传的分片列表,跳过已上传部分,适合跨设备、长时间续传,准确性更高。
四、核心优化点
- 并发控制:同时上传 2~3 个分片,提升上传速度,避免请求过多阻塞网络。
- 失败重试:单个分片上传失败自动重试 2~3 次,超过次数再判定整体失败。
- 进度精准计算:按已上传分片大小总和 / 文件总大小计算整体进度,而非按分片数量。
- 文件唯一标识:通过文件内容生成 MD5 作为唯一 ID,避免同名文件冲突,是断点续传的基础。
五、适用边界
- 适用场景:百 MB 以上大文件、视频、压缩包等上传;网络不稳定的弱网场景。
- 不适用场景:小文件上传无需分片,会增加开发复杂度与服务端合并开销。