首页
/ Axios 浏览器端 HTML 表单提交全解:postForm 与 JSON 序列化的实现原理

Axios 浏览器端 HTML 表单提交全解:postForm 与 JSON 序列化的实现原理

2026-09-04 11:43:17作者:宗隆裙

本文围绕 Axios 的 HTML 表单提交能力展开:如何直接用 axios.postForm 提交页面上的 <form> 元素、如何通过 Content-Type 头将 FormData/HTMLForm 以 JSON 形式发送、字段名的路径(dot/bracket)命名规则如何决定嵌套结构,并结合 lib/ 目录下的源码(Axios.jsdefaults/index.jsformDataToJSON.js)与单元测试,完整讲清这一功能从 API 到数据变换的底层链路,帮助读者在浏览器项目中正确且安全地提交复杂表单。

直接用 postForm 提交 HTML 表单

当页面中已经存在一个表单元素,而你又希望绕开原生 submit 流程、用 Axios 的 Promise 风格提交它时,可以直接把 <form> DOM 节点作为请求体传入:

await axios.postForm('https://httpbin.org/post', document.querySelector('#htmlForm'));

postForm 会默认以 multipart/form-data 编码提交表单数据,且无需编写任何额外的 JavaScript 取值代码——表单里所有带 name 属性的控件(输入框、下拉框等)都会被自动收集。

源码视角:postForm 从哪里来

postForm 并不是手写的方法,而是 Axios 在初始化时通过工厂批量生成的"表单便捷方法"。在 Axios 实例类 中,postputpatchquery 四种方法都会被生成两个变体:

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();

  if (method !== 'query') {
    Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
  }
});

从源码结构看,有两个值得注意的点:

  • isFormtrue 时,工厂会向请求配置注入 'Content-Type': 'multipart/form-data' 头,这正是 postFormpost 的唯一本质差异——后者不预设 Content-Type,而前者明确声明以 multipart 表单格式提交;
  • query 方法被显式排除了 queryForm 快捷方法(源码注释说明:query 是安全的只读方法,与 multipart 表单体的语义不匹配)。

因此 Axios 实例上实际可用的是 postFormputFormpatchForm 三个表单便捷方法。

把 FormData / HTMLForm 以 JSON 提交

FormDataHTMLForm 对象也可以不转成 multipart,而是显式设置 Content-Typeapplication/json,以 JSON 字符串形式提交:

await axios.post('https://httpbin.org/post', document.querySelector('#htmlForm'), {
  headers: {
    'Content-Type': 'application/json',
  },
});

这一分支的实现位于默认请求变换器中。在 defaults/index.jstransformRequest 里可以看到完整的判定逻辑:

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);
}

const isFormData = utils.isFormData(data);

if (isFormData) {
  return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data;
}

调用链非常清晰:

  1. 原始数据是 <form> 元素时(utils.isHTMLForm 基于 kindOfTest('HTMLFormElement') 判定,见 lib/utils.js),先用浏览器原生的 new FormData(data) 把它收敛为 FormData;
  2. 判定为 FormData 后,若请求头声明了 JSON 内容类型,就调用 formDataToJSON 转成普通对象再 JSON.stringify;否则原样返回,交由浏览器(通过 XHR/fetch 适配器)自动以 multipart 形式发送。

浏览器端最终由 XHR 适配器 执行 request.send(requestData):当 requestDataFormData 时,浏览器会自动附加带 boundaryContent-Type 头;当它是 JSON 字符串时,则直接使用你显式设置的 application/json 头。

表单示例与提交结果的完整对照

文档给出的一个可被上述代码提交的有效表单如下,涵盖了嵌套路径、含空格字段名、重名字段和下拉框四种典型形态:

<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"
  }
}

对照这张映射表可以读出四条规则:

表单字段名 输出结构 原因
foo 顶层标量 "foo": "1" 普通字段名,原样保留
deep.prop 嵌套对象 deep.prop 点号是路径分隔符,创建嵌套属性
deep prop spaced 顶层字面键 "deep prop spaced" 空格不是路径分隔符,整名保留为字面键
baz(出现两次) 数组 "baz": ["4", "5"] 同名重复字段自动聚合为数组
user.age 嵌套对象 user.age 下拉框的选中值(value2)按路径写入

字段名路径规则:只有点号与方括号创建嵌套路径

