目录

🥢 JavaScript Fetch

# fetch 的使用

fetch() 方法是一种现代通用网络请求的方法:

let promise = fetch(url, [options])
1
  • url :请求网络路径;
  • options :可选参数(方法、请求头等)。没有可选参数,默认请求方法 get

使用 fetch 发送网络请求后,获取来自服务器的响应需要经过两个阶段:

  • 第一阶段:当服务器发送了响应头, fetch 返回的 promise 就使用内建的 Response 类对象来 对响应头进行解析

    可以在这个阶段,检查响应头,检查 HTTP 状态确定请求是否成功了,这个阶段还没有响应体。

    • 如果 fetch 无法建立一个 HTTP 请求。例如,网络问题;请求网络地址不存在,此时 promise 就会 reject
    • 异常的 HTTP 状态,例如 404 或 500,不会导致出现 error。

    请求 HTTP 状态可以通过两个属性查看:

    • status :HTTP 状态码;
    • ok :布尔值,如果 HTTP 状态码为 200-299,则为 true

    🌰 例子:

    let response = await fetch(url);
    
    if (response.ok) { // 如果 HTTP 状态码为 200-299
      // 获取 response body(此方法会在下面解释)
      let json = await response.json();
    } else {
      alert("HTTP-Error: " + response.status);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
  • 第二阶段:获取 response 响应体,使用一个其他的方法调用。

    Response 提供了多种基于 promise 的方法,来以不同的格式访问 resposne body:

    • response.text() :读取 response ,并以文本形式返回 response;
    • response.json() :将 response 解析为 JSON;
    • response.formData() :以 FormData 对象的形式返回 response;
    • response.blob() :以 Blob 的形式返回 response;
    • response.arrayBuffer() :以 ArrayBuffer 形式返回 response;
    • response.body :是 ReadableStream 对象,允许逐块读取 body

🌰 例子 / 获取响应的 JSON 对象:

let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits';
let response = await fetch(url);

let commits = await response.json();
console.log(commits[0].author.login)
1
2
3
4
5

或者使用纯 promise

fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
  .then(response => response.json())
  .then(commits => alert(commits[0].author.login));
1
2
3

注意

一般只能选择一种读取 body 的方法。例如,如果已经使用了 response.text() 来获取 response , 那么再用 response.json() 不会生效。因为响应体内容已经被处理过了。

# 响应头 Response header

Response header 是位于 response.headers 中的一个类似于 Map 的 header 对象。(不是真正的 Map 对象) 可以通过 名字 获取各个 header ,或者迭代。

🌰 例子 / 获取响应头 Content-type

let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');

// 获取一个 header
alert(response.headers.get('Content-Type')); // application/json; charset=utf-8

// 迭代所有 header
for (let [key, value] of response.headers) {
  alert(`${key} = ${value}`);
}
1
2
3
4
5
6
7
8
9

# 请求头 Request header

可以通过在使用 fetch 时,在选择参数中配置 headers 选项。

🌰 例子:

let response = fetch(protectedUrl, {
  headers: {
    Authentication: 'secret'
  }
})
1
2
3
4
5

为了保证 HTTP 的正确性和安全性,存在不能控制的请求头:https://fetch.spec.whatwg.org/#forbidden-header-name。仅有浏览器控制。

# 请求方法 method

要使用 默认方法 GET 以外的请求方法,通过 method 选项配置 / 或者配置 body (request body, 常用 JSON)。

🌰 例子 / 以 JSON 形式发送对象类型的请求:

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()
console.log(result.message)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

如果请求的 body 是字符串,则 Content-Type 会默认设置为 text/plain;charset=UTF-8 。所以如果要发送 JSON,需要设置请求头 Content-type 控制编码类型。

🌰 例子 / 获取 GitHub 用户信息。

  • 创建一个异步函数 getUsers(names) ,该函数接受 GitHub 登录名数组作为输入,查询 GitHub 以获取有关这些用户的信息,并返回 GitHub 用户数组。
  • GitHub 查询用户信息的 API:https://api.github.com/users/USERNAME
async function getUsers(names) {
  let jobs = [];
  
  for (let name of names) {
    let job = fetch(`https://api.github.com/users/${name}`).then(
      response => {
        if (response.status != 200) return null;
        else return response.json();
      }, 
      error => {
        return null;
      }
    );
    jobs.push(job)
  }
  
  let results = await Promis.all(jobs)
  return results;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 对于 每一个用户名都有一个 fetch 请求。求不应该相互等待。以便能够尽快获取到数据。如果任何一个请求失败了,或者没有这个用户,则函数应该返回 null 到结果数组中。
  • .then 调用紧跟在 fetch 后面,这样当收到响应时,它不会等待其他的 fetch ,而是立即开始读取 .json() 。通过将 .json() 直接添加到每个 fetch 中,就能确保每个 fetch 在收到响应时都会立即开始以 JSON 格式读取数据,而不会彼此等待。
  • 使用 await Promise.all 将会等到所有的 fetch 都获取到响应数据才开始解析。

# Fetch 的应用

# FormData

用于发送 HTML 表单数据的类型对象。构造器使用如下:

let formData = new FormData([form])
1

[form] 是选择性提供的。如果提供了 HTML form 元素,就会自动捕获该 from 元素字段。

FormData 的特殊之处在于网络方法。例如 fetch 可以接受一个 FormData 对象作为 body。它会被编码并发送出去,带有 Content-Type: multipart/form-data

对于服务器,只是普通的表达数据提交。

🌰 例子 / 提交简单的表单数据:

<form id="formElem">
  <input type="text" name="name" value="John">
  <input type="text" name="surname" value="Smith">
  <input type="submit">
</form>
1
2
3
4
5
let formElem = document.querySelector("#formElem");
formElem.onsubmit = async (e) => {
  e.preventDefault
  
  let response = await fetch('/article/formdata/post/user', {
      method: 'POST',
      body: new FormData(formElem)
  });
  
  let result = await response.json()
  console.log(result.message)
}
1
2
3
4
5
6
7
8
9
10
11
12

FormData 提供的方法修改其中的字段:

  • formData.append(name, value) :添加具有给定 namevalue 的表单字段;
  • formData.append(name, blob, fileName) :添加一个相当于 <input type="file"> 输入类型的字段。第三个参数 fileName 设置文件名(而不是表单字段名),因为它是用户文件系统中文件的名称;
  • formData.delete(name) :移除带有给定 name 的字段;
  • formData.get(name) :获取带有给定 name 的字段值;
  • formData.has(name) :如果存在带有给定 name 的字段,则返回 true ,否则返回 false

技术上,可以拥有多个 相同名称 name 的字段。因此,多次调用 append 将会添加多个具有相同名称的字段。使用 set 方法,确保该字段只有一个唯一的值(移除之前存在的 name 字段)。

  • formData.set(name, value)

  • formData.set(name, blob, fileName)

    与使用 append 的方法相同

可以使用 for ... of 循环迭代 formData 中的字段与值:

for (let [name, value] of formData) {
  console.log(`${name} = ${value}`)
}
1
2
3

# FormData 表单上传文件

FormData 表单类型始终带有请求头 Content-type: multipart/form-data 发送数据,这个编码允许发送文件 ( <input type="file"> 类型的输入框)。

🌰 例子 / 上传图片

<form id="formElem">
  <input type="text" name="firstName" value="John">
  Picture: <input type="file" name="picture" accept="image/*">
  <input type="submit">
</form>
1
2
3
4
5
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);
};
1
2
3
4
5
6
7
8
9
10
11
12

