POST请求 之 对数据进行编码处理(下)

911 阅读5分钟

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,你需要使用一些库来帮忙了比如: busboyformidable

但是,如果您想将表单作为 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-dataapplication/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-dataapplication/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 会好得多。

参考

最后,欢迎关注我的公众号:前端学长Joshua