这里有一条对服务端对接极为重要的规则:只有点号(.)和方括号([...])记法会创建属性路径,其余字符——包括空格、-+*&——都保持为字面字段名的一部分。例如 deep.prop 会创建嵌套路径,而 deep prop spaced 则始终是顶层键。

该规则由 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;
}

正则 /[^.[\]]+|\[([^.[\]]*)]/g 的含义是:按"不含 .[] 的连续字符"或"[...] 内的捕获组"两种片段切分字段名。由此得到几项行为:

  • foo[bar.baz] 解析为 ['foo', 'bar', 'baz']——方括号内部同样按点号继续切分;
  • user-nameuser name 这类含连字符或空格的名字保持字面整体,不会被错误拆开(源码注释中引用了 issue #5402 说明这一修复背景);
  • foo[] 中的空方括号被解析为空段 '',配合 buildPath 中的数组逻辑 实现"重复键追加进数组"的语义。

同一转换的边界条件同样来自源码:路径段数超过 DEFAULT_FORM_DATA_MAX_DEPTH(值为 100,定义在 toFormData.js,与反向转换 toFormData 共享该常量以保证往返对称)时,会抛出 ERR_FORM_DATA_DEPTH_EXCEEDED 错误。

另外值得了解的是原型污染防护:在 buildPath 中,遇到 __proto__ 段会直接跳过,并且属性写入只走自有属性判定(utils.hasOwnProp),配合 THREATMODEL.md 中列出的纵深防御策略。这一点在 formDataToJSON 单元测试 中有专门验证——即使表单中混入 __proto__.xconstructor.prototype.y 等恶意字段名,转换结果也只是把 constructor.prototype.y 建成普通对象,而 {} 的原型保持干净。

手动转换与测试辅助:axios.formToJSON

如果不经过网络请求,只想在客户端把 FormData<form> 元素转成普通 JSON 对象(例如用于表单预检、日志、本地存储),Axios 暴露了与上述请求链路同一个 formDataToJSON 实现的便捷 API,见 lib/axios.js:

axios.formToJSON = (thing) =>
  formDataToJSON(utils.isHTMLForm(thing) ? new FormData(thing) : thing);

也就是说 axios.formToJSON(document.querySelector('#htmlForm')) 会得到与上面 JSON 提交示例完全一致的对象,而不发起任何请求。单元测试 tests/unit/helpers/formDataToJSON.test.js 覆盖了这一机制的关键分支:方括号嵌套路径、重复键聚合成数组(包括 3 个以上元素仍保持平坦数组)、空方括号数组、数字下标数组(foo[0]/foo[1])等,均可直接参照这些用例验证自己的字段命名方案。

当前限制:Blob / File 尚不支持 JSON(base64)提交

原文明确给出了一条警告:将 Blob/File 以 JSON(base64)形式发送目前不受支持。也就是说,当表单内含 <input type="file"> 时:

  • postForm / 默认 multipart 路径:文件会作为 multipart 部件正常上传,这是推荐用法;
  • 显式声明 Content-Type: application/json 的路径:formDataToJSON 只会把字段按字符串值收集,二进制内容无法被无损地编码进 JSON,因此该组合不可用。

需要把文件送出去时,请坚持使用 multipart 表单路径(postForm 或不设 JSON 头的 post),并参考 multipart-form-data-format 文档 了解更完整的 multipart 编码行为。

小结

Axios 的浏览器端 HTML 表单提交能力可以概括为一条清晰的数据管线:

  1. postForm/putForm/patchFormlib/core/Axios.js 工厂生成,与 post 的差异仅是预设 multipart/form-data 头;
  2. 默认 transformRequest(lib/defaults/index.js) 负责收敛三种入参形态(HTMLForm → FormData → JSON 字符串或原样 FormData);
  3. JSON 化由 formDataToJSON(lib/helpers/formDataToJSON.js) 完成,遵循"点号与方括号建路径、其余字符保字面、重复键成数组"的确定规则,并内置深度限制与原型污染防护;
  4. 文件类字段(multipart)与 JSON 提交是两条互斥路径,后者目前不支持 base64 文件。

掌握这些规则后,你可以自由设计带嵌套路径的表单字段名(如 user.ageaddress[city]),并准确预测服务端将以何种 JSON 结构收到数据。

登录后查看全文
热门项目推荐
相关项目推荐