引言

在Web开发的历史长河中,文件上传功能一直是前端开发中的重要组成部分。SWFUpload作为早期Web 2.0时代的明星技术,曾因其强大的功能和跨浏览器兼容性而广受欢迎。然而,随着HTML5标准的普及和浏览器安全策略的演进,SWFUpload逐渐退出了历史舞台。本文将深入解析SWFUpload的技术原理、优缺点,并探讨现代Web开发中更为先进和安全的替代方案。

一、SWFUpload技术解析

1.1 SWFUpload的基本原理

SWFUpload是一个基于Adobe Flash的文件上传组件,它通过嵌入一个Flash对象来实现文件上传功能。其核心工作流程如下:

  1. 初始化阶段:开发者在HTML页面中嵌入SWFUpload的Flash对象,并配置相关参数。
  2. 文件选择:用户点击按钮触发Flash的文件选择对话框,选择本地文件。
  3. 文件上传:SWFUpload将文件数据分块上传到服务器,支持进度监控和断点续传。
  4. 回调处理:上传完成后,通过JavaScript回调函数处理服务器响应。

1.2 SWFUpload的核心特性

SWFUpload提供了许多当时HTML4无法实现的功能:

  • 多文件选择:支持一次选择多个文件进行上传
  • 进度监控:实时显示上传进度,提升用户体验
  • 断点续传:网络中断后可从断点处继续上传
  • 文件类型限制:可限制上传文件的扩展名和大小
  • 跨浏览器兼容:在IE6+、Firefox、Chrome等浏览器中表现一致

1.3 SWFUpload的代码示例

以下是一个典型的SWFUpload初始化代码:

// SWFUpload初始化配置
var swfu = new SWFUpload({
    // 上传处理URL
    upload_url: "/upload.php",
    
    // Flash文件路径
    flash_url: "/swfupload.swf",
    
    // 按钮设置
    button_image_url: "/images/button.png",
    button_width: 61,
    button_height: 22,
    button_text: '选择文件',
    button_text_style: ".smallText { font-size: 13px; }",
    button_text_top_padding: 2,
    button_text_left_padding: 18,
    
    // 文件限制
    file_size_limit: "100 MB",
    file_types: "*.jpg;*.jpeg;*.png;*.gif",
    file_types_description: "图片文件",
    file_upload_limit: 10,
    
    // 事件处理
    file_queued_handler: fileQueued,
    file_queue_error_handler: fileQueueError,
    file_dialog_complete_handler: fileDialogComplete,
    upload_start_handler: uploadStart,
    upload_progress_handler: uploadProgress,
    upload_error_handler: uploadError,
    upload_success_handler: uploadSuccess,
    upload_complete_handler: uploadComplete,
    
    // 其他设置
    custom_settings: {
        uploadTarget: "divFileProgressContainer"
    },
    
    // Debug模式
    debug: false
});

// 事件处理函数示例
function fileQueued(file) {
    console.log("文件已加入队列: " + file.name);
}

function uploadProgress(file, bytesLoaded, bytesTotal) {
    var percent = Math.ceil((bytesLoaded / bytesTotal) * 100);
    console.log("上传进度: " + percent + "%");
}

function uploadSuccess(file, serverData) {
    console.log("上传成功: " + file.name);
    console.log("服务器响应: " + serverData);
}

1.4 SWFUpload的服务器端处理

SWFUpload的服务器端处理通常使用PHP、ASP.NET或Java等语言。以下是一个简单的PHP处理示例:

<?php
// upload.php - SWFUpload服务器端处理示例

// 设置响应头
header("Content-Type: text/plain; charset=utf-8");

// 获取上传的文件
$uploadDir = "./uploads/";
$uploadFile = $uploadDir . basename($_FILES['Filedata']['name']);

// 检查文件大小(SWFUpload会发送文件大小信息)
if ($_FILES['Filedata']['size'] > 100 * 1024 * 1024) {
    echo "文件大小超过限制";
    exit;
}

// 检查文件类型
$allowedTypes = array('image/jpeg', 'image/png', 'image/gif');
if (!in_array($_FILES['Filedata']['type'], $allowedTypes)) {
    echo "文件类型不支持";
    exit;
}

