一、要创建一个POST请求,或者其他方法的请求,我们需要使用fetch选项。
二、这是所有可能的fetch选项及其默认值(注释中标注了可选值)的完整列表

let promise = fetch(url, {
    method: 'GET', // POST, PUT, DELETE等
  headers: {
      // 内容类型header值通常是自动设置的
    // 取决于request body
    'Content-Type': 'text/palin;charset=UTF-8'
  },
  body: undefined, // string, FormData, Blob, BufferSource,或URLSearchParams
  referrer: 'about:client', // 或''以不发送Referer header,或者是当前源的url
  referrerPolicy: 'no-referrer-when-downgrade', // no-referrer, origin, same-origin...
  mode: 'cors', // same-origin, no-cors
  credentials: 'same-origin', // omit,include
  cache: 'default', // no-store, reload, no-cache, force-cache,或only-if-cached
  redirect: 'follow', // manual, error
  integrity: '', // 一个hash,像”sha256-abcdef1234567890“
  keepalive: false, // true
  signal: undefined, // AbortController来中止请求
  window: window // null
})

三、fetch选项
1、method —— HTTP 方法,如POST
2、headers —— 具有 request header 的对象(不是所有 header 都是被允许的)
3、body —— 要以 string,FormData,BufferSource,Blob 或 UrlSearchParams 对象的形式发送的数据(request body)。取值如下,其中之一:
(1) 字符串(例如JSON编码的)
(2)FormData对象,以form/multipart形式发送数据
(3)Blob/BufferSource发送二进制数据。
(4)URLSearchParams,以x-www-form-urlencoded编码形式发送数据,很少使用。

method

POST请求

一、JSON形式是最常用的。
【示例1】下面这段代码以JSON形式发送user对象

let user = {
    name: 'John',
  surname: 'Smith'
};

let response = await fetch('/article/fetch/post/user', {
    method: 'POST',
  headers: {
      'Content-Type': 'application/json;charset=utf-8'
  },
  body: JSON.stringify(user)
});

let result = await response.json();
alert(result.message);

三、如果请求的body是字符串,则Content-Type会默认设置为text/plain;charset=UTF-8。但是,当我们要发送JSON时,我们会使用headers选项来发送application/json,这是JSON编码的数据的正确的Content-Type。

headers

Request header

一、要在fetch中设置request header,我们可以使用headers选项。它有一个带有输出header的对象,如下:

let response = fetch(protectedUrl, {
    headers: {
      Authentication: 'secret'
  }
});

二、无法设置的header如下。这些header保证了HTTP的正确性和安全性,所以它们仅由浏览器控制。

  • Accept-Charset, Accept-Encoding
  • Access-Control-Request-Headers
  • Access-Control-Request-Method
  • Connection
  • Content-Length
  • Cookie, Cookie2
  • Date
  • DNT
  • Expect
  • Host
  • Keep-Alive
  • Origin
  • Referer
  • TE
  • Trailer
  • Transfer-Encoding
  • Upgrade
  • Via
  • Proxy-*
  • Sec-*

    body

    FormData

    一、FormData对象是表示HTML表单数据的对象。
    二、构造函数
    let formData = new FormData([form]);
    
    三、如果提供了HTML form元素,它会自动捕获form元素字段。
    四、FormData的特殊之处在于网络方法(network methods)
    【示例1】fetch可以接受一个FormData对象作为body。它会被编码并发送出去,带有Content-Type: multipart/form-data。
    1、从服务器角度来看,它就像是一个普通的表单提交。
    五、我们可以从 HTML 表单创建 new FormData(form),也可以创建一个完全没有表单的对象,然后使用append(), set()方法附加字段。

    FormData方法

    一、我们可以使用以下方法修改 FormData 中的字段:
    1、formData.append(name, value): 添加具有给定 name 和 value 的表单字段,
    2、formData.append(name, blob, fileName) : 添加一个字段,就像它是 ,第三个参数 fileName 设置文件名(而不是表单字段名),因为它是用户文件系统中文件的名称
    3、formData.set(name, value):移除所有具有给定 name 的字段,然后附加一个具有给定 name 和 value 的新字段,
    4、formData.set(name, blob, fileName):移除所有具有给定 name 的字段,然后附加一个新字段
    5、formData.delete(name) : 移除带有给定 name 的字段,
    6、formData.get(name):获取带有给定 name 的字段值,
    7、formData.has(name) :如果存在带有给定 name 的字段,则返回 true,否则返回 false。
    二、从技术上来讲,一个表单可以包含多个具有相同 name 的字段,因此,多次调用 append 将会添加多个具有相同名称的字段。
    三、两个特点:
    1、set 方法会移除具有相同名称(name)的字段,而 append 不会。
    2、要发送文件,需要使用三个参数的语法,最后一个参数是文件名,一般是通过 从用户文件系统中获取的。
    四、我们也可以使用 for..of 循环迭代 formData 字段: ```javascript let formData = new FormData(); formData.append(‘key1’, ‘value1’); formData.append(‘key2’, ‘value2’);

// 列出 key/value 对 for(let [name, value] of formData) { alert(${name} = ${value}); // key1=value1,然后是 key2=value2 }

<a name="L8CR4"></a>
### 发送表单
<a name="9CsnC"></a>
#### 发送一个简单的表单
【示例1】它几乎就是一行代码
```html
<form id="formElem">
  <input type="text" name="name" value="John">
  <input type="text" name="surname" value="Smith">
  <input type="submit">