# Fetch 跨源请求

跨源请求(跨域) (跨源资源请求 Cross-Origin Resource Sharing / CORS ):发送到其他域、协议或者端口的请求。

允许跨源请求的两种请求类型:

  • 安全请求;
  • 所有的其他请求。

安全的请求需要满足两个条件:

  • 安全的请求方法: GETPOSTHEAD
  • 安全的请求头 header 仅允许自定义下列的 header:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type 的值为 application/x-www-form-urlencodedmultipart/form-datatext/plain

除了安全的请求,没有符合上述条件的请求都为非安全的请求。例如,使用 PUT 请求方法,或者具有除了上述的自定义的请求头部。

安全的请求与非安全的请求的本质区别:

  • 可以使用 <form> / script 进行安全请求,无需特殊的方法。

    使用安全的请求能兼容较旧的服务器,不用进行特殊的处理。

  • 而发送非安全的请求,无法进行同样的工作。

    旧的服务器可能会认为此类请求来自具有特权的来源。当尝试发送一个非安全请求时,浏览器会发送一个特殊的预检请求到服务器,询问服务是否接受此类跨源请求吗。

    服务器需要明确通过 header 确认,否则非安全的请求无法发送。

对于安全的请求 CORS

🌰 例子:

https://somewhere.page 请求 https://anywhere.com/request ,发送的请求头为:

GET /request
Host: anywhere.com
Origin: https://somewhere.page
1
2
3

请求头的 Origin 会包含确切的请求来源(域名、协议、端口),没有路径。服务器检查 Origin ,如果同意接受请求,就会在响应中添加一个特殊的 header Access-Control-Allow-Origin 。该 header 包含了允许的源,或者一个星号 * 。然后响应成功,否则报错。