// 移动上传的文件
if (move_uploaded_file($_FILES['Filedata']['tmp_name'], $uploadFile)) {
    echo "上传成功";
} else {
    echo "上传失败";
}
?>

二、SWFUpload的优缺点分析

2.1 SWFUpload的优点

  1. 功能强大:在HTML5出现之前,提供了丰富的文件上传功能
  2. 兼容性好:解决了早期浏览器之间的差异
  3. 用户体验佳:支持进度条、多文件选择等现代功能
  4. 断点续传:对于大文件上传特别有用

2.2 SWFUpload的缺点

  1. 依赖Flash:需要用户安装Flash插件,且Flash存在安全漏洞
  2. 移动端不支持:iOS和Android设备不支持Flash
  3. 性能问题:Flash运行效率不如原生JavaScript
  4. 维护困难:Adobe已停止Flash支持,SWFUpload不再更新
  5. 安全风险:Flash存在多个安全漏洞,容易被攻击

2.3 SWFUpload的淘汰原因

  1. HTML5标准的普及:HTML5提供了原生的文件上传API
  2. 浏览器安全策略:现代浏览器限制了Flash的使用
  3. 移动互联网兴起:移动设备不支持Flash
  4. Adobe停止支持:2020年Adobe正式停止Flash支持

三、现代替代方案

3.1 HTML5 File API

HTML5提供了原生的文件上传API,无需任何插件即可实现文件上传功能。

3.1.1 基础文件上传

<!DOCTYPE html>
<html>
<head>
    <title>HTML5文件上传示例</title>
</head>
<body>
    <input type="file" id="fileInput" multiple>
    <button onclick="uploadFiles()">上传文件</button>
    <div id="progressContainer"></div>
    
    <script>
        function uploadFiles() {
            const fileInput = document.getElementById('fileInput');
            const files = fileInput.files;
            
            if (files.length === 0) {
                alert('请选择文件');
                return;
            }
            
            // 创建FormData对象
            const formData = new FormData();
            
            // 添加文件到FormData
            for (let i = 0; i < files.length; i++) {
                formData.append('files[]', files[i]);
            }
            
            // 创建XMLHttpRequest
            const xhr = new XMLHttpRequest();
            
            // 进度事件处理
            xhr.upload.addEventListener('progress', function(e) {
                if (e.lengthComputable) {
                    const percent = Math.ceil((e.loaded / e.total) * 100);
                    updateProgress(percent);
                }
            });
            
            // 完成事件处理
            xhr.addEventListener('load', function() {
                if (xhr.status === 200) {
                    alert('上传成功: ' + xhr.responseText);
                } else {
                    alert('上传失败: ' + xhr.status);
                }
            });
            
            // 错误事件处理
            xhr.addEventListener('error', function() {
                alert('上传过程中发生错误');
            });
            
            // 发送请求
            xhr.open('POST', '/upload.php');
            xhr.send(formData);
        }
        
        function updateProgress(percent) {
            const container = document.getElementById('progressContainer');
            container.innerHTML = `
                <div style="width: 200px; height: 20px; border: 1px solid #ccc;">
                    <div style="width: ${percent}%; height: 100%; background: #4CAF50;"></div>
                </div>
                <div>${percent}%</div>
            `;
        }
    </script>
</body>
</html>

3.1.2 分块上传实现

对于大文件上传,分块上传可以提高稳定性和用户体验:

// 分块上传实现
class ChunkedUploader {
    constructor(options) {
        this.chunkSize = options.chunkSize || 2 * 1024 * 1024; // 2MB
        this.uploadUrl = options.uploadUrl;
        this.onProgress = options.onProgress;
        this.onSuccess = options.onSuccess;
        this.onError = options.onError;
    }
    
