FormData 深入讲解教程

FormData 是 HTML5 新增的内置对象,用于以键值对的形式封装表单数据,支持文件上传,可通过 XMLHttpRequest 或 Fetch API 异步提交,是前端处理表单数据(尤其是文件上传)的核心工具。本文从基础到进阶,全面讲解 FormData 的使用、原理和实战技巧。

一、FormData 核心特性

  1. 兼容性:支持所有现代浏览器(Chrome、Firefox、Edge、Safari 10+),IE10 及以上部分支持(需注意文件上传兼容性)。
  2. 核心能力
    • 模拟表单提交(multipart/form-data 编码);
    • 动态添加/删除键值对,无需手动拼接字符串;
    • 支持文件/Blob 类型数据上传;
    • 可与 XMLHttpRequest、Fetch、Axios 无缝配合。
  3. 编码类型 :默认采用 multipart/form-data(表单上传文件的标准编码),区别于 application/x-www-form-urlencoded(普通表单默认编码,不支持文件)。

二、基础用法

1. 创建 FormData 对象

方式1:空对象初始化

创建空的 FormData,手动添加键值对:

javascript 复制代码
// 创建空 FormData
const formData = new FormData();

// 添加普通键值对(字符串/数字)
formData.append('username', 'zhangsan');
formData.append('age', 25);

// 添加布尔值(会自动转为字符串 "true"/"false")
formData.append('isVip', true);
方式2:从 DOM 表单初始化

直接基于已有的 <form> 元素创建,自动封装所有表单字段:

html 复制代码
<!-- HTML 表单 -->
<form id="myForm">
  <input type="text" name="username" value="lisi">
  <input type="number" name="age" value="30">
  <input type="file" name="avatar"> <!-- 文件上传字段 -->
  <input type="checkbox" name="hobby" value="coding" checked>
  <input type="radio" name="gender" value="male" checked>
</form>
javascript 复制代码
// 从表单元素初始化 FormData
const form = document.getElementById('myForm');
const formData = new FormData(form);

// 此时 formData 已包含所有表单字段的键值对

2. 核心方法

FormData 提供了一套方法操作键值对,常用如下:

方法 作用 示例
append(key, value) 添加键值对(支持重复键) formData.append('hobby', 'reading')
set(key, value) 设置键值对(覆盖已有值) formData.set('username', 'wangwu')
get(key) 获取指定键的第一个值 formData.get('username') // "wangwu"
getAll(key) 获取指定键的所有值(数组) formData.getAll('hobby') // ["coding", "reading"]
delete(key) 删除指定键的所有值 formData.delete('age')
has(key) 判断是否存在指定键 formData.has('gender') // true
entries() 返回迭代器,遍历所有键值对 for (let [k, v] of formData.entries()) {}
keys() 返回迭代器,遍历所有键 for (let k of formData.keys()) {}
values() 返回迭代器,遍历所有值 for (let v of formData.values()) {}
关键区别:append vs set
  • append:允许同一个键对应多个值(如多选框),不会覆盖已有值;
  • set:会覆盖指定键的所有已有值,最终仅保留最新值。

示例:

javascript 复制代码
const formData = new FormData();
formData.append('tag', 'js');
formData.append('tag', 'html');
console.log(formData.getAll('tag')); // ["js", "html"]

formData.set('tag', 'css');
console.log(formData.getAll('tag')); // ["css"]

3. 遍历 FormData

FormData 是可迭代对象,支持多种遍历方式:

javascript 复制代码
const formData = new FormData();
formData.append('name', '张三');
formData.append('age', 20);

// 方式1:for...of 遍历 entries
for (const [key, value] of formData) {
  console.log(`${key}: ${value}`);
}

// 方式2:forEach
formData.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});

// 方式3:遍历 keys/values
for (const key of formData.keys()) {
  console.log('键:', key);
}
for (const value of formData.values()) {
  console.log('值:', value);
}

三、文件上传(核心场景)

FormData 最核心的用途是文件上传,支持单文件、多文件、大文件分片上传。

1. 单文件上传

步骤1:HTML 布局
html 复制代码
<input type="file" id="fileInput" accept="image/*">
<button id="uploadBtn">上传文件</button>
步骤2:JS 处理上传
javascript 复制代码
const fileInput = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn');

