axios HTML 表单提交实战:postForm、字段名路径记法与 JSON 转换原理
axios 除了传统的 post 方法外,还提供了专门用于表单提交的 postForm/putForm/patchForm 快捷方法,可以直接把页面上的 <form> 元素作为请求体发送,也可以在显式指定 Content-Type: application/json 时将其序列化为 JSON。本文以官方文档 HTML form posting (browser) 为主线,结合当前仓库源码,讲清楚 HTML 表单提交的三种用法、字段名路径记法(dot / bracket notation)的解析规则,以及底层 transformRequest、formDataToJSON 的完整调用链。
一、用 postForm 直接提交 HTML 表单
当页面上已经存在一个 <form> 元素时,无需手动构造 FormData,也无需编写任何取值的 JavaScript 代码,只需用 document.querySelector 拿到表单元素,传给 axios.postForm 即可:
await axios.postForm('https://your.server/api/post', document.querySelector('#htmlForm'));
postForm 的语义是"发送 multipart 表单数据"。从源码看,它并不是一个独立实现,而是在 Axios 类初始化时由 post/put/patch 三个方法统一生成的姊妹方法——生成时会额外注入一个默认的 Content-Type: multipart/form-data 请求头(见 lib/core/Axios.js):
utils.forEach(['post', 'put', 'patch', 'query'], function forEachMethodWithData(method) {
function generateHTTPMethod(isForm) {
return function httpMethod(url, data, config) {
return this.request(
mergeConfig(config || {}, {
method,
headers: isForm
? {
'Content-Type': 'multipart/form-data',
}
: {},
url,
data,
})
);
};
}
Axios.prototype[method] = generateHTTPMethod();
// query 是幂等读方法,multipart 表单体不符合其语义,因此不生成 queryForm
if (method !== 'query') {
Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
}
});
也就是说 postForm、putForm、patchForm 三者等价于对应方法 + multipart/form-data 内容类型,唯一不生成 *Form 变体的是 query(源码注释说明这是出于幂等读方法的语义考虑)。
二、HTML 表单在 transformRequest 中如何变成 FormData
postForm 只是设置了内容类型,真正把 HTMLFormElement 转成 FormData 的动作发生在默认请求转换器里(见 lib/defaults/index.js):
transformRequest: [
function transformRequest(data, headers) {
const contentType = headers.getContentType() || '';
const hasJSONContentType = contentType.indexOf('application/json') > -1;
const isObjectPayload = utils.isObject(data);
if (isObjectPayload && utils.isHTMLForm(data)) {
data = new FormData(data); // 关键:HTML 表单元素 → FormData
}
const isFormData = utils.isFormData(data);
if (isFormData) {
return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data;
}
// ...其余分支(ArrayBuffer / Blob / URLSearchParams / 对象序列化等)
},
],
这段代码解释了文档中两种提交方式的完整行为:
- 表单以 multipart 形式提交:
HTMLFormElement先被new FormData(data)转换,随后因内容类型不是 JSON 而原样返回FormData,由xhr/fetch适配器负责带上multipart/form-data的 boundary 发送; - 表单以 JSON 形式提交:若内容类型包含
application/json,则FormData会被formDataToJSON转换成普通对象后再JSON.stringify。
其中 utils.isHTMLForm 是基于 Object.prototype.toString 的 kindOfTest('HTMLFormElement') 类型探测(见 lib/utils.js),这也是该功能被标注为"仅浏览器"的原因——HTMLFormElement 只存在于浏览器环境。
三、把 FormData / HTMLForm 显式提交为 JSON
文档给出的第二种用法是:显式把 Content-Type 设为 application/json,普通 axios.post 也能直接提交 FormData 或 <form> 元素:
await axios.post('https://your.server/api/post', document.querySelector('#htmlForm'), {
headers: {
'Content-Type': 'application/json',
},
});
走的是上文 transformRequest 中 hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data 这条分支。除了随请求隐式触发,axios 还在实例上暴露了可直接调用的工具方法 axios.formToJSON(见 lib/axios.js),它接收 HTMLFormElement 或 FormData,内部同样先统一转成 FormData 再交给 formDataToJSON:
axios.formToJSON = (thing) =>
formDataToJSON(utils.isHTMLForm(thing) ? new FormData(thing) : thing);
类型定义中 formToJSON 的签名也明确支持两种输入(见 index.d.ts),而 postForm/putForm/patchForm 的 Promise 签名与普通请求方法一致(见 index.d.ts)。
四、字段名路径记法(Field-name path notation)
文档给出了一个可以直接被上述代码提交的示例表单:
<form id="htmlForm">
<input type="text" name="foo" value="1" />
<input type="text" name="deep.prop" value="2" />
<input type="text" name="deep prop spaced" value="3" />
<input type="text" name="baz" value="4" />
<input type="text" name="baz" value="5" />
<select name="user.age">
<option value="value1">Value 1</option>
<option value="value2" selected>Value 2</option>
<option value="value3">Value 3</option>
</select>
<input type="submit" value="Save" />
</form>
以 JSON 方式提交时,它会被序列化为:
{
"foo": "1",
"deep": {
"prop": "2"
},
"deep prop spaced": "3",
"baz": ["4", "5"],
"user": {
"age": "value2"
}
}
这段示例恰好覆盖了 lib/helpers/formDataToJSON.js 中 parsePropPath 的全部解析规则:
function parsePropPath(name) {
// foo[x][y][z] -> ['foo', 'x', 'y', 'z']
// foo.x.y.z -> ['foo', 'x', 'y', 'z']
const path = [];
const pattern = /[^.[\]]+|\[([^.[\]]*)]/g;
let match;
while ((match = pattern.exec(name)) !== null) {
throwIfDepthExceeded(path.length);
path.push(match[0] === '[]' ? '' : match[1] || match[0]);
}
return path;
}
规则逐条对应文档中的 tip:
- 只有
.和[...]会创建嵌套属性路径:deep.prop被拆成['deep', 'prop'],最终生成{"deep": {"prop": "2"}};user.age同理生成{"user": {"age": "value2"}}; - 空格、
-、+、*、&等字符保留为字面字段名的一部分:deep prop spaced不含.或括号,整体就是顶层 key;源码注释也明确说明user-name、user name这类 key 会被原样保留; - 重复的 name 合并为数组:两个
baz输入框被合并为"baz": ["4", "5"]。这一行为发生在formDataToJSON内部的buildPath函数中——当目标 key 已存在时,若原值已是数组则concat,否则包装成[旧值, 新值](见 lib/helpers/formDataToJSON.js); []语义:foo[]中的[]段被解析为空字符串段,buildPath里空段配合目标为数组时会取target.length作为下标,实现数组追加。
值得注意的还有源码中两个健壮性设计:
- 嵌套深度保护:
parsePropPath和buildPath都会调用throwIfDepthExceeded,超过DEFAULT_FORM_DATA_MAX_DEPTH(与toFormData共享的默认嵌套上限,见 lib/helpers/toFormData.js 的导出)会抛出AxiosError(错误码ERR_FORM_DATA_DEPTH_EXCEEDED),防止畸形字段名导致递归失控; - 原型链污染防护:
buildPath遇到名为__proto__的段直接跳过,不写入目标对象(见 lib/helpers/formDataToJSON.js)。formDataToJSON只遍历utils.forEachEntry中的实际条目,不会触碰构造器原型链。
相关行为在仓库中有独立测试覆盖,可查阅 tests/unit/helpers/formDataToJSON.test.js 验证路径解析、重复 key、深度上限等用例。
五、限制与注意事项
文档明确声明的一条限制:
Sending Blobs/Files as JSON (base64) is not currently supported.
即当前不支持把表单中的 Blob/文件字段以 base64 形式编码进 JSON。当表单包含 <input type="file"> 时,应走第一节的 multipart 路径(postForm 默认行为),由 FormData 原生携带二进制文件,而不是显式指定 JSON 内容类型。
另外两点来自源码的适用前提:
- 直接提交
<form>元素依赖HTMLFormElement与new FormData(formElement),属于浏览器能力;在 Node 环境(http/https 适配器)中没有对应的 DOM 表单元素,JSON 路径转换则同样可用(Node 的FormData实现同样满足utils.isFormData); - 显式 JSON 提交走的是
JSON.stringify(formDataToJSON(data)),因此最终 body 是字符串;如需自定义序列化(如字段重命名),可覆盖transformRequest,或在发送前用axios.formToJSON先取出普通对象自行处理。
六、小结
axios.postForm(url, formElement)直接提交页面上的 HTML 表单,等价于post+ 默认Content-Type: multipart/form-data(lib/core/Axios.js);HTMLFormElement到FormData的转换发生在默认transformRequest中(lib/defaults/index.js);- 显式设置
Content-Type: application/json后,FormData/HTML 表单会经formDataToJSON转为 JSON 字符串发送,也可单独调用axios.formToJSON获取普通对象; - 字段名只有
.和[...]产生嵌套路径,其余字符是字面名,重复字段名自动合并为数组;实现细节见 lib/helpers/formDataToJSON.js,含深度上限与__proto__防护; - Blob/文件的 base64 JSON 编码目前不受支持,文件字段请使用 multipart 表单路径。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00