    async uploadFile(file) {
        const totalChunks = Math.ceil(file.size / this.chunkSize);
        const fileId = this.generateFileId();
        
        for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
            const start = chunkIndex * this.chunkSize;
            const end = Math.min(start + this.chunkSize, file.size);
            const chunk = file.slice(start, end);
            
            const formData = new FormData();
            formData.append('fileId', fileId);
            formData.append('chunkIndex', chunkIndex);
            formData.append('totalChunks', totalChunks);
            formData.append('chunk', chunk);
            formData.append('fileName', file.name);
            
            try {
                await this.uploadChunk(formData, chunkIndex, totalChunks);
                
                // 更新进度
                const progress = Math.ceil(((chunkIndex + 1) / totalChunks) * 100);
                if (this.onProgress) {
                    this.onProgress(progress, file.name);
                }
            } catch (error) {
                if (this.onError) {
                    this.onError(error, file.name);
                }
                throw error;
            }
        }
        
        // 所有分块上传完成,通知服务器合并
        await this.completeUpload(fileId, file.name);
        
        if (this.onSuccess) {
            this.onSuccess(file.name);
        }
    }
    
    uploadChunk(formData, chunkIndex, totalChunks) {
        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            
            xhr.open('POST', this.uploadUrl);
            
            xhr.onload = function() {
                if (xhr.status === 200) {
                    resolve(xhr.responseText);
                } else {
                    reject(new Error(`Chunk ${chunkIndex} upload failed: ${xhr.status}`));
                }
            };
            
            xhr.onerror = function() {
                reject(new Error(`Chunk ${chunkIndex} upload error`));
            };
            
            xhr.send(formData);
        });
    }
    
    completeUpload(fileId, fileName) {
        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            const formData = new FormData();
            
            formData.append('action', 'complete');
            formData.append('fileId', fileId);
            formData.append('fileName', fileName);
            
            xhr.open('POST', this.uploadUrl);
            
            xhr.onload = function() {
                if (xhr.status === 200) {
                    resolve(xhr.responseText);
                } else {
                    reject(new Error('Complete upload failed'));
                }
            };
            
            xhr.onerror = function() {
                reject(new Error('Complete upload error'));
            };
            
            xhr.send(formData);
        });
    }
    
    generateFileId() {
        return 'file_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
    }
}

// 使用示例
const uploader = new ChunkedUploader({
    uploadUrl: '/upload.php',
    chunkSize: 2 * 1024 * 1024, // 2MB
    onProgress: (progress, fileName) => {
        console.log(`文件 ${fileName} 上传进度: ${progress}%`);
    },
    onSuccess: (fileName) => {
        console.log(`文件 ${fileName} 上传完成`);
    },
    onError: (error, fileName) => {
        console.error(`文件 ${fileName} 上传失败:`, error);
    }
});

// 上传文件
document.getElementById('fileInput').addEventListener('change', function(e) {
    const file = e.target.files[0];
    if (file) {
        uploader.uploadFile(file).catch(console.error);
    }
});

3.2 拖拽上传与剪贴板粘贴

现代Web应用支持更丰富的交互方式:

<!DOCTYPE html>
<html>
<head>
    <title>拖拽上传示例</title>
    <style>
        .drop-zone {
            width: 400px;
            height: 200px;
            border: 2px dashed #ccc;
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
            transition: all 0.3s;
        }
        
        .drop-zone.dragover {
            border-color: #4CAF50;
            background-color: #f0f8f0;
        }
        
        .preview-container {
            margin-top: 20px;
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
        }
        
        .preview-item {
            width: 100px;
            height: 100px;
            border: 1px solid #ddd;
            border-radius: 4px;
            overflow: hidden;
            position: relative;
        }
        
        .preview-item img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
        
        .preview-item .progress {
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            height: 4px;
            background: #4CAF50;
            transform: scaleX(0);
            transform-origin: left;
            transition: transform 0.3s;
        }
    </style>
