如何获取文件对象:File 与 FileList
一切操作都起始于用户选择文件。在 HTML 中,我们通过 <input type="file"> 元素来触发文件对话框。该元素拥有一个 files 属性,它是一个 FileList 对象,包含用户所选的全部文件。即使只选择单个文件,依旧返回 FileList,开发者需要通过索引 files[0] 访问具体的 File 实例。File 对象本身继承自 Blob,因此它除了携带文件名、大小、类型等元信息外,还能被直接传递给其它 API 进行读取或上传。
如果你打算支持多文件选择,只需给 input 添加 multiple 属性,此时 FileList 的长度可能大于 1。在实际项目中,我们经常监听 input 的 change 事件来获取文件列表。例如:
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (event) => {
const files = event.target.files;
for (let i = 0; i < files.length; i++) {
console.log('文件名:', files[i].name, '大小:', files[i].size);
}
});
需要注意的是,出于安全限制,浏览器并不会暴露文件的本地完整路径。File 对象的 name 属性仅包含文件名,不含目录信息。此外,通过拖拽操作(drop 事件)同样能获取到 FileList,原理与 input 相同,这为复杂的交互提供了基础。
前端预览的两条技术路线
获取到 File 对象后,最常用的需求是在上传前让用户看到文件内容,即“本地预览”。对于图片、视频、文本等不同类型,我们有两种主流实现方式:URL.createObjectURL 与 FileReader。
方法一:URL.createObjectURL 可以将一个 Blob 或 File 对象转换为一段带有唯一标识的 blob: 开头的 URL。将这个 URL 赋给 <img> 的 src 或 <video> 的 src,浏览器便能直接渲染文件内容。优点在于同步获取且读取速度极快,因为并没有真正将文件内容载入 JavaScript 内存,而是由浏览器在渲染层直接处理。图片预览代码非常简洁:
const img = document.getElementById('preview');
const file = files[0];
img.src = URL.createObjectURL(file);
但这种方法存在一个关键问题——若不手动释放,生成的 blob URL 会一直占用内存,直到文档卸载。因此,在预览结束后(比如组件销毁或图片加载完成)应调用 URL.revokeObjectURL(url) 进行回收。一个常见的实践是监听图片的 load 事件,在加载成功后立刻吊销 URL,因为此时该 URL 的使命已完成,页面仍然能显示图片。
方法二:FileReader 则是将文件内容异步读取为不同格式的数据,如 base64 编码的 Data URL、文本或 ArrayBuffer。对于文本文件预览(如 .txt, .json, .csv),我们可以使用 readAsText 方法,将结果直接显示在 <pre> 或 <div> 中。而图片预览也可以用 readAsDataURL 得到 base64 字符串,再赋值给 src。以下是文本预览的典型实现:
const reader = new FileReader();
reader.onload = (e) => {
document.getElementById('textPreview').textContent = e.target.result;
};
reader.readAsText(file);
两种方案各有利弊。createObjectURL 性能更优,适合大文件或多媒体预览;FileReader 则更灵活,可以将文件内容转为 base64 直接内联在 img 标签中,便于保存到 localStorage 或上传时嵌入 JSON。但 base64 会增加约 33% 的体积,且对于超大文件会造成 JavaScript 内存压力。实际开发中可根据场景选择,甚至混合使用。
基于 FormData 的文件上传实现
前端预览只是过渡步骤,最终仍需将文件上传至服务器。现代浏览器中,最便捷的方式是使用 FormData 对象构建 multipart/form-data 请求体。FormData 能自动处理文件流和普通字段,并且能直接在 fetch 或 XMLHttpRequest 中作为 body 发送。一个基本的 fetch 上传代码如下:
const formData = new FormData();
formData.append('file', file);
// 可附带额外字段
formData.append('userId', '12345');
fetch('/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => console.log('上传成功', data))
.catch(err => console.error('上传失败', err));
对于进度监控的需求,传统方案多采用 XMLHttpRequest,因其提供了 progress 事件,可以实时计算上传百分比。XMLHttpRequest 的用法虽然较老,但在上传领域依然广泛存在。我们需要监听 xhr.upload.onprogress,通过已上传字节数与总字节数计算进度:
const xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
console.log('上传进度:', percent + '%');
}
};
xhr.send(formData);
在此过程中,一个容易忽视的细节是:如果使用 URL.createObjectURL 生成的 blob URL,在表单提交之后应该及时调用 URL.revokeObjectURL 释放资源,避免内存泄漏。同时,对于超大文件,单次直接上传可能在网络波动时失败,因此业界常采用分片上传策略——利用 Blob 的 slice 方法将文件切分成多个小块,然后并发或串行上传,最后在后端合并。每个分片本质上仍是 Blob 对象,可以直接 append 到 FormData 中,只是需要额外传递分片序号、总片数等元信息。前端可以通过调整并发数来平衡上传速度和浏览器连接数限制,并在用户暂停或网络异常时实现断点续传。
另外,为了提升用户体验,上传前最好进行文件类型和大小的前端校验。虽然不可替代后端校验,但能提前拦截明显不符合要求的文件,减少无效请求。例如结合 File 对象的 type 属性和文件魔数判断真实类型,通过 size 限制最大体积。
常见错误与兼容性处理
在真实项目中,完善的文件处理模块还需考虑诸多边界情况。例如,在 Safari 或旧版 Edge 中,FileList 对象被设计为不可直接遍历的类数组,虽然可以使用 for 循环索引访问,但无法直接调用 forEach。此时需要用 Array.prototype.slice.call(files) 或 Array.from(files) 转换为数组。另外,部分移动端浏览器对 createObjectURL 支持的格式可能存在限制,尤其是视频编码格式,所以利用 video.canPlayType 进行兼容检测也是好习惯。
当用户重复选择同一个文件时,input 的 change 事件可能不会触发,因为文件路径没有发生变化。解决方案是每次选择后手动清空 input 的 value:event.target.value = '',这样可以确保下次选择相同文件时依然会触发事件。此外,如果使用 FormData 上传的同时还想在请求中发送自定义头,需注意 fetch 与 FormData 结合时,不可手动设置 Content-Type 为 multipart/form-data,否则会丢失浏览器自动生成的 boundary 参数,导致后端无法正确解析。fetch 会自动处理。
对于安全性,虽然 HTML5 文件 API 屏蔽了本地路径,但在将文件内容用于预览或读取为 Data URL 后,如果注入到页面 DOM 中(如将文件内容直接设为 innerHTML),可能会引发 XSS 攻击。因此展示文本类文件内容时,务必使用 textContent 而非 innerHTML,或者对内容进行合适的转义处理。整个文件处理流程需要同时兼顾功能实现、性能优化和安全考量,才能打造出生产级可靠的前端上传组件。
HTML5文件API前端预览文件上传修改时间:2026-08-12 13:42:54