</form>

<script>
  formElem.onsubmit = async (e) => {
      e.preventDefault();
    let resposne = await fetch('/article/formdata/post/user', {
        method: 'POST',
      body: new FormData(formElem); // 一行代码
    });
    let result = await response.json();
    alert(result.message);
  }
</script>

发送带有文件的表单

一、表单始终以 Content-Type: multipart/form-data 来发送数据,这个编码允许发送文件。因此 字段也能被发送,类似于普通的表单提交。
【示例1】

<form id="formElem">
  <input type="text" name="firstName" value="John">
  Picture: <input type="file" name="picture" accept="image/*">
  <input type="submit">
</form>

<script>
  formElem.onsubmit = async (e) => {
    e.preventDefault();

    let response = await fetch('/article/formdata/post/user-avatar', {
      method: 'POST',
      body: new FormData(formElem)
    });

    let result = await response.json();

    alert(result.message);
  };
</script>

发送具有Blob数据的表单

一、以 Blob 发送一个动态生成的二进制数据,例如图片,是很简单的。我们可以直接将其作为 fetch 参数的 body。
1、但在实际中,通常更方便的发送图片的方式不是单独发送,而是将其作为表单的一部分,并带有附加字段(例如 “name” 和其他 metadata)一起发送。
2、并且,服务器通常更适合接收多部分编码的表单(multipart-encoded form),而不是原始的二进制数据。
【示例1】使用 FormData 将一个来自 的图片和一些其他字段一起作为一个表单提交:

<body style="margin:0">
  <canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>

  <input type="button" value="Submit" onclick="submit()">

  <script>
    canvasElem.onmousemove = function(e) {
      let ctx = canvasElem.getContext('2d');
      ctx.lineTo(e.clientX, e.clientY);
      ctx.stroke();
    };

    async function submit() {
      let imageBlob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));

      let formData = new FormData();
      formData.append("firstName", "John");
      formData.append("image", imageBlob, "image.png"); // 添加图片Blob。就像表单中有 <input type="file" name="image"> 一样,用户从他们的文件系统中使用数据 imageBlob(第二个参数)提交了一个名为 image.png(第三个参数)的文件

      let response = await fetch('/article/formdata/post/image-form', {
        method: 'POST',
        body: formData
      });
      let result = await response.json();
      alert(result.message);
    }

  </script>
</body>

1、服务器读取表单数据和文件,就好像它是常规的表单提交一样。

发送图片

一、可以使用Blob或BufferSource对象通过fetch提交二进制数据。
【示例1】有一个,可以通过在其上移动鼠标来进行绘制。点击”submit”按钮将图片发送到服务器

<body style="margin:0">
  <canvas id="canvasElem" width="100" height="80" style="border: 1px solid"></canvas>
  <input type="button" value="Submit" onclick="submit()">
  <script>
    canvasElem.onmousemove = function(e) {
        let ctx = canvasElem.getContext('2d');
      ctx.lineTo(e.clientX, e.clientY);
      ctx.stroke();
    }
    async function submit() {
        let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
      let response = await fetch('/article/fetch/post/image', {
          method: 'POST',
        body: blob,
      });
      // 服务器给出确认信息和图片大小作为响应
      let result = await response.json();
      alert(reslut.message);
    }
  </script>
