JS如何实现多文件上传

来源:Reactjs教程作者:天穹小白头衔:草根站长
导读:本期聚焦于天穹小白创作的《JS如何实现多文件上传》,敬请观看详情。想要实现多文件批量上传的功能,却不知道从哪儿入手?核心思路是先通过input元素的multiple属性让用户选择多个文件,再利用File API获取文件列表做预处理,最后借助FormData把文件数据打包发送到后端。前端需要处理文件类型校验、大小限制、上传进度展示等细节,后端则要接收multipart/form-data格式的数据完成存储。这种方式不需要依赖第三方库,原生JS就能实现,适配绝大多数现代浏览器,还能灵活扩展分片上传、断点续传等高级能力。

多文件上传的基础实现原理

多文件上传的核心依赖浏览器提供的两个原生能力,一个是<input type="file">标签的multiple属性,另一个是File API。当用户点击文件选择框时,multiple属性允许用户在系统的文件选择器中按住Ctrl或者Shift键选中多个文件,选中的文件会存储在input元素的files属性中,这个属性是一个FileList对象,本质上是一个类数组结构,包含了所有选中文件的基本信息,比如文件名、大小、类型、最后修改时间等。

拿到FileList之后,我们不能直接把它发送到后端,因为普通的表单提交或者AJAX请求默认只能处理字符串类型的数据,二进制文件需要被包装成合适的数据格式。这里就需要用到FormData对象,它可以模拟表单的提交行为,支持同时添加多个文件字段和普通字段,浏览器会自动把FormData的内容编码成multipart/form-data格式,这是后端接收文件时最常用的数据格式。整个流程可以概括为:用户选择多文件 → 前端获取文件列表 → 校验文件合法性 → 组装FormData → 发送AJAX请求 → 后端接收处理。

需要注意的是,File API是HTML5新增的特性,IE9及以下版本的浏览器不支持,如果需要兼容旧浏览器,只能让用户逐个选择文件,或者引导用户升级浏览器。另外,multiple属性只影响用户的文件选择行为,并不会自动把文件打包发送,所有的处理逻辑都需要开发者手动编写,这也是我们需要用JS实现多文件上传的核心原因。

原生JS实现多文件上传的完整示例

下面我们来实现一个完整的多文件上传功能,包含前端选择和上传、后端接收的基础逻辑。首先是前端的HTML结构,需要有一个文件选择框、一个上传按钮,以及用来展示上传状态和结果的区域:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>多文件上传示例</title>
</head>
<body>
    <input type="file" id="fileInput" multiple accept="image/*,.pdf">
    <button id="uploadBtn">开始上传</button>
    <div id="progressContainer"></div>
    <div id="resultContainer"></div>
    <script src="upload.js"></script>
</body>
</html>

JS如何实现多文件上传

