POST请求 之 对数据进行编码处理(下)
没办法,因为文章太长,掘金上一次性展示不了,所以要分成上下两个部分来发,大家没有看完上半部分,记得看上半部分
ok,我们继续把上半个部分的问题,进行解密...
FormData
FormData 对象可以表示 HTML 表单的一组key-value的数据。 同时key值也可以是文件,就像 <input type="file"> 一样。
您可以直接给 FormData 对象 添加数据:
const formData = new FormData();
formData.set('foo', 'bar');
formData.set('hello', 'world');
FormData对象也是一个迭代器,因此您可以将它转换为键值对数组或对象,就像使用 URLSearchParams 一样。但是,与 URLSearchParams 不同的是,您可以将 HTML 表单直接读取为 FormData:
const formElement = document.querySelector('form');
const formData = new FormData(formElement);
console.log(formData.get('username'));
这样,您就可以轻松地从表单中获取到数据了。 我经常使用这种方式,所以发现这比单独从每个元素中获取数据要容易得多。
让 FormData 作为Fetch的请求体(body)
与 URLSearchParams 类似,您可以直接使用 FormData 作为 fetch body:
const formData = new FormData();
formData.set('foo', 'bar');
formData.set('hello', 'world');
fetch(url, {
method: 'POST',
body: formData,
});
这会自动将 Content-Type 标头设置为 multipart/form-data,并以这种格式发送数据:
const formData = new FormData();
formData.set('foo', 'bar');
formData.set('hello', 'world');
const request = new Request('', { method: 'POST', body: formData });
console.log(await request.text());
...console.log打印出如下内容:
------WebKitFormBoundaryUekOXqmLphEavsu5
Content-Disposition: form-data; name="foo"
bar
------WebKitFormBoundaryUekOXqmLphEavsu5
Content-Disposition: form-data; name="hello"
world
------WebKitFormBoundaryUekOXqmLphEavsu5--
这就是使用 multipart/form-data 格式发送数据时body的样子。 它比 application/x-www-form-urlencoded 更复杂,但它可以包含文件数据。但是,某些服务器无法处理multipart/form-data ,比如:Express。 如果你想在 Express 中支持 multipart/form-data,你需要使用一些库来帮忙了比如: busboy 或 formidable
但是,如果您想将表单作为 application/x-www-form-urlencoded 发送怎么办? 嗯…
转换为 URLSearchParams
因为 URLSearchParams 构造函数可以接受一个生成键值对的迭代器,而 FormData 的迭代器正是这样做的,它可以生成键值对,因此您可以将 FormData 转换为 URLSearchParams:
const formElement = document.querySelector('form');
const formData = new FormData(formElement);
const searchParams = new URLSearchParams(formData);
fetch(url, {
method: 'POST',
body: searchParams,
});
但是,如果表单数据包含文件数据,则此转换过程将抛出错误。 因为application/x-www-form-urlencoded 不能表示文件数据,所以 URLSearchParams 也不能。
将Fetch的body读取为 FormData
您还可以将 Request 或 Response 对象读取为 FormData:
const formData = await request.formData();
如果Request 或 Response的body是 multipart/form-data 或 application/x-www-form-urlencoded,这个方法是很有效。 它对于服务器中处理表单提交特别有用。
其他可以作为Fetch的body的格式
还有一些其他格式format可以作为Fetch的body:
Blobs
Blob 对象(同时,File也可以作为Fetch的body, 因为它继承自 Blob)可以作为Fetch的body:
fetch(url, {
method: 'POST',
body: blob,
});
这会自动将 Content-Type 设置为 blob.type 的值。
Strings
fetch(url, {
method: 'POST',
body: JSON.stringify({ hello: 'world' }),
headers: { 'Content-Type': 'application/json' },
});
这会自动将 Content-Type 设置为 text/plain;charset=UTF-8,但它可以被覆盖,就像我上面所做的那样,将 Content-Type 设置为 application/json
Buffers
ArrayBuffer 对象,以及由数组缓冲区支持的任何东西,例如 Uint8Array,都可以用作Fetch的body:
fetch(url, {
method: 'POST',
body: new Uint8Array([
// …
]),
headers: { 'Content-Type': 'image/png' },
});
这不会自动设置 Content-Type 字段,因此您需要自己进行设置。
Streams
最后,获取主体可以是流(stream)! 对于 Response 对象,这可以让服务端获取不一样的开发体验,而且它们也可以与request一起使用。
所以,千万不要尝试自己处理 multipart/form-data 或 application/x-www-form-urlencoded 格式的数据,让 FormData 和 URLSearchParams 来帮我们完成这项艰苦的工作!
最后的福利:将 FormData 转换为 JSON
目前有个问题,就是:
如何将 FormData 序列化为 JSON 而不会丢失数据?
表单可以包含这样的字段:
<select multiple name="tvShows">
<option>Motherland</option>
<option>Taskmaster</option>
…
</select>
当然,您可以选择多个值,或者您可以有多个具有相同名称的输入:
<fieldset>
<legend>TV Shows</legend>
<label>
<input type="checkbox" name="tvShows" value="Motherland" />
Motherland
</label>
<label>
<input type="checkbox" name="tvShows" value="Taskmaster" />
Taskmaster
</label>
…
</fieldset>
最后获取到数据的结果是一个具有多个同名字段的 FormData 对象,如下所示:
const formData = new FormData();
formData.append('foo', 'bar');
formData.append('tvShows', 'Motherland');
formData.append('tvShows', 'Taskmaster');
就像我们在 URLSearchParams 中看到的,一些对象的转换是有损的(部分属性是会被剔除丢的):
// { foo: 'bar', tvShows: 'Taskmaster' }
const data = Object.fromEntries(formData);
有以下几种方法可以避免数据丢失,而且最终仍然可以将fromData数据序列化 JSON。
首先,转为[key, value]对数组:
// [['foo', 'bar'], ['tvShows', 'Motherland'], ['tvShows', 'Taskmaster']]
const data = [...formData];
但是如果你想要转为一个对象而不是一个数组,你可以这样做:
const data = Object.fromEntries(
// Get a de-duped set of keys
[...new Set(formData.keys())]
// Map to [key, arrayOfValues]
.map((key) => [key, formData.getAll(key)]),
);
...上诉代码的data变量,最终是:
{
"foo": ["bar"],
"tvShows": ["Motherland", "Taskmaster"]
}
我比较倾向于数据中每个值都是一个数组,即使它只有一个项目。 因为这可以防止服务器上的大量代码分支,并可以简化验证。 虽然,您有可能更倾向于 PHP/Perl 约定,其中以 [] 结尾的字段名称表示“这应该生成一个数组“, 如下:
<select multiple name="tvShows[]">
…
</select>
并我们来转换它:
const data = Object.fromEntries(
// Get a de-duped set of keys
[...new Set(formData.keys())].map((key) =>
key.endsWith('[]')
? // Remove [] from the end and get an array of values
[key.slice(0, -2), formData.getAll(key)]
: // Use the key as-is and get a single value
[key, formData.get(key)],
),
);
...上诉代码的data变量,最终是:
{
"foo": "bar",
"tvShows": ["Motherland", "Taskmaster"]
}
注意:如果form表单中包含文件数据,请不要尝试将表单转换为 JSON。 如果是这种
form表单中包含文件数据的情况,那么使用multipart/form-data会好得多。