JavaScript 文件传输,从入门到放弃系列 🚀

想象一下,你正在开发一个类似 Instagram 的照片分享应用,或者一个类似 Dropbox 的云存储服务。文件上传下载功能就像是这些应用的"心脏",让用户能够自由地分享他们的精彩时刻。今天,让我们一起深入探索 JavaScript 文件传输的奥秘!

1. 传统表单上传 - 最简单的开始 📝

就像我们学习开车从手动挡开始一样,理解文件上传最好也从最基础的表单上传开始:

html 复制代码
<form enctype="multipart/form-data" method="post">
    <input type="file" name="file" />
    <button type="submit">上传</button>
</form>

2. FormData + AJAX 上传 - 现代化的解决方案 ⚡

如果说传统表单上传是自行车,那么 FormData + AJAX 就是摩托车 ------ 更快、更灵活、更强大:

javascript 复制代码
const uploadFile = async (file) => {
    const formData = new FormData();
    formData.append('file', file);
    
    try {
        const response = await fetch('/upload', {
            method: 'POST',
            body: formData
        });
        const result = await response.json();
        console.log('上传成功:', result);
    } catch (error) {
        console.error('上传失败:', error);
    }
};

3. 拖拽上传 - 让用户体验飞起来 🎯

想象一下 Windows 系统中拖拽文件的感觉,现在我们可以在网页中实现同样流畅的体验:

javascript 复制代码
const dropZone = document.getElementById('drop-zone');

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

dropZone.addEventListener('drop', (e) => {
    e.preventDefault();
    dropZone.classList.remove('dragover');
    
    const files = Array.from(e.dataTransfer.files);
    files.forEach(file => uploadFile(file));
});

4. 大文件分片上传 - 化整为零的艺术 🧩

就像搬家时我们会把大件物品拆分成小件一样,处理大文件时也需要进行分片处理:

javascript 复制代码
const chunkSize = 1024 * 1024; // 1MB per chunk

const uploadLargeFile = async (file) => {
    const chunks = Math.ceil(file.size / chunkSize);
    
    for (let i = 0; i < chunks; i++) {
        const chunk = file.slice(
            i * chunkSize,
            Math.min((i + 1) * chunkSize, file.size)
        );
        
        const formData = new FormData();
        formData.append('chunk', chunk);
        formData.append('chunkIndex', i);
        formData.append('totalChunks', chunks);
        
        await fetch('/upload-chunk', {
            method: 'POST',
            body: formData
        });
    }
};

5. 文件下载 - 数据的"快递服务" 📦

文件下载就像网上购物的快递服务,我们有多种方式来处理这个过程:

javascript 复制代码
// 方式1:使用 Blob
const downloadFile = (url, filename) => {
    fetch(url)
        .then(response => response.blob())
        .then(blob => {
            const link = document.createElement('a');
            link.href = URL.createObjectURL(blob);
            link.download = filename;
            link.click();
            URL.revokeObjectURL(link.href);
        });
};

// 方式2:直接下载
const directDownload = (url) => {
    window.location.href = url;
};

6. 进度监控 - 给用户一个期待 ⏳

记得我们在下载游戏时那个让人既期待又焦虑的进度条吗?来看看如何实现它:

javascript 复制代码
const uploadWithProgress = (file, onProgress) => {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        const formData = new FormData();
        formData.append('file', file);
        
        xhr.upload.addEventListener('progress', (e) => {
            if (e.lengthComputable) {
                const percentComplete = (e.loaded / e.total) * 100;
                onProgress(percentComplete);
            }
        });
        
        xhr.addEventListener('load', () => resolve(xhr.response));
        xhr.addEventListener('error', () => reject(xhr.statusText));
        
        xhr.open('POST', '/upload');
        xhr.send(formData);
    });
};

7. 安全考虑 - 构建坚固的城墙 🛡️

就像我们不会让陌生人随意进入家门一样,文件上传系统也需要严格的安全措施:

javascript 复制代码
const validateFile = (file) => {
    // 检查文件类型
    const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
    if (!allowedTypes.includes(file.type)) {
        throw new Error('不支持的文件类型');
    }
    
    // 检查文件大小(例如最大 10MB)
    const maxSize = 10 * 1024 * 1024;
    if (file.size > maxSize) {
        throw new Error('文件太大');
    }
    
    return true;
};

实战案例:打造一个迷你云盘 💾

让我们用以上学到的知识,来实现一个简单但功能完整的文件上传系统:

javascript 复制代码
// 创建一个优雅的拖拽上传区域
class MiniDrive {
    constructor(elementId) {
        this.dropZone = document.getElementById(elementId);
        this.setupDropZone();
        this.setupProgressUI();
    }

