平时的前端开发中基本上都是使用 XMLHttpRequest
或 fetch
来发送 HTTP 请求,提交表单数据,很少使用 <form>
元素自身的 action/method/enctype
等属性。本文让我们来回顾一下这两种写法。
1 通过 <form>
元素提交表单
1.1 基本用法
<form
action="http://localhost:8080/api/login"
method="post"
enctype="application/x-www-form-urlencoded"
target="_self"
>
<div>
<label>姓名</label>
<input type="text" name="name" placeholder="请输入姓名" />
</div>
<div>
<label>密码</label>
<input type="password" name="password" placeholder="请输入密码" />
</div>
<button type="submit">提交</button>
</form>
action
表单提交的 URL,默认为当前页面地址,表单提交后会跳转到该地址action="URL"
提交到 URLaction=""
提交到当前页面地址action="#"
提交到当前页面地址+#
(当然,实际上#
号后的内容并不会被提交到服务端)action="javascript:;"
提交时避免跳转,类似<a href="javascript:;">
action="javascript:void(0);"
提交时避免跳转,类似<a href="javascript:void(0);">
method
提交时使用的 HTTP 请求方法get
表单数据会被追加到action
对应的 URL 后(<URL>?<Query String Parameters>
)post
表单数据会被作为请求体发送
enctype
表单提交时的Content-Type
,method
需要设置为post
application/x-www-form-urlencoded
默认值,提交的内容会经过 URL 编码处理multipart/form-data
上传文件时使用text/plain
调试时使用
target
用于指定表单提交后在哪儿展示响应内容_self
默认值,默认在当前浏览器上下文(页面)展示_blank
在新的浏览器上下文(标签页)中展示_parent
在父级浏览器上下文中展示,如果没有,则相当于_self
_top
在顶级浏览器上下文中展示,如果没有,则相当于_self
- 除此以外,还可以是
window
或者<iframe>
的name
(见 form - MDN 和 iframe - MDN)
<form>
的优势在于是纯 HTML,即使用户禁用浏览器的 JavaScript 功能,依然可以正常提交表单。
1.2 特殊用法一
<form>
的一个特殊用处是实现搜索框时让手机输入法软键盘的回车键显示为“搜索”(必要条件:<form action="url">
和 <input type="search" />
)。
<form action="http://localhost:8080/api/search" id="my-form">
<input type="search" name="keywords" placeholder="搜索" />
<button type="submit">搜索</button>
</form>
注意,表单提交时,默认会跳转到 action
对应地址,可以监听 submit
事件,通过调用 preventDefault()
阻止该行为。
document.getElementById("my-form").addEventListener("submit", function(event) {
event.preventDefault();
return false;
});
谨慎使用如下形式来避免自动跳转:
<form action="javascript:void(0);">
<input type="search" name="keywords" placeholder="搜索" />
<button type="submit">搜索</button>
</form>
这种写法在 React 中,可能会被取消支持。
Warning: A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML try using dangerouslySetInnerHTML instead. React was passed "javascript:void(0);".
更值得注意的是,在 iOS 系统上,使用 action="javascript:void(0);"
或其他等价写法,会产生一次多余的不可见的跳转(具体现象是需要点击两次搜索按钮才触发事件),所以更推荐使用事件处理函数。
其他示例
<!-- 普通 div 容器 -->
<div>
<h2>div 容器</h1>
<p>键盘回车键显示 return (换行)</p>
<div>
<label>type="text"</label>
<input type="text" placeholder="请输入文字">
</div>
<div>
<label>type="number"</label>
<input type="number" placeholder="请输入数字">
</div>
<div>
<label>type="search"</label>
<input type="search" placeholder="搜索">
</div>
</div>
<!-- form 容器 -->
<form action="javascript:;">
<h2>form 容器,配合 action 属性(必需)</h1>
<p>键盘回车键显示 go (前往), search (搜索)</p>
<div>
<label>type="text"</label>
<input type="text" placeholder="请输入文字">
</div>
<div>
<label>type="number"</label>
<input type="number" placeholder="请输入数字">
</div>
<div>
<label>type="search"</label>
<input type="search" placeholder="搜索">
</div>
<div>
<button type="submit">提交</button>
</div>
</form>
1.3 特殊用法二
<form>
的另一个特殊用处是实现通过 POST 方法打开页面。
<form action="http://localhost:8080/posts" method="post" id="my-form">
<input type="hidden" name="name" value="name" />
<input type="hidden" name="password" value="password" />
</form>
<button type="button" id="my-btn">打开一个只能通过 POST 方法访问的页面</button>
document.getElementById('my-btn').addEventListener('click', function() {
document.getElementById('my-form').submit();
});
简单封装一下,动态创建 <form>
和隐藏的 <input>
,便可以像发送 HTTP 请求一样打开页面。
interface NavigateConfig {
/** 跳转地址 */
url?: string;
/** 请求方法 */
method?: 'get' | 'post';
/** 参数 */
params?: {[key: string]: any};
/** 打开方式 */
target?: '_self' | '_blank' | '_parent' | '_top';
}
/**
* 页面跳转
* @param config - 页面跳转配置
* @param config.url - 跳转地址
* @param config.method - 请求方法
* @param config.params - 参数
* @param config.target - 打开方式
*/
function navigateTo(config: NavigateConfig) {
const {
url = window.location.href,
method = 'post',
params = {},
target = '_self'
} = { ...config };
const inputGroup = document.createDocumentFragment();
const form = document.createElement('form');
Object.keys(params || {}).forEach((key) => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = params[key];
inputGroup.appendChild(input);
});
form.action = url;
form.method = method;
form.target = target;
form.appendChild(inputGroup);
document.body.appendChild(form);
form.submit();
}
navigateTo({
url: 'http://localhost:8080/posts',
method: 'post',
params: {
name: '阿里巴巴',
password: '芝麻开门'
}
});
2 通过 Ajax 提交表单
2.1 <form>
表单和 Ajax 的关系
传统的 <form>
表单在提交时会刷新页面或者跳转到新页面,所以 XMLHttpRequest
才有机会得到广泛应用。
后来 XMLHttpRequest Level 2 标准中新增 FormData
对象,使得通过 Ajax 也可以实现文件上传,<form>
的功能都可以借助 Ajax 实现。
以下是一个使用 XMLHttpRequest
的示例:
const formData = new FormData();
formData.append("name", name);
formData.append("password", password);
formData.append("avatar", avatarInputElement.files[0]);
const request = new XMLHttpRequest();
request.open("POST", "http://localhost:8080/api/login");
request.send(formData);
在 2020 年这个时间点,主流已经是使用 Fetch API,为了简洁,以下代码示例跳过 XMLHttpRequest API,直接采用 Fetch API。
注意:在旧版本浏览器上使用 fetch
/URL
/URLSearchParams
/FormData
等 API 时可能需要酌情引入相应 polyfill。
2.2 GET 请求提交表单
const url = new URL("http://localhost:8080/api/login");
url.searchParams.append("name", name);
url.searchParams.append("password", password);
fetch(url, {
method: "GET"
})
.then(function(response) {
return response.json();
})
.then(function(response) {
console.log(response);
});
GET http://localhost:8080/api/login?name=name&password=password HTTP/1.1
请求参数最终拼在 URL 后发送到服务端。在 Chrome 中被标注为查询字符串(Query String)。
URLSearchParams
会自动将空格 " "
被处理成 +
号,自动进行 URL 编码。
2.3 enctype="application/x-www-form-urlencoded"
等价写法
const params = new URLSearchParams();
params.append("name", name);
params.append("password", password);
fetch("http://localhost:8080/api/login", {
method: "POST",
body: params
})
.then(function(response) {
return response.json();
})
.then(function(response) {
console.log(response);
});
POST http://localhost:8080/api/login HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8
name=name&password=password
请求参数最终放在请求体中发送到服务端。在 Chrome 中被标注为表单数据(Form Data)。
默认会自动设置 Content-Type
请求头 application/x-www-form-urlencoded;charset=utf-8
。
2.4 enctype="multipart/form-data"
等价写法
const formData = new FormData();
formData.append("name", name);
formData.append("password", password);
fetch("http://localhost:8080/api/login", {
method: "POST",
body: formData
})
.then(function(response) {
return response.json();
})
.then(function(response) {
console.log(response);
});
Firefox
POST http://localhost:8080/api/login HTTP/1.1
Content-Type: multipart/form-data; boundary=---------------------------160693386017892497023111825902
-----------------------------160693386017892497023111825902
Content-Disposition: form-data; name="name"
name
-----------------------------160693386017892497023111825902
Content-Disposition: form-data; name="password"
password
-----------------------------160693386017892497023111825902--
Chrome
POST http://localhost:8080/api/login HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryp8gr8UmAVYJEVKFc
------WebKitFormBoundaryp8gr8UmAVYJEVKFc
Content-Disposition: form-data; name="name"
name
------WebKitFormBoundaryp8gr8UmAVYJEVKFc
Content-Disposition: form-data; name="password"
password
------WebKitFormBoundaryp8gr8UmAVYJEVKFc--
请求参数最终放在请求体中发送到服务端。在 Chrome 中被标注为表单数据(Form Data)。
默认会自动设置 Content-Type
请求头 multipart/form-data; boundary=${boundary}
。
2.5 application/json
如果要提交 JSON,只能通过 Ajax 来实现,<form>
不支持 enctype="application/json"
。
fetch("http://localhost:8080/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8"
},
body: JSON.stringify({
name: "name",
password: "password"
})
})
.then(function(response) {
return response.json();
})
.then(function(response) {
console.log(response);
});
POST http://localhost:8080/api/login HTTP/1.1
Content-Type: application/json;charset=utf-8
{"name":"name","password":"password"}
请求参数最终放在请求体中发送到服务端。在 Chrome 中被标注为请求负载(Request Payload)。
由于 fetch
的 body
目前只支持 Blob
、BufferSource
(如 ArrayBuffer
/DataView
等对象)、FormData
、URLSearchParams
、字符串、ReadableStream
,因此需要使用 JSON.stringify
将待发送的 JSON 数据序列化成字符串。
同时指定 Content-Type
为 application/json;charset=UTF-8
(未指定的情况下,字符串类型默认的 Content-Type
为 text/plain;charset=UTF-8
)。另外,字符集的名称必须是取自 US-ASCII 中的可打印字符,不区分大小写,charset=utf-8
等价于 charset=UTF-8
。
2.6 小节:几种常见请求参数
- Query String Parameters: URL 参数,追加到 URL 后面
- Form Data:
- 2.1
application/x-www-form-urlencoded
- 2.2
multipart/form-data
- 2.1
- Request Payload:
application/json
说明和思考:
- URL 参数对应的数据被编码成以
&
分隔的键值对, 键和值以=
分隔,非字母数字的字符会被 URL 编码。 FormData
参数对应的数据不会被编码,通过 boundary 分隔,最终发送二进制数据。application/x-www-form-urlencoded
参数虽然和 URL 参数一样编码,但是是放在请求体中而不是拼在 URL 后。- URL 参数对数组和对象的编码方式不统一,需要多加注意。
application/json
可读性更好,但实际使用时,要比application/x-www-form-urlencoded
传输的数据量要大。application/json
不是万能的,要实现文件上传应该使用multipart/form-data
(如果是Blob
,Content-Type
可能需要设置为它的type
属性) 。
3 HTTP 请求的组成
重新复习下 HTTP 请求的组成:
- 请求方法(Method)
- 请求 URL
- HTTP 版本
- 请求头(Request Headers)
- 请求体(Request Body)
<method> <request-URL> <version>
<headers>
<entity-body>
如上面的 GET/POST 请求报文:
GET http://localhost:8080/api/login?name=name&password=password HTTP/1.1
POST http://localhost:8080/api/login HTTP/1.1
Content-Type: application/json;charset=utf-8
{"name":"name","password":"password"}
4 总结
技术是不断变化和进步的,无论是传统的 <form>
还是 Ajax,最核心的部分在于 HTTP 请求,可见熟悉 HTTP 标准是前端的基础。
5 相关链接
- 四种常见的 POST 提交数据方式
- 关于前端传参问题 - V2EX
- 标准
<form>
- https://xhr.spec.whatwg.org/
XMLHttpRequest
FormData
- https://url.spec.whatwg.org/
URL
URLSearchParams
- https://fetch.spec.whatwg.org/
fetch
- https://w3c.github.io/FileAPI/
Blob
File
FileReader
charset=utf-8
还是charset=UTF-8
?