File API

File API允许Web应用读取和处理用户选择的文件。本章将介绍File API的基本概念和使用方法。

文件选择

input元素

<input type="file" id="fileInput" multiple accept="image/*">

<script>
    const input = document.getElementById('fileInput');
    
    input.addEventListener('change', (e) => {
        const files = e.target.files;
        
        for (const file of files) {
            console.log('文件名:', file.name);
            console.log('文件类型:', file.type);
            console.log('文件大小:', file.size);
            console.log('最后修改:', new Date(file.lastModified));
        }
    });
</script>

accept属性

const acceptExamples = {
    'image/*': '所有图片类型',
    'image/png': 'PNG图片',
    '.pdf': 'PDF文件',
    'audio/*,video/*': '音频和视频',
    '.jpg,.jpeg,.png': '指定扩展名',
    'image/*,.pdf': '图片和PDF'
};

拖放选择

const dropZone = document.getElementById('dropZone');

dropZone.addEventListener('drop', (e) => {
    e.preventDefault();
    
    const files = e.dataTransfer.files;
    handleFiles(files);
});

dropZone.addEventListener('dragover', (e) => {
    e.preventDefault();
});

function handleFiles(files) {
    Array.from(files).forEach(file => {
        console.log('处理文件:', file.name);
    });
}

FileReader

读取文本

function readAsText(file) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        
        reader.onload = () => resolve(reader.result);
        reader.onerror = () => reject(reader.error);
        
        reader.readAsText(file);
    });
}

const text = await readAsText(file);
console.log(text);

读取DataURL

function readAsDataURL(file) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        
        reader.onload = () => resolve(reader.result);
        reader.onerror = () => reject(reader.error);
        
        reader.readAsDataURL(file);
    });
}

const dataURL = await readAsDataURL(file);
document.getElementById('preview').src = dataURL;

读取ArrayBuffer

function readAsArrayBuffer(file) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        
        reader.onload = () => resolve(reader.result);
        reader.onerror = () => reject(reader.error);
        
        reader.readAsArrayBuffer(file);
    });
}

const buffer = await readAsArrayBuffer(file);
const view = new DataView(buffer);

读取进度

function readFileWithProgress(file, onProgress) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        
        reader.onprogress = (e) => {
            if (e.lengthComputable) {
                const percent = (e.loaded / e.total) * 100;
                onProgress?.(percent);
            }
        };
        
        reader.onload = () => resolve(reader.result);
        reader.onerror = () => reject(reader.error);
        
        reader.readAsDataURL(file);
    });
}

const result = await readFileWithProgress(file, (percent) => {
    console.log(`读取进度: ${percent.toFixed(1)}%`);
});

Blob操作

创建Blob

const blob1 = new Blob(['Hello, World!'], { type: 'text/plain' });

const blob2 = new Blob(
    ['<html><body>Hello</body></html>'],
    { type: 'text/html' }
);

const blob3 = new Blob(
    [JSON.stringify({ name: '张三' })],
    { type: 'application/json' }
);

const parts = ['Hello, ', 'World!'];
const blob4 = new Blob(parts, { type: 'text/plain' });

Blob方法

const text = await blob.text();

const buffer = await blob.arrayBuffer();

const stream = blob.stream();

const url = URL.createObjectURL(blob);

const slice = blob.slice(0, 1024, blob.type);

Blob转File

const blob = new Blob(['Hello'], { type: 'text/plain' });
const file = new File([blob], 'hello.txt', {
    type: 'text/plain',
    lastModified: Date.now()
});

文件上传

FormData上传

async function uploadFile(file) {
    const formData = new FormData();
    formData.append('file', file);
    formData.append('name', file.name);
    
    const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData
    });
    
    return response.json();
}

async function uploadMultipleFiles(files) {
    const formData = new FormData();
    
    Array.from(files).forEach((file, index) => {
        formData.append(`files[${index}]`, file);
    });
    
    const response = await fetch('/api/upload/multiple', {
        method: 'POST',
        body: formData
    });
    
    return response.json();
}

进度上传

function uploadWithProgress(file, onProgress) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        
        xhr.upload.onprogress = (e) => {
            if (e.lengthComputable) {
                const percent = (e.loaded / e.total) * 100;
                onProgress?.(percent);
            }
        };
        
        xhr.onload = () => {
            if (xhr.status === 200) {
                resolve(JSON.parse(xhr.responseText));
            } else {
                reject(new Error('上传失败'));
            }
        };
        
        xhr.onerror = () => reject(new Error('网络错误'));
        
        const formData = new FormData();
        formData.append('file', file);
        
        xhr.open('POST', '/api/upload');
        xhr.send(formData);
    });
}