</head>
<body>
    <div class="drop-zone" id="dropZone">
        <div>
            <p>拖拽文件到此处或点击选择</p>
            <input type="file" id="fileInput" multiple style="display: none;">
            <button onclick="document.getElementById('fileInput').click()">选择文件</button>
        </div>
    </div>
    
    <div class="preview-container" id="previewContainer"></div>
    
    <script>
        const dropZone = document.getElementById('dropZone');
        const fileInput = document.getElementById('fileInput');
        const previewContainer = document.getElementById('previewContainer');
        
        // 拖拽事件处理
        dropZone.addEventListener('dragover', (e) => {
            e.preventDefault();
            dropZone.classList.add('dragover');
        });
        
        dropZone.addEventListener('dragleave', () => {
            dropZone.classList.remove('dragover');
        });
        
        dropZone.addEventListener('drop', (e) => {
            e.preventDefault();
            dropZone.classList.remove('dragover');
            
            const files = e.dataTransfer.files;
            handleFiles(files);
        });
        
        // 文件选择事件处理
        fileInput.addEventListener('change', (e) => {
            handleFiles(e.target.files);
        });
        
        // 处理文件
        function handleFiles(files) {
            Array.from(files).forEach(file => {
                // 创建预览
                createPreview(file);
                
                // 上传文件
                uploadFile(file);
            });
        }
        
        // 创建预览
        function createPreview(file) {
            const previewItem = document.createElement('div');
            previewItem.className = 'preview-item';
            previewItem.id = `preview_${file.name}`;
            
            if (file.type.startsWith('image/')) {
                const reader = new FileReader();
                reader.onload = (e) => {
                    const img = document.createElement('img');
                    img.src = e.target.result;
                    previewItem.appendChild(img);
                };
                reader.readAsDataURL(file);
            } else {
                const icon = document.createElement('div');
                icon.style.cssText = 'width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#f5f5f5;';
                icon.textContent = file.name.split('.').pop().toUpperCase();
                previewItem.appendChild(icon);
            }
            
            // 进度条
            const progressBar = document.createElement('div');
            progressBar.className = 'progress';
            previewItem.appendChild(progressBar);
            
            previewContainer.appendChild(previewItem);
        }
        
        // 上传文件
        function uploadFile(file) {
            const formData = new FormData();
            formData.append('file', file);
            
            const xhr = new XMLHttpRequest();
            
            xhr.upload.addEventListener('progress', (e) => {
                if (e.lengthComputable) {
                    const percent = e.loaded / e.total;
                    const previewItem = document.getElementById(`preview_${file.name}`);
                    if (previewItem) {
                        const progressBar = previewItem.querySelector('.progress');
                        progressBar.style.transform = `scaleX(${percent})`;
                    }
                }
            });
            
            xhr.addEventListener('load', () => {
                if (xhr.status === 200) {
                    console.log(`文件 ${file.name} 上传成功`);
                } else {
                    console.error(`文件 ${file.name} 上传失败`);
                }
            });
            
            xhr.open('POST', '/upload.php');
            xhr.send(formData);
        }
        
        // 剪贴板粘贴支持
        document.addEventListener('paste', (e) => {
            const items = e.clipboardData.items;
            const files = [];
            
            for (let i = 0; i < items.length; i++) {
                if (items[i].type.indexOf('image') !== -1) {
                    const file = items[i].getAsFile();
                    files.push(file);
                }
            }
            
            if (files.length > 0) {
                handleFiles(files);
            }
        });
    </script>
</body>
</html>

3.3 现代JavaScript库

3.3.1 Uppy - 功能强大的文件上传库

Uppy是一个现代化的文件上传库,提供了丰富的功能和插件系统:

// 安装: npm install @uppy/core @uppy/dashboard @uppy/xhr-upload

import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import XHRUpload from '@uppy/xhr-upload';

// 初始化Uppy
const uppy = new Uppy({
    debug: true,
    autoProceed: false,
    restrictions: {
        maxFileSize: 100 * 1024 * 1024, // 100MB
        maxNumberOfFiles: 10,
        minNumberOfFiles: 1,
        allowedFileTypes: ['image/*', '.pdf', '.doc']
    }
});

// 添加Dashboard插件(UI界面)
uppy.use(Dashboard, {
    inline: true,
    target: '#uppy-dashboard',
    showProgressDetails: true,
    proudlyDisplayPoweredByUppy: false,
    width: 800,
    height: 550,
    thumbnailWidth: 280,
    note: '图片、PDF、文档,最多10个文件,每个最大100MB'
});

// 添加XHR上传插件
uppy.use(XHRUpload, {
    endpoint: '/upload.php',
    formData: true,
    fieldName: 'files[]',
    limit: 5, // 并发上传限制
    timeout: 30000, // 30秒超时
    retryDelays: [0, 1000, 3000, 5000], // 重试延迟
    bundle: true // 是否打包上传
});