</body>

1、这里我们没有手动设置Content-Type header,因为Blob对象具有内建的类型(这里是image/png,通过toBlob生成的)。对于Blob对象,这个类型就变成了Content-Type的值。
2、可以在不使用async/await的情况下重写submit()函数,像这样

function submit() {
    canvasElem.toBlob(function(blob) {
        fetch('/article/fetch/post/image', {
            method: 'POST',
            body: blob
        })
            .then(response => response.json())
            .then(reslut => alert(JSON.stringify(reslut, null, 2)))
    }, 'image/png');
}

referrer,referrerPolicy

referrer

一、这些选项决定了fetch如何设置HTTP的Referer header
二、通常来说,这个header是被自动设置的,并包含了发出请求的页面的url。在大多数情况下,它一点也不重要,但有时处于安全考虑,删除或缩短它是有意义的。
三、referer选项允许设置在当前域的任何Referer,或者移除它。
1、要不发送referer,可以将referer设置为空字符串

fetch('/page', {
    referrer: '' // 没有Referer header
})

2、设置在当前域内的另一个url:

fetch('/page', {
    // 假设我们在https://javascript.info
  // 我们可以设置任何Referer header,但必须是在当前域内
  referer: 'https://javascript.info/anotherpage'
})

referrerPolicy

一、referrerPolicy选项为Referer设置一般的规则
二、请求分为3种类型
1、同源请求
2、跨源请求
3、从HTTPS到HTTP的请求(从安全协议到不安全协议)
三、与referrer选项允许设置确切的Referer值不同,refererPolicy告诉浏览器针对各个请求类型的一般规则。
四、可能的值在Referrer Policy 规范中有详细描述 | 值 | 描述 | 同源 | 跨源 | HTTPS->HTTP | | —- | —- | —- | —- | —- | | “no-referrer” | 从不发送 Referer | - | - | - | | “no-referrer-when-downgrade” 或 “” | 默认值:除非我们从 HTTPS 发送请求到 HTTP(到安全性较低的协议),否则始终会发送完整的 Referer | 完整的 url | 完整的 url | - | | “origin” | 只发送在 Referer 中的域,而不是完整的页面 URL,例如,只发送 http://site.com而不是 http://site.com/path | 仅域 | 仅域 | 仅域 | | “origin-when-cross-origin” | 发送完整的 Referer 到相同的源,但对于跨源请求,只发送域部分(同上)。 | 完整的 url | 仅域 | 仅域 | | “same-origin” | 发送完整的 Referer 到相同的源,但对于跨源请求,不发送 Referer。 | 完整的 url | - | - | | “strict-origin” | 只发送域,对于 HTTPS→HTTP 请求,则不发送 Referer。 | 仅域 | 仅域 | - | | “strict-origin-when-cross-origin” | 对于同源情况下则发送完整的 Referer,对于跨源情况下,则只发送域,如果是 HTTPS→HTTP 请求,则什么都不发送。 | 完整的 url | 仅域 | - | | “unsafe-url” | 在 Referer 中始终发送完整的 url,即使是 HTTPS→HTTP 请求 | 完整的 url | 完整的 url | 完整的 url |

五、假如我们有一个带有URL结构的管理区域(admin zone),它不应该被网站外看到。
1、如果我们发送了一个fetch,则默认情况下,它总是发送带有页面完整url的Referer header(我们从HTTPS向HTTP发送请求的情况除外,这种情况下没有Referer)。
例如Referer: https://javascript.info/admin/secret/paths
2、如果我们想让其他网站只知道域的部分,而不是URL路径,我们可以这样设置选项

fetch('https://another.com/page', {
    // ...
  referrerPolicy: 'Origin-when-cross-origin' // Referer: https://javascript.info
})