    setupDropZone() {
        this.dropZone.innerHTML = `
            <div class="upload-icon">📁</div>
            <p>把文件拖到这里,或者点击上传</p>
            <div class="progress-bar" style="display: none"></div>
        `;
        
        // 添加点击上传功能
        this.dropZone.addEventListener('click', () => {
            const input = document.createElement('input');
            input.type = 'file';
            input.multiple = true;
            input.onchange = (e) => this.handleFiles(e.target.files);
            input.click();
        });

        // 添加拖拽功能
        this.dropZone.addEventListener('dragover', this.handleDragOver.bind(this));
        this.dropZone.addEventListener('drop', this.handleDrop.bind(this));
    }
    
    // ... 其他实现代码
}

// 使用方式
const miniDrive = new MiniDrive('drop-zone');

趣味小贴士 💡

  1. 文件上传失败?试试"断点续传" ------ 就像看视频时可以从上次暂停的地方继续播放一样
  2. 别让用户等太久 ------ 添加图片预览功能,让等待变得有趣
  3. 给上传添加一些动画效果,比如文件飞入效果,让界面更生动

常见问题解决方案 🔧

  1. 上传超时怎么办?

    javascript 复制代码
    const uploadWithRetry = async (file, maxRetries = 3) => {
        let retries = 0;
        
        while (retries < maxRetries) {
            try {
                const formData = new FormData();
                formData.append('file', file);
                
                const response = await fetch('/upload', {
                    method: 'POST',
                    body: formData,
                    timeout: 30000 // 30秒超时
                });
                
                if (response.ok) {
                    return await response.json();
                }
            } catch (error) {
                retries++;
                console.log(`上传失败,第 ${retries} 次重试...`);
                // 等待一段时间后重试
                await new Promise(resolve => setTimeout(resolve, 2000 * retries));
            }
        }
        
        throw new Error('上传失败,已超过最大重试次数');
    };
  2. 如何处理大量小文件?

    javascript 复制代码
    class BatchUploader {
        constructor(maxConcurrent = 3) {
            this.queue = [];
            this.maxConcurrent = maxConcurrent;
            this.currentUploads = 0;
        }
        
        addToQueue(files) {
            this.queue.push(...Array.from(files));
            this.processQueue();
        }
        
        async processQueue() {
            while (this.queue.length > 0 && this.currentUploads < this.maxConcurrent) {
                const file = this.queue.shift();
                this.currentUploads++;
                
                try {
                    await this.uploadFile(file);
                } finally {
                    this.currentUploads--;
                    this.processQueue();
                }
            }
        }
        
        async uploadFile(file) {
            const formData = new FormData();
            formData.append('file', file);
            
            const response = await fetch('/upload', {
                method: 'POST',
                body: formData
            });
            
            return response.json();
        }
    }
    
    // 使用方式
    const uploader = new BatchUploader(3);
    uploader.addToQueue(fileList);
  3. 用户上传出错了怎么办?

    javascript 复制代码
    class UploadManager {
        constructor() {
            this.uploads = new Map(); // 存储上传进度
        }
        
        async uploadWithProgress(file) {
            const uploadId = Date.now().toString();
            this.uploads.set(uploadId, {
                progress: 0,
                status: 'pending',
                file: file
            });
            
            try {
                const xhr = new XMLHttpRequest();
                
                // 进度监控
                xhr.upload.addEventListener('progress', (e) => {
                    if (e.lengthComputable) {
                        const progress = (e.loaded / e.total) * 100;
                        this.updateProgress(uploadId, progress);
                    }
                });
                
                // 错误处理
                xhr.upload.addEventListener('error', () => {
                    this.handleError(uploadId, '上传失败');
                });
                
                // 成功处理
                xhr.upload.addEventListener('load', () => {
                    this.updateStatus(uploadId, 'success');
                });
                
                // 开始上传
                const formData = new FormData();
                formData.append('file', file);
                xhr.open('POST', '/upload');
                xhr.send(formData);
                
                // 返回上传ID,方便后续查询状态
                return uploadId;
            } catch (error) {
                this.handleError(uploadId, error.message);
                throw error;
            }
        }
        
        updateProgress(uploadId, progress) {
            const upload = this.uploads.get(uploadId);
            if (upload) {
                upload.progress = progress;
                this.notifyProgressUpdate(uploadId, progress);
            }
        }
        
        updateStatus(uploadId, status) {
            const upload = this.uploads.get(uploadId);
            if (upload) {
                upload.status = status;
            }
        }
        
        handleError(uploadId, error) {
            const upload = this.uploads.get(uploadId);
            if (upload) {
                upload.status = 'error';
                upload.error = error;
                
                // 自动重试逻辑
                if (!upload.retryCount || upload.retryCount < 3) {
                    upload.retryCount = (upload.retryCount || 0) + 1;
                    setTimeout(() => {
                        this.uploadWithProgress(upload.file);
                    }, 2000 * upload.retryCount);
                }
            }
        }
        
        notifyProgressUpdate(uploadId, progress) {
            // 触发进度更新事件
            const event = new CustomEvent('uploadProgress', {
                detail: { uploadId, progress }
            });
            window.dispatchEvent(event);
        }
    }
    
    // 使用示例
    const manager = new UploadManager();
    
    // 监听上传进度
    window.addEventListener('uploadProgress', (e) => {
        const { uploadId, progress } = e.detail;
        console.log(`文件上传进度: ${progress}%`);
        updateProgressUI(progress); // 更新UI进度条
    });
    
    // 开始上传
    const uploadId = await manager.uploadWithProgress(file);
  4. 如何实现断点续传?

    javascript 复制代码
    class ResumableUploader {
        constructor(file, chunkSize = 1024 * 1024) {
            this.file = file;
            this.chunkSize = chunkSize;
            this.chunks = Math.ceil(file.size / chunkSize);
            this.currentChunk = 0;
        }
        
        async start() {
            // 获取已上传的部分
            const uploadedChunks = await this.getUploadedChunks();
            this.currentChunk = uploadedChunks.length;
            
            while (this.currentChunk < this.chunks) {
                const chunk = this.getChunk(this.currentChunk);
                try {
                    await this.uploadChunk(chunk, this.currentChunk);
                    this.currentChunk++;
                    this.saveProgress();
                } catch (error) {
                    console.error('上传失败,将在3秒后重试...');
                    await new Promise(resolve => setTimeout(resolve, 3000));
                    // 继续当前chunk的上传,不增加currentChunk
                }
            }
            
            // 所有分片上传完成,通知服务器合并文件
            await this.mergeChunks();
        }
        
        getChunk(index) {
            const start = index * this.chunkSize;
            const end = Math.min(start + this.chunkSize, this.file.size);
            return this.file.slice(start, end);
        }
        
        async uploadChunk(chunk, index) {
            const formData = new FormData();
            formData.append('chunk', chunk);
            formData.append('index', index);
            formData.append('fileId', this.file.name);
            
            const response = await fetch('/upload-chunk', {
                method: 'POST',
                body: formData
            });
            
            if (!response.ok) {
                throw new Error('Chunk upload failed');
            }
        }
        
        saveProgress() {
            localStorage.setItem(`upload-${this.file.name}`, this.currentChunk);
        }
        
        async getUploadedChunks() {
            const savedChunk = localStorage.getItem(`upload-${this.file.name}`);
            if (savedChunk) {
                // 验证服务器端已上传的块
                const response = await fetch(`/verify-chunks?fileId=${this.file.name}`);
                const { chunks } = await response.json();
                return chunks;
            }
            return [];
        }
        
        async mergeChunks() {
            const response = await fetch('/merge-chunks', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    fileId: this.file.name,
                    chunks: this.chunks
                })
            });
            
            if (!response.ok) {
                throw new Error('Failed to merge chunks');
            }
        }
    }
    
    // 使用示例
    const uploader = new ResumableUploader(file);
    uploader.start().then(() => {
        console.log('文件上传完成!');
    }).catch(error => {
        console.error('上传失败:', error);
    });
相关推荐
ordinary9023 分钟前
指令-v-for的key
前端·javascript·vue.js
rpa_top26 分钟前
RPA 助力电商:自动化商品信息上传,节省人力资源 —— 以影刀 RPA 为例【rpa.top】
大数据·前端·人工智能·自动化·rpa
新时代农民工--小明27 分钟前
前端自动化部署更新,自动化打包部署
运维·前端·自动化
前端Hardy40 分钟前
HTML&CSS:酷炫的3D开关控件
前端·javascript·css·3d·html
小马超会养兔子1 小时前
如何写一个数字老虎机滚轮
开发语言·前端·javascript·vue
Web打印1 小时前
web打印插件 HttpPrinter 使用半年评测
javascript·json·firefox·jquery·html5
武昌库里写JAVA1 小时前
使用React Strict DOM改善React生态系统
数据结构·vue.js·spring boot·算法·课程设计
m0_672449601 小时前
前端(组件传参案例)
前端
devotemyself1 小时前
vue的ElMessage的css样式不生效
前端·css·vue.js
Mr。轩。1 小时前
Vue 单表 CRUD模板 前端
前端·javascript·vue.js