// 事件监听
uppy.on('file-added', (file) => {
    console.log(`文件已添加: ${file.name}`);
});

uppy.on('upload-progress', (file, progress) => {
    console.log(`上传进度: ${progress}%`);
});

uppy.on('upload-success', (file, response) => {
    console.log(`上传成功: ${file.name}`);
    console.log('服务器响应:', response.body);
});

uppy.on('upload-error', (file, error, response) => {
    console.error(`上传失败: ${file.name}`, error);
});

uppy.on('complete', (result) => {
    console.log('所有文件上传完成');
    console.log('成功:', result.successful);
    console.log('失败:', result.failed);
});

// 手动触发上传
// uppy.upload();

3.3.2 Dropzone.js - 轻量级拖拽上传库

// 安装: npm install dropzone

// HTML
<div id="my-dropzone" class="dropzone"></div>

// JavaScript
import Dropzone from 'dropzone';

// 配置Dropzone
Dropzone.autoDiscover = false;

const myDropzone = new Dropzone("#my-dropzone", {
    url: "/upload.php", // 上传地址
    method: "post", // 上传方法
    paramName: "file", // 参数名
    maxFilesize: 100, // 最大文件大小(MB)
    maxFiles: 10, // 最大文件数量
    acceptedFiles: "image/*,.pdf,.doc,.docx", // 接受的文件类型
    addRemoveLinks: true, // 显示删除链接
    dictDefaultMessage: "拖拽文件到此处或点击上传",
    dictRemoveFile: "删除",
    dictCancelUpload: "取消上传",
    dictCancelUploadConfirmation: "确定取消上传吗?",
    dictFileTooBig: "文件过大 ({{filesize}}MB). 最大允许: {{maxFilesize}}MB.",
    dictInvalidFileType: "不支持的文件类型",
    dictResponseError: "服务器返回错误 {{statusCode}}",
    dictMaxFilesExceeded: "已达到最大文件数量限制",
    
    // 并行上传
    parallelUploads: 5,
    
    // 重试设置
    retryChunks: true,
    retryChunksLimit: 3,
    
    // 进度显示
    previewsContainer: "#previews",
    previewTemplate: `
        <div class="dz-preview dz-file-preview">
            <div class="dz-details">
                <div class="dz-filename"><span data-dz-name></span></div>
                <div class="dz-size" data-dz-size></div>
                <img data-dz-thumbnail />
            </div>
            <div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
            <div class="dz-success-mark">✓</div>
            <div class="dz-error-mark">✗</div>
            <div class="dz-error-message"><span data-dz-errormessage></span></div>
        </div>
    `,
    
    // 事件处理
    init: function() {
        this.on("addedfile", function(file) {
            console.log("文件已添加:", file.name);
        });
        
        this.on("uploadprogress", function(file, progress, bytesSent) {
            console.log("上传进度:", progress + "%");
        });
        
        this.on("success", function(file, response) {
            console.log("上传成功:", file.name);
        });
        
        this.on("error", function(file, errorMessage) {
            console.error("上传失败:", file.name, errorMessage);
        });
        
        this.on("complete", function(file) {
            console.log("上传完成:", file.name);
        });
        
        this.on("removedfile", function(file) {
            console.log("文件已删除:", file.name);
        });
    }
});

3.4 服务器端处理示例

3.4.1 Node.js + Express 处理上传

// 安装: npm install express multer

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');

const app = express();
const PORT = 3000;

// 配置Multer
const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        const uploadDir = './uploads';
        if (!fs.existsSync(uploadDir)) {
            fs.mkdirSync(uploadDir, { recursive: true });
        }
        cb(null, uploadDir);
    },
    filename: function (req, file, cb) {
        // 生成唯一文件名
        const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
        const ext = path.extname(file.originalname);
        cb(null, file.fieldname + '-' + uniqueSuffix + ext);
    }
});

// 文件过滤器
const fileFilter = (req, file, cb) => {
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
    if (allowedTypes.includes(file.mimetype)) {
        cb(null, true);
    } else {
        cb(new Error('不支持的文件类型'), false);
    }
};

// 配置Multer中间件
const upload = multer({
    storage: storage,
    limits: {
        fileSize: 100 * 1024 * 1024 // 100MB
    },
    fileFilter: fileFilter
});