3、我们可以将其置于所有fetch调用中,也可以将其集成到我们项目的执行所有请求并在内部使用fetch的JavaScript中
4、与默认行为相比,它的唯一区别在于,对于跨源请求,fetch只发送URL域的部分(例如https://javascript.info,没有路径)。对于同源请求,我们仍然可以获得完整的Referer(可能对于调试目的是有用的)。
六、Referrer policy不仅适用于fetch
1、可以使用Referrer-Policy HTTP header,或者为每个链接设置,来为整个页面设置默认策略(policy)。

mode

一、mode 选项是一种安全措施,可以防止偶发的跨源请求:
1、”cors” :默认值,允许跨源请求
2、”same-origin”: 禁止跨源请求,
3、”no-cors”: 只允许简单的跨源请求。
二、当 fetch 的 URL 来自于第三方,并且我们想要一个“断电开关”来限制跨源能力时,此选项可能很有用。

credentials

一、credentials 选项指定 fetch 是否应该随请求发送 cookie 和 HTTP-Authorization header。
1、”same-origin”: 默认值,对于跨源请求不发送,
2、”include” “ 总是发送,需要来自跨源服务器的 Accept-Control-Allow-Credentials,才能使 JavaScript 能够访问响应
3、”omit” —— 不发送,即使对于同源请求。

cache

一、默认情况下,fetch 请求使用标准的 HTTP 缓存。就是说,它遵从 Expires,Cache-Control header,发送 If-Modified-Since,等。就像常规的 HTTP 请求那样。
二、使用 cache 选项可以忽略 HTTP 缓存或者对其用法进行微调:
1、”default”: fetch 使用标准的 HTTP 缓存规则和 header
2、”no-store”: 完全忽略 HTTP 缓存,如果我们设置 header If-Modified-Since,If-None-Match,If-Unmodified-Since,If-Match,或 If-Range,则此模式会成为默认模式,
3、”reload”: 不从 HTTP 缓存中获取结果(如果有),而是使用响应填充缓存(如果 response header 允许),
4、”no-cache”: 如果有一个已缓存的响应,则创建一个有条件的请求,否则创建一个普通的请求。使用响应填充 HTTP 缓存
5、”force-cache” : 使用来自 HTTP 缓存的响应,即使该响应已过时(stale)。
(1)如果 HTTP 缓存中没有响应,则创建一个常规的 HTTP 请求,行为像正常那样
6、”only-if-cached” : 使用来自 HTTP 缓存的响应,即使该响应已过时(stale)。
(1)如果 HTTP 缓存中没有响应,则报错。
(2)只有当 mode 为 same-origin 时生效。

redirect

一、通常来说,fetch 透明地遵循 HTTP 重定向,例如 301,302 等。
二、redirect 选项允许对此进行更改:
1、”follow” : 默认值,遵循 HTTP 重定向,
2、”error” : HTTP 重定向时报错,
3、”manual” : 不遵循 HTTP 重定向,但 response.url 将是一个新的 URL,并且 response redirectd 将为 true,以便我们能够手动执行重定向到新的 URL(如果需要的话)。

integrity

一、integrity 选项允许检查响应是否与已知的预先校验和相匹配。
二、正如 规范 所描述的,支持的哈希函数有 SHA-256,SHA-384,和 SHA-512,可能还有其他的,这取决于浏览器。
【示例1】我们下载一个文件,并且我们知道它的 SHA-256 校验和为 “abcdef”(当然,实际校验和会更长)。
我们可以将其放在 integrity 选项中,就像这样:

fetch('http://site.com/file', {
  integrity: 'sha256-abcdef'
});

然后 fetch 将自行计算 SHA-256 并将其与我们的字符串进行比较。如果不匹配,则会触发错误。

keepalive

一、keepalive 选项表示该请求可能会使发起它的网页“失活(outlive)”。
【示例1】我们收集有关当前访问者是如何使用我们的页面(鼠标点击,他查看的页面片段)的统计信息,以分析和改善用户体验。
1、当访问者离开我们的网页时 —— 我们希望能够将数据保存到我们的服务器上。
(1)我们可以使用 window.onunload 事件来实现:

window.onunload = function() {
  fetch('/analytics', {
    method: 'POST',
    body: "statistics",
    keepalive: true
  });
};

二、通常,当一个文档被卸载时(unloaded),所有相关的网络请求都会被中止。但是,keepalive 选项告诉浏览器,即使在离开页面后,也要在后台执行请求。所以,此选项对于我们的请求成功至关重要。
三、它有一些限制:
1、我们无法发送兆字节的数据:keepalive请求的 body 限制为 64kb。
(1)如果我们需要收集有关访问的大量统计信息,我们则应该将其定期以数据包的形式发送出去,这样就不会留下太多数据给最后的 onunload 请求了。
(2)此限制是被应用于当前所有 keepalive 请求的总和的。换句话说,我们可以并行执行多个 keepalive请求,但它们的 body 长度之和不得超过 64KB。
2、如果文档(document)已卸载(unloaded),我们就无法处理服务器响应。因此,在我们的示例中,因为keepalive,所以fetch会成功,但是后续的函数将无法正常工作。
(1)在大多数情况下,例如发送统计信息,这不是问题,因为服务器只接收数据,并通常向此类请求发送空的响应。

signal:Fetch中止(Abort)

一、fetch返回一个promise。JavaScript通常并没有“中止”promise的概念。
二、中止fetch的场景
1、用户在我们网站上的操作表明不再需要fetch
三、特殊的内建对象:AbortController。它不仅可以中止fetch,还可以中止其他异步任务。

AbortController对象

一、创建一个控制器(controller)

let controller = new AbortController();

二、控制器是一个极其简单的对象
1、属性
(1)signal:可以在这个属性上设置事件监听
2、方法
(1)abort()
三、当abort()被调用时
1、controller.signal就会触发abort事件
2、controller.signal.aborted属性变为true
四、通常,处理分为两部分
1、一部分是一个可取消的操作,它在controller.signal上设置一个监听器。
2、另一部分是取消:在需要的时候调用controller.abort()
五、【示例1】目前还没有fetch

let controller = new AbortController();
let signal = controller.signal;
// 可取消的操作这一部分
// 获取"signal"对象
// 将监听器设置在controller.abort()被调用时触发
signal.addEventListener('abort', () => alert('abort!'));
// 另一部分,取消(在之后的任何时候)
controller.abort(); // 中止
// 事件触发,signal.aborted变为true
alert(signal.aborted); // true

1、AbortController只是在abort()被调用时传递abort事件的一种方式。
2、我们可以自己在代码中实现相同类型的事件监听,而根本不需要AbortController对象。
3、fetch知道如何与AbortController对象一起工作,它俩是集成在一起的。

与fetch一起使用

一、为了能够取消fetch,可以将AbortController的signal属性作为fetch的一个可选参数(option)进行传递

let controller = new AbortController();
fetch(url, {
    signal: controller.signal // fetch从signal获取了事件并中止了请求
})

二、fetch方法知道如何与AbortController一起工作。它会监听signal上的abort事件。
三、想要中止fetch,调用controller.abort()即可

controller.abort()

四、当一个fetch被中止,它的promise就会以一个error AbortError reject,因此我们应该对其进行处理,例如在try…catch中。
五、fetch在1秒后中止的完整示例

// 1 秒后中止
let controller = new AbortController();
setTimeout(() => controller.abort(), 1000);

try {
  let response = await fetch('/article/fetch-abort/demo/hang', {
    signal: controller.signal
  });
} catch(err) {
  if (err.name == 'AbortError') { // handle abort()
    alert("Aborted!");
  } else {
    throw err;
  }
}

AbortController 是可伸缩的

一、AbortController 是可伸缩的,它允许一次取消多个 fetch。
【示例1】该代码并行 fetch 很多 urls,并使用单个控制器将其全部中止:

let urls = [...]; // 要并行 fetch 的 url 列表
let controller = new AbortController();

// 一个 fetch promise 的数组
let fetchJobs = urls.map(url => fetch(url, {
  signal: controller.signal
}));

let results = await Promise.all(fetchJobs);

// 如果 controller.abort() 被从其他地方调用,
// 它将中止所有 fetch

二、如果我们有自己的与 fetch 不同的异步任务,我们可以使用单个 AbortController 中止这些任务以及 fetch。
1、在我们的任务中,我们只需要监听其 abort 事件:

let urls = [...];
let controller = new AbortController();

let ourJob = new Promise((resolve, reject) => { // 我们的任务
  ...
  controller.signal.addEventListener('abort', reject);
});

let fetchJobs = urls.map(url => fetch(url, { // fetches
  signal: controller.signal
}));

// 等待完成我们的任务和所有 fetch
let results = await Promise.all([...fetchJobs, ourJob]);

// 如果 controller.abort() 被从其他地方调用,
// 它将中止所有 fetch 和 ourJob