深入浅出 post data,multipart/form-data,json,application/x-www-form-urlencoded

2,147 阅读5分钟

如果你像下面这么发送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可以直接用作为RequestResponsebody,因此文章开头的“正确”代码版本是:

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标头来覆盖默认值。

你不能把一个RequestResponsebody读作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 中提供支持,则需要使用诸如busboyformidable之类的东西。

但是如果你想发送一个表单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

您还可以将 aRequestResponseobject读作FormData

const formData = await request.formData();

如果请求/响应有效则此content-type是multipart/form-dataapplication/x-www-form-urlencoded。它对于在 Service Worker 中处理表单提交特别有用。