// 处理单文件上传
app.post('/upload-single', upload.single('file'), (req, res) => {
    if (!req.file) {
        return res.status(400).json({ error: '没有文件被上传' });
    }
    
    res.json({
        success: true,
        message: '文件上传成功',
        file: {
            originalName: req.file.originalname,
            filename: req.file.filename,
            size: req.file.size,
            mimetype: req.file.mimetype,
            path: req.file.path
        }
    });
});

// 处理多文件上传
app.post('/upload-multiple', upload.array('files', 10), (req, res) => {
    if (!req.files || req.files.length === 0) {
        return res.status(400).json({ error: '没有文件被上传' });
    }
    
    const files = req.files.map(file => ({
        originalName: file.originalname,
        filename: file.filename,
        size: file.size,
        mimetype: file.mimetype
    }));
    
    res.json({
        success: true,
        message: `成功上传 ${files.length} 个文件`,
        files: files
    });
});

// 处理分块上传
const chunkStorage = multer.diskStorage({
    destination: function (req, file, cb) {
        const chunkDir = `./chunks/${req.body.fileId}`;
        if (!fs.existsSync(chunkDir)) {
            fs.mkdirSync(chunkDir, { recursive: true });
        }
        cb(null, chunkDir);
    },
    filename: function (req, file, cb) {
        cb(null, `chunk_${req.body.chunkIndex}`);
    }
});

const chunkUpload = multer({ storage: chunkStorage });

app.post('/upload-chunk', chunkUpload.single('chunk'), (req, res) => {
    const { fileId, chunkIndex, totalChunks, fileName } = req.body;
    
    // 检查是否所有分块都已上传
    const chunkDir = `./chunks/${fileId}`;
    const uploadedChunks = fs.readdirSync(chunkDir).length;
    
    if (uploadedChunks === parseInt(totalChunks)) {
        // 所有分块上传完成,合并文件
        const outputPath = `./uploads/${fileName}`;
        const writeStream = fs.createWriteStream(outputPath);
        
        for (let i = 0; i < parseInt(totalChunks); i++) {
            const chunkPath = path.join(chunkDir, `chunk_${i}`);
            const chunkData = fs.readFileSync(chunkPath);
            writeStream.write(chunkData);
        }
        
        writeStream.end();
        
        // 清理临时分块
        fs.rmSync(chunkDir, { recursive: true, force: true });
        
        res.json({
            success: true,
            message: '文件合并完成',
            filePath: outputPath
        });
    } else {
        res.json({
            success: true,
            message: `分块 ${chunkIndex} 上传成功,等待其他分块`
        });
    }
});

// 错误处理中间件
app.use((err, req, res, next) => {
    if (err instanceof multer.MulterError) {
        if (err.code === 'LIMIT_FILE_SIZE') {
            return res.status(400).json({ error: '文件大小超过限制' });
        }
        if (err.code === 'LIMIT_FILE_COUNT') {
            return res.status(400).json({ error: '文件数量超过限制' });
        }
    }
    
    res.status(500).json({ error: err.message });
});

app.listen(PORT, () => {
    console.log(`服务器运行在 http://localhost:${PORT}`);
});

3.4.2 Python Flask 处理上传

# 安装: pip install flask werkzeug

from flask import Flask, request, jsonify
import os
import uuid
from werkzeug.utils import secure_filename
from werkzeug.datastructures import FileStorage

app = Flask(__name__)

# 配置
UPLOAD_FOLDER = './uploads'
CHUNK_FOLDER = './chunks'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf', 'doc', 'docx'}
MAX_FILE_SIZE = 100 * 1024 * 1024  # 100MB

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE

# 创建上传目录
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(CHUNK_FOLDER, exist_ok=True)

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/upload-single', methods=['POST'])
def upload_single():
    if 'file' not in request.files:
        return jsonify({'error': '没有文件被上传'}), 400
    
    file = request.files['file']
    
    if file.filename == '':
        return jsonify({'error': '没有选择文件'}), 400
    
    if file and allowed_file(file.filename):
        # 生成唯一文件名
        filename = secure_filename(file.filename)
        unique_name = f"{uuid.uuid4().hex}_{filename}"
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_name)
        
        file.save(filepath)
        
        return jsonify({
            'success': True,
            'message': '文件上传成功',
            'file': {
                'original_name': filename,
                'saved_name': unique_name,
                'size': os.path.getsize(filepath),
                'path': filepath
            }
        })
    
    return jsonify({'error': '不支持的文件类型'}), 400