这里的accept属性用来限制用户可以选择的文件类型,image/*表示所有图片类型,.pdf表示只允许PDF文件,用户选择不符合类型的文件时会直接被系统过滤器过滤,无法选中。接下来是核心的JS逻辑,写在upload.js中:

const fileInput = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn');
const progressContainer = document.getElementById('progressContainer');
const resultContainer = document.getElementById('resultContainer');

// 点击上传按钮触发逻辑
uploadBtn.addEventListener('click', async () => {
    const files = fileInput.files;
    // 没有选择文件时直接返回
    if (files.length === 0) {
        alert('请先选择要上传的文件');
        return;
    }
    // 遍历文件列表做基础校验
    for (let i = 0; i < files.length; i++) {
        const file = files[i];
        // 限制单个文件大小不超过2MB
        if (file.size > 2 * 1024 * 1024) {
            alert(`文件${file.name}大小超过2MB,无法上传`);
            return;
        }
    }
    // 创建FormData对象,添加所有文件
    const formData = new FormData();
    // 给每个文件添加相同的字段名,后端可以用数组接收
    for (let i = 0; i < files.length; i++) {
        formData.append('uploadFiles', files[i]);
    }
    // 也可以添加其他的表单字段,比如用户ID
    formData.append('userId', '10001');
    
    // 创建XMLHttpRequest对象,监听上传进度
    const xhr = new XMLHttpRequest();
    // 监听上传进度事件
    xhr.upload.addEventListener('progress', (e) => {
        if (e.lengthComputable) {
            const percent = Math.round((e.loaded / e.total) * 100);
            progressContainer.innerHTML = `上传进度:${percent}%`;
        }
    });
    // 监听请求完成事件
    xhr.addEventListener('load', () => {
        if (xhr.status === 200) {
            const res = JSON.parse(xhr.responseText);
            resultContainer.innerHTML = `上传成功,共上传${res.fileCount}个文件`;
        } else {
            resultContainer.innerHTML = `上传失败,错误码:${xhr.status}`;
        }
    });
    // 监听错误事件
    xhr.addEventListener('error', () => {
        resultContainer.innerHTML = '网络错误,上传失败';
    });
    // 发送请求,后端接口地址假设为/upload
    xhr.open('POST', 'https://ipipp.com/upload');
    xhr.send(formData);
});

上面的代码使用了XMLHttpRequest来发送请求,主要是因为它原生支持upload对象的progress事件,可以精准获取上传的进度,这是Fetch API目前还不具备的能力。如果你不需要进度展示,也可以用Fetch API来发送FormData,代码会更简洁一些。比如把发送部分替换为:

fetch('https://ipipp.com/upload', {
    method: 'POST',
    body: formData
})
.then(res => res.json())
.then(data => {
    resultContainer.innerHTML = `上传成功,共上传${data.fileCount}个文件`;
})
.catch(err => {
    resultContainer.innerHTML = '上传失败,请重试';
});

后端接收的部分以Node.js的Express框架为例,需要使用multer中间件来处理multipart/form-data格式的数据,具体代码如下:

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

const app = express();
// 配置multer存储,文件保存到uploads目录
const storage = multer.diskStorage({
    destination: (req, file, cb) => {
        cb(null, 'uploads/');
    },
    filename: (req, file, cb) => {
        // 给文件加上时间戳避免重名
        const uniqueName = Date.now() + '-' + file.originalname;
        cb(null, uniqueName);
    }
});
const upload = multer({ storage: storage });

// 处理多文件上传的接口,字段名和前端FormData的append第一个参数一致
app.post('/upload', upload.array('uploadFiles', 10), (req, res) => {
    // req.files就是所有上传的文件信息数组
    console.log('上传的文件:', req.files);
    console.log('其他字段:', req.body.userId);
    res.json({
        code: 0,
        msg: '上传成功',
        fileCount: req.files.length
    });
});

app.listen(3000, () => {
    console.log('服务启动在3000端口');
});

多文件上传的常见优化与问题处理

基础的上传功能虽然能跑通,但在实际生产环境中还需要处理很多细节问题。首先是文件校验的完善,除了大小限制,还可以校验文件类型,因为accept属性只是前端的限制,用户可以通过修改请求绕过,所以后端也需要做类型校验,前端也可以在拿到File对象后通过file.type属性判断,不过这个属性也不是完全可信,更稳妥的方式是校验文件的魔术数字,也就是文件开头的二进制标识,但前端处理二进制相对麻烦,一般前端做基础校验,后端做严格校验即可。

其次是上传的并发控制,如果用户一次选择几十个甚至上百个文件,全部同时发送会给服务器造成很大压力,也可能导致浏览器同时发起的请求数量超过限制,部分请求会被阻塞。这时候可以实现一个简单的并发控制逻辑,比如最多同时上传3个文件,一个上传完成后再启动下一个。我们可以把文件列表改造成队列,用计数器记录当前正在上传的数量,当计数器小于并发数且有未上传的文件时,就取出下一个文件发起上传请求。

// 并发控制上传函数,max是最大并发数
function uploadWithConcurrency(files, max = 3) {
    return new Promise((resolve) => {
        let index = 0; // 当前待上传的文件索引
        let finished = 0; // 已完成的上传数量
        const total = files.length;
        const result = []; // 存储每个文件的上传结果
        
        // 启动上传的函数
        function startUpload() {
            // 如果所有文件都已经处理完,就结束
            if (index >= total && finished >= total) {
                resolve(result);
                return;
            }
            // 如果当前并发数没满,且还有未上传的文件,就启动新的上传
            while (index < total && finished + (index - finished) < max) {
                const currentIndex = index;
                const file = files[currentIndex];
                index++;
                // 单个文件的上传逻辑
                const formData = new FormData();
                formData.append('uploadFiles', file);
                fetch('https://ipipp.com/upload', {
                    method: 'POST',
                    body: formData
                })
                .then(res => res.json())
                .then(data => {
                    result[currentIndex] = { success: true, data };
                    finished++;
                    startUpload(); // 当前文件上传完成,启动下一个
                })
                .catch(err => {
                    result[currentIndex] = { success: false, error: err };
                    finished++;
                    startUpload();
                });
            }
        }
        startUpload();
    });
}

// 使用方式
uploadBtn.addEventListener('click', async () => {
    const files = fileInput.files;
    if (files.length === 0) return;
    const results = await uploadWithConcurrency(files, 3);
    console.log('所有文件上传完成:', results);
});

另外还需要处理上传失败的重试逻辑,比如某个文件因为网络波动上传失败了,可以自动重试2-3次,再失败才提示用户。还有大文件的分片上传,如果单个文件超过几百MB,直接上传很容易失败,这时候需要把文件切成多个小片段,分别上传,后端再合并所有片段,这部分逻辑相对复杂,需要前后端配合实现,但核心还是基于FormData和File API的slice方法,把大文件切割成小的Blob对象再逐个发送。

最后要注意跨域问题,如果前端和后端不在同一个域下,后端需要设置CORS相关的响应头,允许前端域名的请求,否则浏览器会拦截响应。另外,FormData在发送的时候不需要手动设置Content-Type请求头,浏览器会自动设置正确的multipart/form-data类型和对应的边界标识,如果手动设置了反而可能导致后端无法正确解析数据。

多文件上传File APIFormData修改时间:2026-08-19 16:41:11

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。