Axios multipart/form-data 完整指南:FormData 自动序列化、formSerializer 配置与 formToJSON 反向转换
【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios
本文围绕 Axios 的multipart/form-data发送能力展开:从浏览器与 Node.js 环境下的基础提交,到 v0.27.0 引入的对象自动序列化机制、formSerializer各配置项(dots、metaTokens、indexes、maxDepth等)的取值与行为,再到 Node.js 下formDataHeaderPolicy头安全策略与formToJSON()的反向解析。读完你可以完整掌握 Axios 表单序列化的规则细节,并结合 toFormData.js、setFormDataHeaders.js、formDataToJSON.js 三处源码理解其底层实现。
基础提交:浏览器与 Node.js 两种写法
Axios 支持发送multipart/form-data格式的请求,该格式常用于文件上传场景。基本思路是:构造一个FormData对象并append数据,再将其作为data传给 axios 请求配置。
浏览器端直接使用原生FormData:
const formData = new FormData(); formData.append('foo', 'bar'); axios.post('https://httpbin.org/post', formData);不要手动设置Content-Type头。对于浏览器、Web Worker 或 React Native 环境中的FormData对象,环境本身会自动添加 multipart boundary(边界串),手动设置反而会破坏请求体格式。
在 Node.js 中通常使用社区form-data包(Axios 自身的 polyfill 也基于它):
const FormData = require('form-data'); const form = new FormData(); form.append('my_field', 'my value'); form.append('my_buffer', Buffer.alloc(10)); form.append('my_file', fs.createReadStream('/foo/bar.jpg')); axios.post('https://example.com', form);自动序列化为 FormData(v0.27.0+)
自 v0.27.0 起,当请求的Content-Type头被设置为multipart/form-data时,Axios 会自动把普通 JavaScript 对象序列化为FormData对象——你无需手工 append 任何字段:
import axios from 'axios'; axios .post( 'https://httpbin.org/post', { x: 1 }, { headers: { 'Content-Type': 'multipart/form-data', }, } ) .then(({ data }) => console.log(data));在 Node.js 版本中默认使用form-datapolyfill;你也可以通过配置项env.FormData替换所用的 FormData 类,尽管大多数情况下没有必要:
const axios = require('axios'); var FormData = require('form-data'); axios .post( 'https://httpbin.org/post', { x: 1, buf: Buffer.alloc(10) }, { headers: { 'Content-Type': 'multipart/form-data', }, } ) .then(({ data }) => console.log(data));从源码看,这条“自动序列化”链路位于默认请求转换器 defaults/index.js:当data是对象且Content-Type包含multipart/form-data(或data本身是 FileList)时,会调用toFormData(),并从this.env.FormData中取出自定义类实例作为容器:
// lib/defaults/index.js const formSerializer = own(this, 'formSerializer'); // ... if ( (isFileList = utils.isFileList(data)) || contentType.indexOf('multipart/form-data') > -1 ) { const env = own(this, 'env'); const _FormData = env && env.FormData; return toFormData( isFileList ? { 'files[]': data } : data, _FormData && new _FormData(), formSerializer ); }两个值得注意的实现细节:其一,纯 FileList 会被包装成{ 'files[]': data }后再序列化,因此文件批量上传默认使用files[]字段名;其二,transformRequest里还有反向判断——如果data已经是FormData而 Content-Type 却是 JSON,会走formDataToJSON(data)再JSON.stringify(见 defaults/index.js),即“FormData + JSON 头”会被自动转成 JSON 发送。
Node.js FormData 的头拷贝策略 formDataHeaderPolicy(仅 Node.js)
当传入一个暴露了getHeaders()方法的 Node.jsFormData对象(如form-data包)时,Axios 默认会把它返回的全部头拷贝到请求上。这一行为保持了与 v1 的兼容性,但当FormData来自不可信来源时存在隐患:getHeaders()可能覆盖Authorization等关键头,或注入任意自定义头。
设置formDataHeaderPolicy: 'content-only'后,Axios 只从getHeaders()拷贝Content-Type和Content-Length两个头,其余请求头必须通过headers配置显式声明:
await axios.post('https://example.com/upload', form, { formDataHeaderPolicy: 'content-only', headers: { Authorization: 'Bearer my-token', }, });该配置的默认值是'legacy'(全部拷贝)。参数解析逻辑非常直白,见 setFormDataHeaders.js:
const FORM_DATA_CONTENT_HEADERS = ['content-type', 'content-length']; export default function setFormDataHeaders(headers, formHeaders, policy) { if (policy !== 'content-only') { headers.set(formHeaders); return; } Object.entries(formHeaders || {}).forEach(([key, val]) => { if (FORM_DATA_CONTENT_HEADERS.includes(key.toLowerCase())) { headers.set(key, val); } }); }调用点有两处:统一在 resolveConfig.js 中基于getHeaders()应用一次,Node 的 http 适配器 adapters/http.js 中再次应用以覆盖重定向等场景。更完整的配置说明可参考请求配置文档中的formDataHeaderPolicy小节(docs/fr/pages/advanced/request-config.md)。
特殊终结符:{}与[]
Axios 的 FormData 序列化器支持两种字段名终结符,用于表达特殊序列化操作:
{}— 将该值用JSON.stringify序列化后作为单个字符串字段发送;[]— 把数组类型的对象拆解为多个同名字段发送。
注意:对数组和FileList类型,拆解(展开为同名字段)是默认行为,无需加[]。
配置序列化器:config.formSerializer
序列化器支持通过config.formSerializer配置对象传入额外选项,用于处理各种特殊需求:
| 选项 | 类型与默认值 | 说明 |
|---|---|---|
visitor | Function | 用户自定义的访问器函数,会被递归调用以按自定义规则将数据对象序列化为 FormData |
dots | boolean = false | 序列化数组和对象时使用点号记法(user.name)替代方括号记法(user[name]) |
metaTokens | boolean = true | 在字段名中保留特殊终结符(如user{}: '{"name": "John"}')。后端 body-parser 可利用这些元信息自动按 JSON 解析该值 |
indexes | null \| false \| true = false | 控制为展开后的数组字段追加何种索引:null— 不加括号(arr: 1、arr: 2、arr: 3);false(默认)— 加空括号(arr[]: 1…);true— 加真实下标(arr[0]: 1、arr[1]: 2、arr[2]: 3) |
maxDepth | number = 100 | 序列化递归的最大对象嵌套深度。超限抛出 code 为ERR_FORM_DATA_DEPTH_EXCEEDED的AxiosError,用于防御服务端通过深层嵌套负载发起的 DoS 攻击;设为Infinity可关闭限制 |
Blob | typeof Blob | 将ArrayBuffer值转换为符合规范的FormData时使用的 Blob 构造函数。仅在你的运行时以其他标识符提供了兼容 Blob 构造器时才需要替换 |
例如,对确实会超过 100 层嵌套的数据结构放宽限制:
// 允许更深的嵌套,用于确实超过 100 层的数据结构 axios.postForm('/api', data, { formSerializer: { maxDepth: 200 } });安全说明:默认 100 层上限是有意为之。服务端把客户端可控的 JSON 透传给 axios 作为data的代码,缺少该保护时会面临调用栈溢出风险;仅在你的数据结构确实需要时才调高maxDepth。
源码视角:序列化器内部如何工作
核心实现位于 toFormData.js。默认上限与反向转换共享同一个常量,保证 FormData 与 JSON 互转的深度语义对称(toFormData.js):
// lib/helpers/toFormData.js export const DEFAULT_FORM_DATA_MAX_DEPTH = 100;几个关键机制:
- 深度保护:
build()在每次递归前调用throwIfMaxDepthExceeded(depth)检查层级,超限即抛出带ERR_FORM_DATA_DEPTH_EXCEEDEDcode 的AxiosError(toFormData.js);对{}终结符触发的JSON.stringify也内置了等价的深度限制stringifyWithDepthLimit()。 - 循环引用检测:
build()维护一个stack,遇到重复对象立即抛出Circular reference detected错误,避免死递归(toFormData.js)。 indexes三分支:平铺数组展开时的字段名生成就写死在defaultVisitor中——indexes === true走renderKey([key], index, dots)生成arr[0],indexes === null直接用key,其余情况(默认false)拼key + '[]'(toFormData.js)。metaTokens:{}终结符在metaTokens为true时原样保留在字段名里,否则被key.slice(0, -2)剥掉(toFormData.js)。dots:字段路径渲染由renderKey(path, key, dots)完成,dots为真时用.连接,否则用方括号包裹(toFormData.js)。- 值类型转换:
Date转为 ISO 字符串、布尔转字符串、ArrayBuffer/TypedArray 在有规范兼容Blob时转 Blob、在 Node 有 Buffer 时转 Buffer(convertValue,toFormData.js)。 visitor扩展点:自定义 visitor 以 FormData 为this被调用,返回true表示继续递归遍历该值;同时可访问defaultVisitor、convertValue、isVisitable及一组is*类型判断辅助函数(exposedHelpers,toFormData.js)。
序列化过程完整示例
以一个典型的嵌套对象为例:
const obj = { x: 1, arr: [1, 2, 3], arr2: [1, [2], 3], users: [ { name: 'Peter', surname: 'Griffin' }, { name: 'Thomas', surname: 'Anderson' }, ], 'obj2{}': [{ x: 1 }], };Axios 序列化器内部等价于执行了以下步骤:
const formData = new FormData(); formData.append('x', '1'); formData.append('arr[]', '1'); formData.append('arr[]', '2'); formData.append('arr[]', '3'); formData.append('arr2[0]', '1'); formData.append('arr2[1][0]', '2'); formData.append('arr2[2]', '3'); formData.append('users[0][name]', 'Peter'); formData.append('users[0][surname]', 'Griffin'); formData.append('users[1][name]', 'Thomas'); formData.append('users[1][surname]', 'Anderson'); formData.append('obj2{}', '[{"x":1}]');规则可以归纳为:纯标量直接追加;无嵌套的平铺数组默认展开为key[]同名多值;混合数组按真实下标生成路径(arr2[1][0]);对象数组递归展开为users[0][name]这类路径;{}终结符的值整体JSON.stringify成单字段。
反向转换:axios.formToJSON()
axios.formToJSON()能把字段名中的点号与方括号记法还原为嵌套的对象和数组结构。只有.、[、]是结构性分隔符,其余字符(如-、空格、+、*、&)都会原样保留在字面量键中:
const form = new FormData(); form.append('user-name', 'johndoe'); form.append('user.name', 'john'); console.log(axios.formToJSON(form)); // { // 'user-name': 'johndoe', // user: { name: 'john' } // }user[name]同样会产生嵌套对象路径,items[]则产生数组。
实现见 formDataToJSON.js,几个实现要点:
- 路径解析使用正则
/[^.[\]]+|\[([^.[\]]*)]/g,把foo[x][y][z]拆为['foo','x','y','z'],foo.x.y.z同理;段内不含[的设计让解析保持线性复杂度(formDataToJSON.js)。 __proto__段被直接跳过,杜绝原型污染;同名重复字段会合并为数组(formDataToJSON.js)。- 数字键(
0、1…)在目标位置自动形成数组,非数字键还原为对象属性;解析同样受 100 层深度上限保护,超限抛ERR_FORM_DATA_DEPTH_EXCEEDED。 - 该函数挂在 axios 实例上,且支持直接传 HTML
<form>元素——内部会先new FormData(thing)再解析(axios.js)。对应的单元测试见 tests/unit/helpers/formDataToJSON.test.js。
toFormData本身也作为axios.toFormData暴露(axios.js),可以脱离请求流程手动序列化对象,测试用例见 tests/unit/toFormData.test.js。
便捷方法:postForm / putForm / patchForm
Axios 还提供三个快捷方法:postForm、putForm、patchForm。它们与对应的 HTTP 方法完全等价,唯一区别是预先将Content-Type头设置为multipart/form-data,因此可以直接传普通对象:
// 等价于 axios.post('/api', data, { headers: { 'Content-Type': 'multipart/form-data' } }) axios.postForm('/api', { name: 'john', file: new Blob(['...']) });这也意味着postForm系列方法天然触发前文描述的自动序列化流程,并受同一套formSerializer选项约束。
小结
- 浏览器 / Web Worker / React Native:用原生
FormData直接传data,不要手动设置Content-Type(boundary 由环境生成)。 - Node.js:使用
form-data包或依赖自动序列化;对不可信来源的 FormData,用formDataHeaderPolicy: 'content-only'收敛头拷贝范围。 - 对象自动序列化要求
Content-Type含multipart/form-data;formSerializer的visitor/dots/metaTokens/indexes/maxDepth/Blob覆盖几乎全部特殊场景,maxDepth默认 100 层是服务端 DoS 防护线。 - 终结符
{}(JSON 化)与[](字段展开)是字段级的序列化开关;formToJSON()按.、[、]三个结构性分隔符做无损逆向解析,并与序列化器共享同一深度上限。
【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考