@app.route('/upload-multiple', methods=['POST'])
def upload_multiple():
    if 'files' not in request.files:
        return jsonify({'error': '没有文件被上传'}), 400
    
    files = request.files.getlist('files')
    uploaded_files = []
    
    for file in files:
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            unique_name = f"{uuid.uuid4().hex}_{filename}"
            filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_name)
            
            file.save(filepath)
            
            uploaded_files.append({
                'original_name': filename,
                'saved_name': unique_name,
                'size': os.path.getsize(filepath)
            })
    
    return jsonify({
        'success': True,
        'message': f'成功上传 {len(uploaded_files)} 个文件',
        'files': uploaded_files
    })

@app.route('/upload-chunk', methods=['POST'])
def upload_chunk():
    file_id = request.form.get('fileId')
    chunk_index = request.form.get('chunkIndex')
    total_chunks = request.form.get('totalChunks')
    file_name = request.form.get('fileName')
    
    if 'chunk' not in request.files:
        return jsonify({'error': '没有分块数据'}), 400
    
    chunk = request.files['chunk']
    
    # 创建分块目录
    chunk_dir = os.path.join(CHUNK_FOLDER, file_id)
    os.makedirs(chunk_dir, exist_ok=True)
    
    # 保存分块
    chunk_path = os.path.join(chunk_dir, f'chunk_{chunk_index}')
    chunk.save(chunk_path)
    
    # 检查是否所有分块都已上传
    uploaded_chunks = len([f for f in os.listdir(chunk_dir) if f.startswith('chunk_')])
    
    if uploaded_chunks == int(total_chunks):
        # 所有分块上传完成,合并文件
        output_path = os.path.join(UPLOAD_FOLDER, file_name)
        
        with open(output_path, 'wb') as output_file:
            for i in range(int(total_chunks)):
                chunk_path = os.path.join(chunk_dir, f'chunk_{i}')
                with open(chunk_path, 'rb') as chunk_file:
                    output_file.write(chunk_file.read())
        
        # 清理临时分块
        import shutil
        shutil.rmtree(chunk_dir)
        
        return jsonify({
            'success': True,
            'message': '文件合并完成',
            'file_path': output_path
        })
    
    return jsonify({
        'success': True,
        'message': f'分块 {chunk_index} 上传成功,等待其他分块'
    })

if __name__ == '__main__':
    app.run(debug=True, port=5000)

四、现代替代方案的比较

4.1 功能对比表

特性 SWFUpload HTML5 File API Uppy Dropzone.js
依赖 Flash插件 无插件 无插件 无插件
多文件选择
进度监控
断点续传 需手动实现 ✓(插件) ✓(插件)
拖拽上传
移动端支持
文件预览
批量上传
文件类型限制
文件大小限制
自定义UI 有限 完全自定义 高度可定制 可定制
社区支持 已停止 活跃 活跃 活跃
学习曲线 中等 中等

4.2 性能对比

指标 SWFUpload HTML5 File API Uppy Dropzone.js
启动速度 慢(需加载Flash) 中等
上传速度 中等
内存占用 中等
浏览器兼容性 IE6+ IE10+ IE11+ IE10+
安全性 低(Flash漏洞)

五、最佳实践建议

5.1 选择合适的方案

  1. 简单需求:使用HTML5 File API + 原生JavaScript
  2. 中等复杂度:使用Dropzone.js
  3. 企业级应用:使用Uppy,它提供了最全面的功能和插件系统
  4. 移动端优先:确保使用HTML5标准API,避免任何插件依赖

5.2 安全考虑

  1. 文件类型验证:前端和后端都要验证文件类型
  2. 文件大小限制:设置合理的文件大小限制
  3. 文件名处理:使用安全的文件名处理,防止路径遍历攻击
  4. 病毒扫描:对上传的文件进行病毒扫描
  5. 存储安全:将上传文件存储在Web根目录之外

