如果你像下面这么发送http数据:
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';
}
这是不好的,可能会导致安全问题。这是出错的地方:body: `text=${text}`
. 未转义的文本被添加到具有定义编码的格式中。它类似于 SQL/HTML 注入,因为某种旨在作为“值”的东西可以直接与格式交互。
我将深入研究正确的方法,但也会浏览一些相关的、鲜为人知的 API:
网址搜索参数
URLSearchParams
处理编码和解码application/x-www-form-urlencoded
数据。它非常方便,因为,嗯……
这种
application/x-www-form-urlencoded
格式在很多方面都是一种异常的怪物,是多年实施事故和妥协的结果,导致了一组互操作性所需的要求,但绝不代表良好的设计实践。特别提醒读者密切注意涉及字符编码和字节序列之间重复(在某些情况下是嵌套)转换的扭曲细节。不幸的是,由于 HTML 表单的流行,该格式被广泛使用。— URL 标准
......所以是的,尝试自己编码/解码它是一个坏主意。这是它的工作原理:
const searchParams = new URLSearchParams();
searchParams.set('foo', 'bar');
searchParams.set('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());
或者传入一个字符串
const searchParams = new URLSearchParams('foo=bar&hello=world');
// Logs 'foo=bar&hello=world'
console.log(searchParams.toString());
读取 URLSearchParams
有多种 read 和 mutate URLSearchParams
的方法,这些方法记录在 MDN 上,但如果你想处理所有数据,那么它的迭代器就派上用场了:
for (const [key, value] of searchParams) {
console.log(key, value);
}
可以轻松简单地将其转换为名称/值对数组:
// To [['foo', 'bar'], ['hello', 'world']]
const keyValuePairs = [...searchParams];
或者将它与支持生成名称/值对的迭代器的 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'));
不幸的是,location.searchParams
是未定义的。这是因为为了定义window.location
的某些属性如何跨源工作而变得复杂。例如设置 otherWindow.location.href
跨源工作,但不允许获取它。无论如何,要解决它:
// 没有定义
location.searchParams;
const url = new URL(location.href);
// 有定义好的searchParams
url.searchParams;
// 或者:
const searchParams = new URLSearchParams(location.search);
URLSearchParams 作为 Fetch 的post body
好的,现在我们进入正题。文章开头示例中的代码已损坏,因为它没有转义输入:
const value = 'hello&world';
const badEncoding = `text=${value}`;
// 😬 打印 [['text', 'hello'], ['world', '']]
console.log([...new URLSearchParams(badEncoding)]);
const correctEncoding = new URLSearchParams({ text: value });
// 打印 '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
用作正文,Content-Type
标题会自动设置为application/x-www-form-urlencoded
,这很好,因为即使在成为 Web 开发人员 20 多年之后,我也永远记不起该内容类型。您仍然可以提供自己的Content-Type
标头来覆盖默认值。
你不能把一个Request
或Response
body读作URLSearchParams
,但有办法解决这个问题……
表单数据
FormData
对象可以表示 HTML 表单的名称/值状态。这意味着这些值可以是文件,就像<input type="file">
.
您可以FormData
直接填充状态:
const formData = new FormData();
formData.set('foo', 'bar');
formData.set('hello', 'world');
它也是一个迭代器,因此可以将其转换为名称/值对数组或对象,就像使用URLSearchParams
那样. 但是,与 不同的是URLSearchParams
,您可以将 HTML 表单直接读取为FormData
:
const formElement = document.querySelector('form');
const formData = new FormData(formElement);
console.log(formData.get('username'));
这为您提供了从form元素表单所有的数据。我经常发现这比单独从每个元素获取数据要容易得多。
FormData 作为 Fetch 主体
与 类似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());
...打印出如下内容:
------WebKitFormBoundaryUekOXqmLphEavsu5
Content-Disposition: form-data; name="foo"
bar
------WebKitFormBoundaryUekOXqmLphEavsu5
Content-Disposition: form-data; name="hello"
world
------WebKitFormBoundaryUekOXqmLphEavsu5--
这就是multipart/form-data
看起来的样子。它比 application/x-www-form-urlencoded
复杂,但它可以包含文件数据。但是,某些服务器无法处理multipart/form-data
,包括Express。如果您想multipart/form-data
在 Express 中提供支持,则需要使用诸如busboy或formidable之类的东西。
但是如果你想发送一个表单application/x-www-form-urlencoded
怎么办?请继续往下看。
转换为 URLSearchParams
由于URLSearchParams
构造函数接受一个生成名称/值对FormData
的迭代器,而的迭代器正是这样做的,因此您可以从一个转换为另一个:
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 主体作为 FormData
您还可以将 aRequest
或Response
object读作FormData
:
const formData = await request.formData();
如果请求/响应有效则此content-type是multipart/form-data
或 application/x-www-form-urlencoded
。它对于在 Service Worker 中处理表单提交特别有用。