File API 与文件操作(File API and File Operations)
一、File API 概述
1.1 File API 的组成
File API 是浏览器提供的一套用于处理文件的 API,主要包括:
- File:表示用户选择的文件
- Blob:表示原始二进制数据
- FileReader:异步读取文件内容
- URL.createObjectURL():创建文件 URL
- FormData:构建multipart/form-data 数据
这些 API 使得 Web 应用可以像桌面应用一样处理文件,而不需要服务器参与。
1.2 文件选择
<input type="file" id="fileInput">
<input type="file" id="multipleFiles" multiple>
<input type="file" id="imageFiles" accept="image/*">
<input type="file" id="videoFiles" accept="video/*">
<input type="file" id="audioFiles" accept="audio/*">
<input type="file" id="customTypes" accept=".pdf,.doc,.docx">
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (e) => {
const files = e.target.files;
console.log('Files count:', files.length);
for (const file of files) {
console.log('Name:', file.name);
console.log('Size:', file.size);
console.log('Type:', file.type);
console.log('Last Modified:', new Date(file.lastModified));
}
});
二、File 和 Blob
2.1 File 对象的属性
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
// 核心属性
console.log(file.name); // 文件名
console.log(file.size); // 文件大小(字节)
console.log(file.type); // MIME 类型
console.log(file.lastModified); // 上次修改时间(时间戳)
console.log(file.lastModifiedDate); // 上次修改时间(Date 对象)
console.log(file.webkitRelativePath); // 相对路径(某些浏览器)
});
2.2 Blob 对象
Blob(Binary Large Object)表示原始二进制数据,File 继承自 Blob:
// 创建 Blob
const blob = new Blob(['Hello, world!'], { type: 'text/plain' });
// 从 Blob 获取信息
console.log(blob.size); // 13
console.log(blob.type); // "text/plain"
// 切片 Blob
const partial = blob.slice(0, 5); // 前 5 个字节
// 转换为其他格式
const url = URL.createObjectURL(blob);
// 转换为 text
const textPromise = blob.text();
const text = await textPromise;
// 转换为 ArrayBuffer
const arrayBufferPromise = blob.arrayBuffer();
const arrayBuffer = await arrayBufferPromise;
2.3 File 继承关系
Blob
└── File
├── FileList (非标准)
└── DataTransferItem (拖放 API)
2.4 文件类型检测
// 检测 MIME 类型
function isImage(file) {
return file.type.startsWith('image/');
}
function isPDF(file) {
return file.type === 'application/pdf';
}
function isVideo(file) {
return file.type.startsWith('video/');
}
// 根据扩展名检测
function getExtension(filename) {
return filename.slice((filename.lastIndexOf('.') - 1 >>> 0) + 2).toLowerCase();
}
function isTextFile(file) {
const textTypes = ['text/plain', 'text/html', 'text/css', 'text/javascript'];
return textTypes.includes(file.type) ||
['.txt', '.md', '.json', '.xml', '.csv'].some(ext =>
file.name.toLowerCase().endsWith(ext)
);
}
三、FileReader API
3.1 FileReader 的方法
const reader = new FileReader();
// 读取为文本
reader.readAsText(file);
// 读取为 Data URL(base64)
reader.readAsDataURL(file);
// 读取为 ArrayBuffer
reader.readAsArrayBuffer(file);
// 中止读取
reader.abort();
3.2 FileReader 事件
const reader = new FileReader();
reader.onload = (e) => {
console.log('Load complete');
console.log('Result:', reader.result);
};
reader.onerror = (e) => {
console.error('Error:', reader.error);
};
reader.onprogress = (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
console.log(`Progress: ${percent.toFixed(2)}%`);
}
};
reader.onloadstart = () => console.log('Load started');
reader.onloadend = () => console.log('Load ended');
reader.onabort = () => console.log('Load aborted');
3.3 读取示例
// 读取文本文件
async function readTextFile(file) {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsText(file);
});
}
// 读取图片并显示
async function readImageAsDataURL(file) {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
}
// 使用
const fileInput = document.getElementById('imageInput');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
const dataUrl = await readImageAsDataURL(file);
document.getElementById('preview').src = dataUrl;
}
});
四、URL.createObjectURL
4.1 基本用法
// 创建对象 URL
const url = URL.createObjectURL(file);
// 使用 URL
const img = document.createElement('img');
img.src = url;
document.body.appendChild(img);
// 释放 URL
URL.revokeObjectURL(url);
4.2 与 FileReader 的对比
| 特性 | createObjectURL | FileReader |
|---|---|---|
| 性能 | 更快(不复制数据) | 较慢(复制到内存) |
| 内存 | 由浏览器管理 | JavaScript 堆 |
| 生命周期 | document 存在期间有效 | 可持久使用 |
| 适用场景 | 临时预览、video/audio src | base64 编码、上传到服务器 |
4.3 完整示例:图片预览
class ImagePreview {
constructor(inputId, previewId) {
this.input = document.getElementById(inputId);
this.preview = document.getElementById(previewId);
this.currentUrl = null;
this.input.addEventListener('change', () => this.handleChange());
}
handleChange() {
const file = this.input.files[0];
if (!file) return;
// 释放旧的 URL
if (this.currentUrl) {
URL.revokeObjectURL(this.currentUrl);
}
// 创建新的 URL
this.currentUrl = URL.createObjectURL(file);
this.preview.src = this.currentUrl;
}
destroy() {
if (this.currentUrl) {
URL.revokeObjectURL(this.currentUrl);
}
}
}
// 使用
const preview = new ImagePreview('fileInput', 'preview');
五、拖放文件
5.1 拖放区域
<div id="dropZone" class="drop-zone">
<p>拖放文件到这里</p>
</div>
const dropZone = document.getElementById('dropZone');
// 阻止默认行为
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
});
dropZone.addEventListener('dragenter', (e) => {
e.preventDefault();
e.stopPropagation();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
dropZone.classList.remove('drag-over');
const files = e.dataTransfer.files;
console.log('Dropped files:', files);
});
5.2 拖放与文件列表
// 处理拖放的文件
dropZone.addEventListener('drop', (e) => {
const files = e.dataTransfer.files;
for (const file of files) {
// 检查文件类型
if (!file.type.startsWith('image/')) {
console.warn('Not an image:', file.name);
continue;
}
// 预览
const url = URL.createObjectURL(file);
console.log('Preview URL:', url);
}
});
// 获取拖放的文本内容
dropZone.addEventListener('drop', (e) => {
const text = e.dataTransfer.getData('text/plain');
console.log('Dropped text:', text);
});
// 多类型拖放
dropZone.addEventListener('drop', (e) => {
// 文件
if (e.dataTransfer.files.length > 0) {
console.log('Files:', e.dataTransfer.files);
}
// 纯文本
if (e.dataTransfer.types.includes('text/plain')) {
console.log('Plain text:', e.dataTransfer.getData('text/plain'));
}
// HTML
if (e.dataTransfer.types.includes('text/html')) {
console.log('HTML:', e.dataTransfer.getData('text/html'));
}
});
六、大文件处理
6.1 分片读取
class ChunkedFileReader {
constructor(file, chunkSize = 1024 * 1024) {
this.file = file;
this.chunkSize = chunkSize;
this.offset = 0;
}
async readNextChunk() {
if (this.offset >= this.file.size) {
return null;
}
const slice = this.file.slice(this.offset, this.offset + this.chunkSize);
const buffer = await slice.arrayBuffer();
this.offset += chunkSize;
return {
data: buffer,
offset: this.offset,
progress: this.offset / this.file.size
};
}
async readAllChunks(onProgress) {
const chunks = [];
let result;
while ((result = await this.readNextChunk()) !== null) {
chunks.push(result.data);
if (onProgress) {
onProgress(result.progress);
}
}
return new Blob(chunks);
}
}
// 使用
const reader = new ChunkedFileReader(file, 2 * 1024 * 1024); // 2MB chunks
reader.readAllChunks((progress) => {
console.log(`Progress: ${(progress * 100).toFixed(2)}%`);
}).then((blob) => {
console.log('All chunks read:', blob);
});
6.2 大文件上传
async function uploadFile(file, uploadUrl) {
const chunkSize = 1024 * 1024; // 1MB
const totalChunks = Math.ceil(file.size / chunkSize);
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('chunk', chunk);
formData.append('index', i);
formData.append('total', totalChunks);
formData.append('filename', file.name);
await fetch(uploadUrl, {
method: 'POST',
body: formData
});
console.log(`Uploaded chunk ${i + 1}/${totalChunks}`);
}
}
七、图片缩略图生成
7.1 Canvas 缩放
async function generateThumbnail(file, maxWidth = 200, maxHeight = 200) {
// 读取图片
const img = await createImageBitmap(file);
// 计算缩放比例
let width = img.width;
let height = img.height;
if (width > height) {
if (width > maxWidth) {
height *= maxWidth / width;
width = maxWidth;
}
} else {
if (height > maxHeight) {
width *= maxHeight / height;
height = maxHeight;
}
}
// 绘制缩略图
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
// 转换为 blob
return new Promise((resolve) => {
canvas.toBlob(resolve, 'image/jpeg', 0.8);
});
}
// 使用
const input = document.getElementById('fileInput');
input.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
const thumbnail = await generateThumbnail(file);
const url = URL.createObjectURL(thumbnail);
document.getElementById('thumbnail').src = url;
}
});
八、安全考虑
8.1 文件路径泄露
// ❌ 危险:暴露完整路径
console.log(file.webkitRelativePath);
// ✅ 安全:只获取文件名
console.log(file.name);
8.2 文件类型欺骗
// 用户可能伪造文件类型
// 不应该仅依赖 file.type 判断文件内容
// 应该检测文件内容(magic numbers)
async function detectFileType(file) {
const buffer = await file.slice(0, 4).arrayBuffer();
const bytes = new Uint8Array(buffer);
// PNG: 89 50 4E 47
if (bytes[0] === 0x89 && bytes[1] === 0x50) {
return 'image/png';
}
// JPEG: FF D8 FF
if (bytes[0] === 0xFF && bytes[1] === 0xD8) {
return 'image/jpeg';
}
// PDF: 25 50 44 46
if (bytes[0] === 0x25 && bytes[1] === 0x50) {
return 'application/pdf';
}
return 'unknown';
}