分片上传

class ChunkUploader {
    constructor(file, options = {}) {
        this.file = file;
        this.chunkSize = options.chunkSize || 5 * 1024 * 1024;
        this.chunks = Math.ceil(file.size / this.chunkSize);
        this.currentChunk = 0;
    }
    
    async upload(onProgress) {
        const uploadId = await this.initUpload();
        
        for (let i = 0; i < this.chunks; i++) {
            const start = i * this.chunkSize;
            const end = Math.min(start + this.chunkSize, this.file.size);
            const chunk = this.file.slice(start, end);
            
            await this.uploadChunk(uploadId, i, chunk);
            
            this.currentChunk = i + 1;
            onProgress?.((this.currentChunk / this.chunks) * 100);
        }
        
        return await this.completeUpload(uploadId);
    }
    
    async initUpload() {
        const response = await fetch('/api/upload/init', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                name: this.file.name,
                size: this.file.size,
                chunks: this.chunks
            })
        });
        const { uploadId } = await response.json();
        return uploadId;
    }
    
    async uploadChunk(uploadId, index, chunk) {
        const formData = new FormData();
        formData.append('uploadId', uploadId);
        formData.append('index', index);
        formData.append('chunk', chunk);
        
        await fetch('/api/upload/chunk', {
            method: 'POST',
            body: formData
        });
    }
    
    async completeUpload(uploadId) {
        const response = await fetch('/api/upload/complete', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ uploadId })
        });
        return response.json();
    }
}

文件下载

创建下载

function downloadFile(content, filename, mimeType = 'text/plain') {
    const blob = new Blob([content], { type: mimeType });
    const url = URL.createObjectURL(blob);
    
    const link = document.createElement('a');
    link.href = url;
    link.download = filename;
    link.click();
    
    URL.revokeObjectURL(url);
}

downloadFile('Hello, World!', 'hello.txt');
downloadFile(JSON.stringify(data), 'data.json', 'application/json');

function downloadBlob(blob, filename) {
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = filename;
    link.click();
    URL.revokeObjectURL(url);
}

流式下载

async function downloadStream(url, onProgress) {
    const response = await fetch(url);
    const contentLength = response.headers.get('Content-Length');
    
    const reader = response.body.getReader();
    const chunks = [];
    let receivedLength = 0;
    
    while (true) {
        const { done, value } = await reader.read();
        
        if (done) break;
        
        chunks.push(value);
        receivedLength += value.length;
        
        if (contentLength) {
            onProgress?.((receivedLength / contentLength) * 100);
        }
    }
    
    const blob = new Blob(chunks);
    return blob;
}

const blob = await downloadStream('/api/download/large-file', (percent) => {
    console.log(`下载进度: ${percent.toFixed(1)}%`);
});
downloadBlob(blob, 'large-file.zip');

文件预览

图片预览

function previewImage(file) {
    return new Promise((resolve) => {
        const reader = new FileReader();
        
        reader.onload = (e) => {
            const img = document.createElement('img');
            img.src = e.target.result;
            resolve(img);
        };
        
        reader.readAsDataURL(file);
    });
}

async function previewImages(files) {
    const container = document.getElementById('preview-container');
    container.innerHTML = '';
    
    for (const file of files) {
        if (file.type.startsWith('image/')) {
            const img = await previewImage(file);
            img.style.maxWidth = '200px';
            img.style.margin = '10px';
            container.appendChild(img);
        }
    }
}

PDF预览

async function previewPDF(file) {
    const arrayBuffer = await file.arrayBuffer();
    
    const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
    
    const page = await pdf.getPage(1);
    
    const scale = 1.5;
    const viewport = page.getViewport({ scale });
    
    const canvas = document.getElementById('pdf-canvas');
    const context = canvas.getContext('2d');
    
    canvas.width = viewport.width;
    canvas.height = viewport.height;
    
    await page.render({
        canvasContext: context,
        viewport: viewport
    }).promise;
}

东巴文小贴士

📁 文件处理建议

  1. 文件大小限制:前端验证文件大小,避免上传过大文件
  2. 文件类型验证:检查MIME类型和扩展名
  3. 内存管理:及时释放URL对象和FileReader
  4. 用户体验:提供进度反馈和错误提示

🔒 安全注意事项

  • 不要信任客户端验证,服务端必须再次验证
  • 限制上传文件类型和大小
  • 处理文件名中的特殊字符
  • 防止路径遍历攻击

下一步

下一章将探讨 [Web Components基础](file:///e:/db-w.cn/md_data/javascript/81_Web Components基础.md),学习如何创建可复用的自定义组件。