浏览器作为中间人:确保了发送的跨源请求带有正确的 Origin;检查响应头中是否带有许可 Access-Control-Allow-Origin ,如果存在,则允许 JavaScript 访问响应,否则将失败并报错。

响应的请求头:

200 OK
Content-Type:text/html; charset=UTF-8
Access-Control-Allow-Origin: *
1
2
3

对于响应头 (Response Header)处理跨源请求时,JavaScript 只能访问 安全的响应头:

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

对于其他的响应头都会导致出错。

要授予 JavaScript 对任何其他请求的访问权限,服务器必须发送 Access-Control-Expose-Headers header。它包含一个以逗号分隔的应该被设置为可访问的非安全 header 名称列表。

对于非安全的请求

为了使用任何的 HTTP 请求方法(不仅 GET / POST,还有可能是 PATCH、DELETE 或者其他)。

  • 浏览器发送预检请求,来请求许可:

    预检请求使用 OPTIONS 方法,它没有 body,但是有三个 header:

    • Access-Control-Request-Method header 带有非安全请求的方法。
    • Access-Control-Request-Headers header 提供一个以逗号分隔的非安全 HTTP-header 列表。
  • 如果服务器同意处理请求,那么会进行响应以下响应头信息:

    • Access-Control-Allow-Origin 必须为 * 或进行请求的源才能允许此请求。
    • Access-Control-Allow-Methods 必须具有允许的方法。
    • Access-Control-Allow-Headers 必须具有一个允许的 header 列表。
    • Access-Control-Max-Age 可以指定缓存此权限的秒数。因此,浏览器不是必须为满足给定权限的后续请求发送预检。

🌰 例子:

  • 发送预检请求 /preflight request:

    OPTIONS /service.json
    Host: site.com
    Origin: https://somewhere.info
    Access-Control-Request-Method: PATCH
    Access-Control-Request-Headers: Content-Type,API-Key
    
    1
    2
    3
    4
    5
    • Origin : 来源。
    • Access-Control-Request-Method : 请求方法。
    • Access-Control-Request-Headers : 以逗号分隔的非安全 header 列表。
  • 服务器响应预检响应 /preflight response:

    Access-Control-Allow-Origin: https://somewhere.page
    Access-Control-Allow-Methods: PUT,PATCH,DELETE
    Access-Control-Allow-Headers: Content-Type,API-Key
    Access-Control-Max-Age: 86400
    
    1
    2
    3
    4

    这将允许后续通信,否则会触发错误。如果服务器将来需要其他方法和 header,则可以通过将这些方法和 header 添加到列表中来预先允许它们。

    可以看到 PATCHAccess-Control-Allow-Methods 中, Content-Type,API-Key 在列表 Access-Control-Allow-Headers 中,因此它将发送主请求。

    Access-Control-Max-Age 带有一个表示秒的数字,则在给定的时间内,预检权限会被缓存。上面的响应将被缓存 86400 秒,也就是一天。在此时间范围内,后续请求将不会触发预检。假设它们符合缓存的配额,则将直接发送它们。

  • 发送实际请求:

    最后的请求头:

    PATCH /service.json
    Host: site.com
    Content-Type: application/json
    API-Key: secret
    Origin: https://javascript.info
    
    1
    2
    3
    4
    5

    响应头应该添加 Access-Control-Allow-Origin

    例如:

    Access-Control-Allow-Origin: https://somewhere.pages
    
    1

# Fetch 请求凭证

默认情况下,由 JavaScript 代码发起的跨源请求,不会带任何凭据(cookies 或者 HTTP 网络认证)。

因为具有凭据的请求比没有凭据的请求要强大得多。如果被允许,它会使用它们的凭据授予 JavaScript 代表用户行为和访问敏感信息的全部权力。

所以要在 跨源请求 必须显式地带有允许请求的凭据和附加 header。

例子:要在 fetch 中发送凭据,需要添加 credentials: "include" 选项:

fetch('http://another.com', {
  credentials: "include"
});
1
2
3

现在, fetch 将把源自 another.com 的 cookie 和我们的请求发送到该网站。

如果服务器同意接受 带有凭据 的请求,则除了 Access-Control-Allow-Origin 外,服务器还应该在响应中添加 header Access-Control-Allow-Credentials: true

响应头如下:

200 OK
Access-Control-Allow-Origin: https://somewhere.pages
Access-Control-Allow-Credentials: true
1
2
3

注意:对于具有凭据的请求,禁止 Access-Control-Allow-Origin 使用星号 * 。如上所示,它必须有一个确切的源。这是另一项安全措施,以确保服务器真的知道它信任的发出此请求的是谁。

📢 上次更新: 2022/09/02, 10:18:16