uploadBtn.addEventListener('click', async () => {
  // 获取选中的文件
  const file = fileInput.files[0];
  if (!file) {
    alert('请选择文件');
    return;
  }

  // 创建 FormData 并添加文件
  const formData = new FormData();
  formData.append('file', file); // 键名 "file" 需与后端接口约定
  formData.append('desc', '用户头像'); // 可同时添加其他参数

  try {
    // 发送请求(Fetch API)
    const response = await fetch('/api/upload', {
      method: 'POST',
      body: formData, // FormData 作为 body,自动设置 Content-Type: multipart/form-data
      // 无需手动设置 Content-Type,浏览器会自动添加边界符(boundary)
    });

    const result = await response.json();
    console.log('上传成功:', result);
  } catch (error) {
    console.error('上传失败:', error);
  }
});

2. 多文件上传

只需将 input 的 multiple 属性设为 true,然后遍历 files 数组添加:

html 复制代码
<input type="file" id="multiFileInput" multiple accept="image/*">
<button id="multiUploadBtn">批量上传</button>
javascript 复制代码
const multiFileInput = document.getElementById('multiFileInput');
const multiUploadBtn = document.getElementById('multiUploadBtn');

multiUploadBtn.addEventListener('click', async () => {
  const files = multiFileInput.files;
  if (files.length === 0) {
    alert('请选择文件');
    return;
  }

  const formData = new FormData();
  // 遍历多文件,添加到同一个键(后端接收数组)
  for (let i = 0; i < files.length; i++) {
    formData.append('files', files[i]); // 键名统一为 "files"
  }

  // 发送请求
  const response = await fetch('/api/multi-upload', {
    method: 'POST',
    body: formData,
  });

  const result = await response.json();
  console.log('批量上传成功:', result);
});

3. 上传进度监控

通过 XMLHttpRequest 可监控文件上传进度(Fetch API 需结合 ReadableStream,较复杂):

javascript 复制代码
const fileInput = document.getElementById('fileInput');

fileInput.addEventListener('change', () => {
  const file = fileInput.files[0];
  if (!file) return;

  const formData = new FormData();
  formData.append('file', file);

  const xhr = new XMLHttpRequest();
  xhr.open('POST', '/api/upload');

  // 监控上传进度
  xhr.upload.addEventListener('progress', (e) => {
    if (e.lengthComputable) {
      const progress = (e.loaded / e.total) * 100;
      console.log(`上传进度:${progress.toFixed(2)}%`);
    }
  });

  // 上传完成回调
  xhr.addEventListener('load', () => {
    if (xhr.status >= 200 && xhr.status < 300) {
      console.log('上传成功:', JSON.parse(xhr.responseText));
    } else {
      console.error('上传失败');
    }
  });

  // 发送请求
  xhr.send(formData);
});

四、与主流库配合使用

1. Axios 中使用 FormData

Axios 会自动识别 FormData,无需手动设置 Content-Type:

javascript 复制代码
import axios from 'axios';

// 单文件上传
async function uploadFile(file) {
  const formData = new FormData();
  formData.append('file', file);
  formData.append('name', '测试文件');

  try {
    const response = await axios.post('/api/upload', formData, {
      // 监控上传进度
      onUploadProgress: (e) => {
        const progress = (e.loaded / e.total) * 100;
        console.log(`进度:${progress}%`);
      },
      headers: {
        // 无需设置 Content-Type,Axios 会自动添加 boundary
        // "Content-Type": "multipart/form-data"
      }
    });
    return response.data;
  } catch (error) {
    console.error('上传失败:', error);
  }
}

2. React/Vue 中使用 FormData

以 React 为例(Vue 逻辑一致):

jsx 复制代码
import React, { useRef } from 'react';
import axios from 'axios';

function UploadComponent() {
  const fileInputRef = useRef(null);

  const handleUpload = async () => {
    const file = fileInputRef.current.files[0];
    if (!file) return;

    const formData = new FormData();
    formData.append('file', file);

    await axios.post('/api/upload', formData);
  };

  return (
    <div>
      <input type="file" ref={fileInputRef} />
      <button onClick={handleUpload}>上传</button>
    </div>
  );
}

五、常见问题与解决方案

1. 手动设置 Content-Type 导致上传失败

