POST 申请 之 对数据进行编码解决
<!– TOC –>
-
URLSearchParams
- URLSearchParams 的读取和转换操作
- url.searchParams
- 让 URLSearchParams 作为 Fetch 的申请体(body)
-
FormData
- 让 FormData 作为 Fetch 的申请体(body)
- 转换为 URLSearchParams
- 将 Fetch 的 body 读取为 FormData
-
其余能够作为 Fetch 的 body 的格局
- Blobs
- Strings
- Buffers
- Streams
- 最初的福利:将 FormData 转换为 JSON
- 参考
<!– /TOC –>
好, 来。咱们先来先来看个代码例子:
async function isPositive(text) {
const response = await fetch(`http://text-processing.com/api/sentiment/`, {
method: 'POST',
body: `text=${text}`,
headers: {'Content-Type': 'application/x-www-form-urlencoded',},
});
const json = await response.json();
return json.label === 'pos';
}
这块代码写得比拟蹩脚,可能会导致平安问题。为什么呢?因为:text=${text}
这块中央存在问题:
未本义的文本被增加到具备定义编码的格局中。就是说,这里的 text
变量,它是没有通过本义(或者说是编码)就间接被写到了申请体中,而在这个申请中,是有要求编码格局的'Content-Type': 'application/x-www-form-urlencoded'
。
这种写法有点相似于 SQL/HTML 注入,因为某种旨在作为“值”的货色(指相似 text 变量
的一些值)能够间接与格局进行交互。
所以,我将深入研究正确的办法,同时也会浏览一些相干的、鲜为人知的 API:
URLSearchParams
URLSearchParams 能够用来 解决编码和解码 application/x-www-form-urlencoded 数据。它十分不便,因为,嗯……
The application/x-www-form-urlencoded format is in many ways an aberrant monstrosity, the result of many years of implementation accidents and compromises leading to a set of requirements necessary for interoperability, but in no way representing good design practices. In particular, readers are cautioned to pay close attention to the twisted details involving repeated (and in some cases nested) conversions between character encodings and byte sequences. Unfortunately the format is in widespread use due to the prevalence of HTML forms. — The URL standard
…… 所以, 是的,十分不倡议你本人 对 application/x-www-form-urlencoded 的数据
进行编码 / 解码。
上面是 URLSearchParams 的工作原理:
const searchParams = new URLSearchParams();
searchParams.set('foo', 'bar');
searchParams.set('hello', 'world');
// Logs 'foo=bar&hello=world'
console.log(searchParams.toString());
URLSearchParams 这个构造函数还能够承受一个 [key, value] 对的数组,或一个产生 [key, value] 对的迭代器:
const searchParams = new URLSearchParams([['foo', 'bar'],
['hello', 'world'],
]);
// Logs 'foo=bar&hello=world'
console.log(searchParams.toString());
或者是一个对象:
const searchParams = new URLSearchParams({
foo: 'bar',
hello: 'world',
});
// Logs 'foo=bar&hello=world'
console.log(searchParams.toString());
或者是一个字符串:
const searchParams = new URLSearchParams('foo=bar&hello=world');
// Logs 'foo=bar&hello=world'
console.log(searchParams.toString());
URLSearchParams 的读取和转换操作
读取(指对数据进行枚举等读取操作)和转换(指将其转为数组或者对象等)URLSearchParams 的办法还是很多的,MDN 上都有具体阐明。
如果在某些场景下,您想解决所有数据,那么它的迭代器就派上用场了:
const searchParams = new URLSearchParams('foo=bar&hello=world');
for (const [key, value] of searchParams) {console.log(key, value);
}
这同时意味着您能够轻松地将其转换为 [key, value] 对数组:
// To [['foo', 'bar'], ['hello', 'world']]
const keyValuePairs = [...searchParams];
或者将它与反对生成 key-value 对的迭代器的 API 一起应用,例如 Object.fromEntries,能够把它转换为一个对象:
// To {foo: 'bar', hello: 'world'}
const data = Object.fromEntries(searchParams);
然而,请留神,转换为对象有时是有损转换的哦:就是可能会造成某些值得失落
const searchParams = new URLSearchParams([['foo', 'bar'],
['foo', 'hello'],
]);
// Logs "foo=bar&foo=hello"
console.log(searchParams.toString());
// To {foo: 'hello'}
const data = Object.fromEntries(searchParams);
url.searchParams
URL 对象上有一个 searchParams 属性,十分不便地获取到申请参数:
const url = new URL('https://jakearchibald.com/?foo=bar&hello=world');
// Logs 'world'
console.log(url.searchParams.get('hello'));
可怜的是,在 window.location
上没有 location.searchParams
这个属性。
这是因为 window.location 因为它的某些属性如何跨源工作而变得复杂。例如设置 otherWindow.location.href 能够跨源工作,但不容许获取它。
然而无论如何,咱们都要解决它,让咱们能比拟容易地从地址栏中获取到申请参数:
// Boo, undefined
location.searchParams;
const url = new URL(location.href);
// Yay, defined!
url.searchParams;
// Or:
const searchParams = new URLSearchParams(location.search);
让 URLSearchParams 作为 Fetch 的申请体(body)
好的,当初咱们进入正题。文章结尾示例中的代码存在一些问题,因为它没有进行本义输出:
const value = 'hello&world';
const badEncoding = `text=${value}`;
// 😬 Logs [['text', 'hello'], ['world', '']]
console.log([...new URLSearchParams(badEncoding)]);
const correctEncoding = new URLSearchParams({text: value});
// Logs 'text=hello%26world'
console.log(correctEncoding.toString());
为方便使用,URLSearchParams 对象
是能够间接被用作 申请 Request
或 响应 Response
的 主体 body
,因而文章结尾的“正确”代码版本是:
async function isPositive(text) {
const response = await fetch(`http://text-processing.com/api/sentiment/`, {
method: 'POST',
body: new URLSearchParams({text}),
});
const json = await response.json();
return json.label === 'pos';
}
如果应用 URLSearchParams 作为 body
,则 Content-Type
字段会 主动设置 为 application/x-www-form-urlencoded
您不能将 申请 Request
或 响应 Response
的 主体 body
读取为 URLSearchParams 对象,但咱们有一些办法能够解决这个问题……
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
会好得多。
参考
- Encoding data for POST requests