5.3 性能优化

  1. 分块上传:对于大文件(>10MB),使用分块上传
  2. 并发控制:限制并发上传数量,避免服务器过载
  3. 进度反馈:提供清晰的进度反馈,提升用户体验
  4. 错误处理:完善的错误处理和重试机制
  5. CDN加速:使用CDN加速文件上传和下载

5.4 用户体验优化

  1. 拖拽支持:支持拖拽上传,提升交互体验
  2. 文件预览:上传前显示文件预览
  3. 批量操作:支持批量上传、批量删除
  4. 响应式设计:确保在移动设备上正常使用
  5. 无障碍访问:支持键盘操作和屏幕阅读器

六、未来趋势

6.1 WebAssembly技术

WebAssembly(Wasm)为Web带来了接近原生的性能,未来可能用于实现更高效的文件处理:

// 示例:使用WebAssembly处理大文件(概念性代码)
// 实际应用需要编译C/C++/Rust代码为Wasm模块

// 加载Wasm模块
const wasmModule = await WebAssembly.instantiateStreaming(
    fetch('file-processor.wasm')
);

// 使用Wasm函数处理文件
function processLargeFile(file) {
    const reader = new FileReader();
    reader.onload = function(e) {
        const arrayBuffer = e.target.result;
        const uint8Array = new Uint8Array(arrayBuffer);
        
        // 调用Wasm函数处理数据
        const result = wasmModule.instance.exports.processFile(
            uint8Array.byteOffset,
            uint8Array.length
        );
        
        console.log('Wasm处理结果:', result);
    };
    reader.readAsArrayBuffer(file);
}

6.2 Service Worker与离线上传

Service Worker可以实现离线上传和后台上传:

// 注册Service Worker
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw-upload.js')
        .then(registration => {
            console.log('Service Worker注册成功:', registration);
        })
        .catch(error => {
            console.error('Service Worker注册失败:', error);
        });
}

// 在Service Worker中处理上传队列
// sw-upload.js
self.addEventListener('fetch', event => {
    if (event.request.url.includes('/upload')) {
        event.respondWith(
            caches.match(event.request).then(response => {
                if (response) {
                    return response;
                }
                
                // 网络请求失败时,将请求存入IndexedDB
                return event.request.clone().text().then(body => {
                    saveToUploadQueue(event.request.url, body);
                    return new Response('上传已排队,将在网络恢复后自动上传');
                });
            })
        );
    }
});

6.3 云存储集成

现代应用越来越多地直接上传到云存储服务:

// 直接上传到AWS S3(使用预签名URL)
async function uploadToS3(file, presignedUrl) {
    const response = await fetch(presignedUrl, {
        method: 'PUT',
        body: file,
        headers: {
            'Content-Type': file.type
        }
    });
    
    if (response.ok) {
        console.log('文件上传到S3成功');
        return true;
    } else {
        console.error('上传到S3失败');
        return false;
    }
}

// 生成预签名URL(服务器端)
// Node.js示例
const AWS = require('aws-sdk');
const s3 = new AWS.S3();

function generatePresignedUrl(fileName, fileType) {
    const params = {
        Bucket: 'your-bucket-name',
        Key: `uploads/${Date.now()}_${fileName}`,
        ContentType: fileType,
        Expires: 3600, // 1小时过期
        ACL: 'public-read'
    };
    
    return s3.getSignedUrl('putObject', params);
}

七、总结

SWFUpload作为Web开发历史上的重要技术,为早期的文件上传功能做出了巨大贡献。然而,随着HTML5标准的普及和浏览器安全策略的演进,SWFUpload已经完成了它的历史使命。现代Web开发提供了更强大、更安全、更易用的文件上传解决方案。

对于新项目,强烈建议使用HTML5 File API作为基础,根据需求选择合适的库(如Uppy或Dropzone.js)。这些现代方案不仅功能更强大,而且具有更好的性能、安全性和用户体验。

在选择技术方案时,开发者需要综合考虑项目需求、目标用户、浏览器兼容性、安全要求和性能指标。随着Web技术的不断发展,文件上传功能将继续演进,为用户带来更加流畅和智能的体验。