问题 :手动设置 Content-Type: multipart/form-data 后,浏览器不会自动添加边界符(boundary),后端无法解析。
解决方案:不手动设置 Content-Type,让浏览器/Axios 自动生成(包含 boundary)。

2. 文件大小限制

问题 :大文件上传超时/失败。
解决方案

  • 前端:分片上传(将文件切分成多个 Blob,分批上传);
  • 后端:配置文件大小限制(如 Node.js/Express 需设置 express-fileuploadlimits)。

3. 跨域上传文件

问题 :跨域时请求被拦截。
解决方案

  • 后端配置 CORS:允许 Content-Type: multipart/form-data,并允许 OPTIONS 预检请求;
  • 前端请求时无需额外配置(Fetch/Axios 自动处理)。

4. FormData 无法打印/调试

问题console.log(formData) 只能看到空对象,无法直接查看内容。
解决方案

javascript 复制代码
// 方式1:遍历打印
formData.forEach((v, k) => console.log(k, v));

// 方式2:转为对象(仅适用于非文件类型,文件会显示 [object File])
const formDataObj = Object.fromEntries(formData.entries());
console.log(formDataObj);

六、进阶技巧

1. 分片上传大文件

核心思路:将文件按固定大小切分,分批上传,后端合并:

javascript 复制代码
async function sliceUpload(file) {
  const chunkSize = 1024 * 1024; // 1MB 每片
  const totalChunks = Math.ceil(file.size / chunkSize);
  const fileHash = Date.now() + '-' + file.name; // 唯一标识文件

  for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
    // 切分文件
    const start = chunkIndex * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);

    // 创建 FormData
    const formData = new FormData();
    formData.append('chunk', chunk);
    formData.append('chunkIndex', chunkIndex);
    formData.append('totalChunks', totalChunks);
    formData.append('fileHash', fileHash);
    formData.append('fileName', file.name);

    // 上传分片
    await fetch('/api/upload-chunk', {
      method: 'POST',
      body: formData,
    });
  }

  // 所有分片上传完成,通知后端合并
  await fetch('/api/merge-chunk', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ fileHash, fileName, totalChunks }),
  });
}

2. 自定义 Blob 上传

除了文件,还可以上传自定义 Blob(如生成的文本、图片):

javascript 复制代码
// 上传自定义文本 Blob
const textBlob = new Blob(['Hello FormData'], { type: 'text/plain' });
const formData = new FormData();
formData.append('textFile', textBlob, 'custom.txt'); // 第三个参数为文件名

fetch('/api/upload', {
  method: 'POST',
  body: formData,
});

七、后端接收示例(Node.js/Express)

以 Express 为例,使用 multer 处理 FormData 上传的文件:

javascript 复制代码
const express = require('express');
const multer = require('multer');
const app = express();

// 配置文件存储路径
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, './uploads'); // 上传文件保存目录
  },
  filename: (req, file, cb) => {
    cb(null, Date.now() + '-' + file.originalname); // 重命名文件
  }
});

const upload = multer({ storage });

// 单文件上传接口
app.post('/api/upload', upload.single('file'), (req, res) => {
  // req.file 是上传的文件信息
  // req.body 是 FormData 中的其他参数(如 desc)
  res.json({
    code: 200,
    msg: '上传成功',
    file: req.file,
    body: req.body
  });
});

// 多文件上传接口
app.post('/api/multi-upload', upload.array('files', 5), (req, res) => {
  // req.files 是多文件数组
  res.json({
    code: 200,
    msg: '批量上传成功',
    files: req.files
  });
});

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

八、总结

FormData 是前端处理表单数据(尤其是文件上传)的核心工具,核心要点:

  1. 优先使用 append 添加数据,set 用于覆盖;
  2. 上传文件时无需手动设置 Content-Type,避免丢失 boundary;
  3. 结合 XMLHttpRequest 监控上传进度,结合 Axios 简化请求;
  4. 大文件上传推荐分片策略,解决超时/失败问题;
  5. 后端需对应配置文件解析(如 Express + multer)。
相关推荐
password-u1 年前
axios get 请求发送 FormData 数据
axios·get·formdata
1024小神2 年前
flutter3使用dio库发送FormData数据格式时候的坑,和get库冲突解决办法
flutter·dio·formdata