1

HTTP请求的几种方式 - 风吹草

 1 year ago
source link: https://www.cnblogs.com/jann8/p/17472129.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

1.HTTP请求简介[1]

HTTP(Hypertest Transfer Protocol)是用于传输像HTML这样的超文本文件的应用层协议。它被设计用于WEB浏览器端和WEB服务端的交互,但也有其它用途。HTTP遵循经典的client-server模型,客户端发起请求尝试建立连接,然后等待服务端的应答。HTTP是无状态协议,这意味着服务端在两次请求间不会记录任何状态。

2.HTTP请求内容

2.1请求URL

每个请求有一个请求URL。

2.2请求方法[2]:

HTTP定义了一系列请求方法,这些方法表明要对给定资源所做的操作。HTTP请求方法包含GET、HEAD、POST、PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH等8种类型。

2.3应答状态码[3]

HTTP应答状态码表名一个HTTP请求是否成功完成。应答状态码被分为5类:

信息应答(100199)

成功应答(200299)

重定向信息(300399)

客户端错误(400499)

服务端错误(500599)

2.4 HTTP头[4]

HTTP头使得客户端和服务端之间可以通过HTTP请求和应答传递信息。HTTP头包含大小写敏感的名称,后面跟一个“:”,然后是http头的值。HTTP的值前面的空格会被忽略。

2.4.1 Authentication
  • WWW-Authenticate: 请求资源时所用的认证方法。可为Basic、Negotiate、NTLM等[5]。
  • Authorization: 包含服务端验证用户的凭据。
2.4.2 Cookies
  • Cookie: 包含上一次服务端发送的Set-cookie头中的HTTP cookies。
  • Set-Cookie: 从服务端向用户侧发送Cookie。
2.4.3 CORS
  • Access-Control-Allow-Origin:表明应答可以与哪些Origin共享。

2.5 请求体[6]

请求体(body)是Request接口的一个属性,它是包含请求体内容的可读流。GET和HEAD请求不能携带请求体,如果携带请求体会返回null。

Demo1发起了一个POST类型的请求,在Chrome的开发者工具中可以看到请求URL、请求方法、应答状态码、HTTP头和请求体等信息,如图1。

var xhttp = new XMLHttpRequest();
xhttp.open("POST", "http://localhost:8080/day05_lzs/", true);
xhttp.send("fname=Bill&lname=Gates");

Demo1. 使用XMLHttpRequest发起一个简单的POST请求

1389306-20230610222701185-2017999689.png

图1. Chrome浏览器中使用XMLHttpRequest发起的一个简单的POST请求的请求信息

3.几种请求的方式

3.1 document类型的请求

如图2,从Chrome浏览器的开发者工具可以看出,请求的类型分为Fetch/XHR、JS、CSS、Img、Media、Font、Doc、WS (WebSocket)、 Wasm (WebAssembly)、Manifest和Other(不属于前面列出类型中的一种)。通过浏览器地址栏发起的请求和form表单请求的类型都是document。

1389306-20230610222745375-1470659185.png

图2. Chrome浏览器开发者工具中请求的几种类型

3.1.1 通过浏览器地址栏发起的请求

当我们访问一个web页面时,在浏览器地址栏输入访问的地址并确认后,会发起document类型的请求,如图3。

1389306-20230610222759510-1087737908.png

图3. 使用Chrome浏览器在地址栏发起的document类型的请求

3.1.2 Form请求

Demo2是一个form表单。当form表单提交时,会发送一个document类型的请求,如图4。

<form action="http://127.0.0.1:8080/day05_lzs">
    <fieldset>
        <legend>Personal information:</legend>
        First name:<br>
        <input type="text" name="firstname" value="Mickey">
        <br>
        Last name:<br>
        <input type="text" name="lastname" value="Mouse">
        <br><br>
        <input type="submit" value="Submit"></fieldset>
</form>

Demo2. 一个form表单的HTML代码

1389306-20230610222813617-2057570073.png

图4. 使用Chrome浏览器form表单提交时发起document类型的请求

3.2 XHR请求

3.2.1 XMLHttpRequest简介

XMLHttpRequest(XHR) 对象用于和服务端交互。它可以从URL取回数据而不用刷新整个页面,这使得可以只更新页面的局部而不影响整个页面。[7]

如Demo3所示,使用XMLHttpRequest发送GET请求,在URL中添加“?fname=Bill&lname=Gates”实现发送信息。XMLHttpResponse 对象的 onreadystatechange 属性中定义了请求接收到应答是所执行的操作。该GET请求的在浏览器中的请求信息如图5所示。

//GET请求
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
        console.log("请求成功回调")
    }
};

xhttp.open("GET", "http://localhost:8080/day05_lzs/?fname=Bill&lname=Gates", true);
xhttp.send();

Demo3. 使用XMLHttpRequest发送GET请求

1389306-20230610222916783-777356348.png

图5. 使用XMLHttpRequest发送GET请求

3.2.2 使用XMLHttpRequest发送post请求的4种方式

如Demo3所示,使用XMLHttpRequest发送POST请求,发送数据的方式有4种[8]。4中POST请求在浏览器中的请求信息如图6-9所示,他们具有不同的请求头content_type,请求体的格式分为“Request Load”和“Form Data”两种。

//POST请求,发送数据方式一
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "http://localhost:8080/day05_lzs/", true);
xhttp.send("fname=Bill&lname=Gates");

//POST请求,发送数据方式二
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "http://localhost:8080/day05_lzs/", true);
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhttp.send("fname=Bill&lname=Gates");

//POST请求,发送数据方式三
var xhttp = new XMLHttpRequest();
const formData = new FormData();
formData.append("fname", "Bill");
formData.append("lname", "Gates"); 
xhttp.open("POST", "http://localhost:8080/day05_lzs/", true);
xhttp.send(formData);

//POST请求,发送数据方式四
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "http://localhost:8080/day05_lzs/", true);
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.send('{"fname":"Bill","lname":"Gates"}');

Demo3. 使用XMLHttpRequest发送POST请求时发送数据的四种方式

1389306-20230610223139651-880342569.png

图6. 使用XMLHttpRequest发送POST请求时发送数据方式一

1389306-20230610223201053-1620889213.png

图7.使用XMLHttpRequest发送POST请求时发送数据方式二

1389306-20230610223223291-2112175519.png

图8.使用XMLHttpRequest发送POST请求时发送数据方式三

1389306-20230610223235062-1874598344.png

图9. 使用XMLHttpRequest发送POST请求时发送数据方式四

3.2.3 JQuery.AJAX()简介[9]

AJAX 组合了XMLHttpRequest 对象、JavaScript 和 HTML DOM;XMLHttpRequest 对象用于从 web 服务器请求数据,JavaScript 和 HTML DOM用于显示或使用数据。不同的浏览器对于AJAX的使用方式可能有所不同,JQUERY中解决了这个问题。在JQUERY使用ajax只需一行代码。

//方式一
$.ajax({ url: "redirectTest",method:"GET",async:true,
        success: function(){
    console.log("ajax请求成功回调")
}});

//方式二
$.get("redirectTest",function(){console.log("ajax请求成功回调")});

Demo4. JQuery中发送ajax请求的2中方式

3.2.4 AJAX请求跨域时的浏览器策略

为了减少跨域请求的风险(比如csrf),浏览器对从脚本中发起的跨域HTTP请求有严格限制。比如,XMLHttpRequest和Fetch这两个API都遵循同源策略(same-origin policy),而对form表单提交的、浏览器地址栏发起的、HTML重定向和JavaScript重定向(详见3.4.2和3.4.3节)等document类型的请求则没有限制。同源策略是指,浏览器只能加载相同初始域名(origin domain)的应答,如果要加载其它域名的应答,应答头中必须包含必要的CORS头,比如“Access-Control-Allow-Origin”[10]。如Demo5,由初始域名为localhost:8080向目标域名为127.0.0.1:8080发送ajax请求时,浏览器会报出如图10的“CORS error”错误,错误的详细信息如图11。可以通过在CORSFilter对请求进行拦截(如Demo6),设置应答头”Access-Control-Allow-Origin:*“,再次发送跨域请求,浏览器不再报出”CORS error“的错误。

//这是localhost:8080/day05_lzs请求响应的index.html页面,即初始域名为localhost:8080

var xhttp = new XMLHttpRequest();
//向目标域名127.0.0.1:8080发送跨域请求
xhttp.open("GET", "http://127.0.0.1:8080/day05_lzs/?fname=Bill&lname=Gates", true);
xhttp.send();

Demo5. 初始域名为localhost:8080,向目标域名为127.0.0.1:8080发送ajax请求

1389306-20230610223302112-467981062.png

图10. 跨域发送ajax请求时,浏览器报出“CORS error”

1389306-20230610223314101-1128029319.png

图11. 跨域发送ajax请求时,浏览器控制台输出的详细错误信息

public class CORSFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //允许所有的初始域名(origin domain)加载该应答
        rep.setHeader("Access-Control-Allow-Origin","*");
    }
}

Demo6. 设置应答头"Access-Control-Allow-Origin",已解决跨域问题

3.3 Fetch请求

3.3.1 Fetch简介[11]

Fetch API提供了获取资源的接口,它比XMLHttpRequest更强大和灵活。使用fetch()方法可以发起请求并获取资源。fetch()是一个在Window和Worker的context中的全局方法,这使得可以在任何的context下使用fetch方法。fetch()方法有一个参数必须要有,需要获取资源的路径。它应答一个Promise,Promise会被解析为Response。

与XMLHttpRequest属于callback-based API不同,Fetch是promise-based的并可以很容易地在service worker中[12][13]使用。Fetch也整合了前沿的HTTP概念,像HTTP中的CORS及其它扩展。

一个简单的fetch请求看起来如下图:示例中使用了async/await。“async function”声明了一个async函数,在函数体中可以使用await。async/await使得异步的、基于promise的代码实现更加简洁,避免了额外配置复杂的promise链[14]。

async function logJSONData() {
  const response = await fetch("http://example.com/movies.json");
  const jsonData = await response.json();
  console.log(jsonData);
}

Demo7. 发起一个简单的fetch请求

3.3.1.1 案例一:

如Demo8,fetch()方法可以接收包含多个配置的对象作为第二个参数。mode(请求跨源模式)中可取值为 no-cors, cors, same-origin,默认值是cors。出于安全原因,在Chromium中,no-cors曾一度只在Service Worker中可用,其余环境会直接拒绝相应的promise[15](后来已经重新可用[16])。credentials表示请求是否需要带上认证凭据,可取值为omitsame-origininclude,默认值为same-origin

// Example POST method implementation:
async function postData(url = "", data = {}) {
  // Default options are marked with *
  const response = await fetch(url, {
    method: "POST", // *GET, POST, PUT, DELETE, etc.
    mode: "cors", // no-cors, *cors, same-origin
    cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
    credentials: "same-origin", // include, *same-origin, omit
    headers: {
      "Content-Type": "application/json",
      // 'Content-Type': 'application/x-www-form-urlencoded',
    },
    redirect: "follow", // manual, *follow, error
    referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
    body: JSON.stringify(data), // body data type must match "Content-Type" header
  });
  return response.json(); // parses JSON response into native JavaScript objects
}

postData("https://example.com/answer", { answer: 42 }).then((data) => {
  console.log(data); // JSON data parsed by `data.json()` call
});

Demo8. 使用fetch()发送请求案例一

3.3.1.2 案例二:

如Demo9,使用HTML标签<input type="file" />、FormData()和fetch()进行文件上传。

async function upload(formData) {
  try {
    const response = await fetch("https://example.com/profile/avatar", {
      method: "PUT",
      body: formData,
    });
    const result = await response.json();
    console.log("Success:", result);
  } catch (error) {
    console.error("Error:", error);
  }
}

const formData = new FormData();
const fileField = document.querySelector('input[type="file"]');

formData.append("username", "abc123");
formData.append("avatar", fileField.files[0]);

upload(formData);

Demo9. 使用fetch()发送请求案例二

3.3.1.3 案例三:

与jQuery.ajax()请求不同,fetch()返回的promise,即使状态码为404或500,也不会被判断为网络错误而被拒绝。当网络错误发生或CORS配置错误时,才会被拒绝。所以判断fetch()是否成功包括判断解析出的promise,判断Reponse.ok是否为true,如Demo10。

async function fetchImage() {
  try {
    const response = await fetch("flowers.jpg");
    if (!response.ok) {
      throw new Error("Network response was not OK");
    }
    const myBlob = await response.blob();
    myImage.src = URL.createObjectURL(myBlob);
  } catch (error) {
    console.error("There has been a problem with your fetch operation:", error);
  }
}

Demo10. 使用fetch()发送请求案例三

3.3.1.4 案例四:

如Demo11,除了直接将请求路径直接传递到fetch()方法之外,也可以先创建Request()构造器,并将该构造器作为fetch()方法的参数传递。

async function fetchImage(request) {
  try {
    const response = await fetch(request);
    if (!response.ok) {
      throw new Error("Network response was not OK");
    }
    const myBlob = await response.blob();
    myImage.src = URL.createObjectURL(myBlob);
  } catch (error) {
    console.error("Error:", error);
  }
}

const myHeaders = new Headers();

const myRequest = new Request("flowers.jpg", {
  method: "GET",
  headers: myHeaders,
  mode: "cors",
  cache: "default",
});

fetchImage(myRequest);

Demo11. 使用fetch()发送请求案例四

​ fetch()与jQuery.ajax()的主要区别有以下2点:

1)当应答的HTTP状态码为404或500时,fetch()返回的promise不会被作为HTTP错误而被拒绝。在服务器应答后,当应答的状态码在200-299之间时,则reponse.ok的值为true,否则为false。promise被拒绝仅在网络错误或CORS配置错误等而导致请求无法完成时发生。

2)除非fetch()方法中的credentials配置项配置为include,否则fetch()将:

  • 在跨域请求时不发送cookies;
  • 跨域应答中不会设置任何cookies;
  • 从2018开始,默认的credentials策略默认改为same-origin。
3.3.2 Promise
3.3.2.1 Promise简介

Promise对象代表一个异步操作的完成状态和结果。Promise代表的完成状态和结果可以在将来某个时间点用到。在Demo12中创建了一个简单的Promise,这个Promise的resolve()方法的参数值被方法.then()中的成功状态下的回调函数所用到。Promise的.then()方法有两个参数,第一个参数是成功状态下的回调函数,第二个参数是失败状态下的回调函数,第一个参数是必须要有的。.then()方法的返回值还是一个Promise,如果.then()中回调函数的完成状态为成功,则可以出现像Demo2中的Promise链。[17]

const myFirstPromise = new Promise((resolve, reject) => {
    resolve("Success!");
});

myFirstPromise.then((successMessage) => {
  console.log(`Yay! ${successMessage}`);
});

//输出结果为:Yay! Success!

Demo12. 创建一个简单的Promise和Promise链

Demo13将Promise的.then()方法中的回调函数抽取独立的函数了。最下面是Promise链,它包含3个.then()调用,1个.catch()调用,最后是finally()调用。只有1个.catch()是通常的做法,将promise链中所有的失败状状态的回调函数去掉,只需要在promise的最后添加1个.catch()即可。Promise链开头创建Promise时的参数为函数tetheredGetNumber,函数中resolve()表示完成状态为成功,reject()表示完成状态为失败。可以看出Promise代表的完成状态和结果是不确定的。第一个.then()方法中的失败回调函数troubleWithGetNumber是可以去掉的,因为在promise链最后有了.catch()方法。.catch(failureCallback)方法可以看成是.then(null,failureCallback)的简写。[17]

// To experiment with error handling, "threshold" values cause errors randomly
const THRESHOLD_A = 8; // can use zero 0 to guarantee error

function tetheredGetNumber(resolve, reject) {
    const randomInt = Date.now();
    const value = randomInt % 10;
    if (value < THRESHOLD_A) {
      resolve(value);
    } else {
      reject(`Too large: ${value}`);
    }
}

function determineParity(value) {
  const isOdd = value % 2 === 1;
  return { value, isOdd };
}

function troubleWithGetNumber(reason) {
  const err = new Error("Trouble getting number", { cause: reason });
  console.error(err);
  throw err;
}

function promiseGetWord(parityInfo) {
  return new Promise((resolve, reject) => {
    const { value, isOdd } = parityInfo;
    if (value >= THRESHOLD_A - 1) {
      reject(`Still too large: ${value}`);
    } else {
      parityInfo.wordEvenOdd = isOdd ? "odd" : "even";
      resolve(parityInfo);
    }
  });
}

new Promise(tetheredGetNumber)
  .then(determineParity, troubleWithGetNumber)
  .then(promiseGetWord)
  .then((info) => {
    console.log(`Got: ${info.value}, ${info.wordEvenOdd}`);
    return info;
  })
  .catch((reason) => {
    if (reason.cause) {
      console.error("Had previously handled error");
    } else {
      console.error(`Trouble with promiseGetWord(): ${reason}`);
    }
  })
  .finally((info) => console.log("All done"));

Demo13. 一个使用Promise链的案例

Promise链是魔法一般的存在,Promise链的传统写法是金字塔式的,看着很不优雅。一个简单的Promise如Demo14所示,这里不考虑Promise是如何创建的,所以用doSomething()表示一个promise对象。Demo14中的createAudioFileAsync()根据给定的参数记录生成了音像文件,并且有2个回调函数,1个在音像文件创建成功时使用,1个在音像文件创建失败后使用。示例的传统写法如Demo15所示。可以看出,单个.then()方法的promise写法与传统写法差别很小,但使用2个及以上.then()方法的Promise链与传统写法的差异很大。

//promise调用的基本结构
const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);

//一个promise示例
createAudioFileAsync(audioSettings).then(successCallback, failureCallback);
function successCallback(result) {
  console.log(`Audio file ready at URL: ${result}`);
}

function failureCallback(error) {
  console.error(`Error generating audio file: ${error}`);
}

Demo14 promise调用的基本结构与示例

//传统写法
createAudioFileAsync(audioSettings, successCallback, failureCallback);

Demo15. promise调用示例(Demo14)的传统写法

在一次执行多个异步操作,且下一个异步操作在上一个异步操作成功后被执行,并使用上一个异步操作成功后的结果,是一个常见的需求。连续进行几个异步操作传统的写法是金子塔式的,如Demo16所示。你也可以使用Promise链来实现这个需求,如图Demo17所示。使用Promise链使多个异步操作更简洁。Demo17也可以使用箭头函数来实现,如Demo18。

doSomething(function (result) {
  doSomethingElse(result, function (newResult) {
    doThirdThing(newResult, function (finalResult) {
      console.log(`Got the final result: ${finalResult}`);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

Demo16. 连续多个操作的传统写法,下一个异步操作在上一个异步操作后执行且使用上一个异步操作的结果

doSomething()
  .then(function (result) {
    return doSomethingElse(result);
  })
  .then(function (newResult) {
    return doThirdThing(newResult);
  })
  .then(function (finalResult) {
    console.log(`Got the final result: ${finalResult}`);
  })
  .catch(failureCallback);

Demo17. 使用Promise链实现连续多个操作,下一个异步操作在上一个操作后执行且使用上一个异步操作的结果

doSomething()
  .then((result) => doSomethingElse(result))
  .then((newResult) => doThirdThing(newResult))
  .then((finalResult) => {
    console.log(`Got the final result: ${finalResult}`);
  })
  .catch(failureCallback);

Demo18. Demo17使用箭头函数实现的代码

3.3.2.2 Promise使用的常见错误

Promise使用的常见错误一是在Promise的.then()中忘记return了,这时使用使用这个.then()方法的返回值作为参数的下一个.then()方法将不能正常执行。由于无法获取上一个.then()方法的返回值,这个.then()方法将不会等待上一个.then()方法执行完再执行,两个.then()方法的执行时异步的,有竞争关系,如Demo19。如Demo20,如果上一个的.then()方法没有返回值,那么下一个.then()方法将会更早被调用,这导致控制台输出的listOfIngredients总是为[]。

doSomething()
  .then((url) => {
    // 忘记return了
    fetch(url);
  })
  .then((result) => {
    // 由于上一个.then()方法没有返回值,这个.then()不会等待上一次.then()方法执行完再执行,且result为undefined
  });

Demo19. Promise使用的常见错误.then()方法中忘记return了

const listOfIngredients = [];

doSomething()
  .then((url) => {
    // 忘记return了
    fetch(url)
      .then((res) => res.json())
      .then((data) => {
        listOfIngredients.push(data);
      });
  })
  .then(() => {
    console.log(listOfIngredients);
    // 总是[],因为上一个.then()方法没有完成
  });

Demo20. Promise使用的常见错误.then()方法中忘记return了

Promise的.then()方法没有返回值是一个常见的错误,除此之外还有一些其它的常见错误,Demo21这个案例中包含了3个常见的错误。第一个错误是Promise的.then()方法没有返回值的错误,已经介绍过。第二个错误是Promise的嵌套是不必要的。第三个错误是Promise链的结尾没有加上.catch()方法。对该案例错误修改后的代码如Demo22。

// 错误案例,命中3个错误

doSomething()
  .then(function (result) {
    // 1.忘记return了
    // 2.不必要的嵌套
    doSomethingElse(result).then((newResult) => doThirdThing(newResult));
  })
  .then(() => doFourthThing());
// 3.忘记加上.catch()方法

Demo21. Promise使用的常见3个错误

doSomething()
  .then(function (result) {
   
    return doSomethingElse(result);
  })
  //如果这个.then()方法的返回值在下一个.then()方法中没有用到,下一个.then()方法不用写参数
  .then((newResult) => doThirdThing(newResult))
  .then((/* 参数不用写*/) => doFourthThing())
  .catch((error) => console.error(error));

Demo22. Demo21中常见的3个错误修改后的代码

3.3.2.3 Promise中的拒绝事件(reject_event)

如果一个promise的拒绝事件没有被任何处理程序处理,他将会冒泡到调用栈的顶部,主机需要将其抛出。在web上,当一个promise被拒绝,有两个事件中一个会被发送到全局范围(通常是通过window,如果使用了webworker,则通过worker或基于worker的接口)。这2个事件是:

unhandledrejection

当一个promise被拒绝但没有可用的拒绝事件的处理程序时,发送该事件。

rejectionhandled

当一个被拒绝的promise已引起unhandledrejection事件的发送,再次被拒绝时一个处理程序被附加到被拒绝的promise时发送该事件。

这两种情况下,类型为PromiseRejectionEvent的事件都有一个promise的属性作为成员,这个属性表明promise已被拒绝;还包括一个reason属性,这个属性提供了promise被拒绝的原因。

这使得为promises提供错误处理称为可能,也为你的promise管理的debug问题提供帮助。这些处理程序在每个context中是全局的,因此所有的错误不管源头在哪,都会去到同样的事件处理程序。

在Node.js中,promise拒绝的处理有些许不同。你可以通过为Node.js的unhandledRejection(注意与js中unhandledrejection事件的大小写区别)事件添加处理程序,来捕获未被处理的拒绝。

process.on("unhandledRejection", (reason, promise) => {
  // Add code here to examine the "promise" and "reason" values
});

Demo22. nodejs中promise拒绝的处理

对于node.js,为了阻止错误输出到控制台,你可以添加process.on监听器,取代浏览器运行时的preventDefault() 方法。如果你添加了process.on监听器,但是并没有对被拒绝的promise进行处理,那这个被拒绝的信息将被默默忽略掉。你应该在监听器中添加代码以检查每个被拒绝的promise并确认是否实际上由代码的bug引起。

3.3.2.4 Promise中的异常处理

谈到Promise的异常处理,你可能会想到早期金字塔式的连续异步操作示例中的failurecallback方法,如Demo23所示。在Promise中的异常处理通常如Demo24的方式,在promise链的最后调用.catch()方法。当异常发生时,浏览器会沿着Promise链从上到下查找.catch()或onrejected处理器。这很大程度上模仿了如Demo25中的同步代码的工作方式,Demo25的代码中使用了await/async。Promises解决了一个金字塔式调用的底层缺陷,它会捕获所有的错误,包括抛出的异常和编码错误。Promise的异常处理是一个异步操作必要的功能组成部分。

doSomething(function (result) {
  doSomethingElse(result, function (newResult) {
    doThirdThing(newResult, function (finalResult) {
      console.log(`Got the final result: ${finalResult}`);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

Demo23. 早期金字塔式的连续异步操作,且下一个异步操作使用上一个异步操作的结果

doSomething()
  .then((result) => doSomethingElse(result))
  .then((newResult) => doThirdThing(newResult))
  .then((finalResult) => console.log(`Got the final result: ${finalResult}`))
  .catch(failureCallback);

Demo24. Promise链中使用.catch()进行异常处理

async function foo() {
  try {
    const result = await doSomething();
    const newResult = await doSomethingElse(result);
    const finalResult = await doThirdThing(newResult);
    console.log(`Got the final result: ${finalResult}`);
  } catch (error) {
    failureCallback(error);
  }
}

Demo25. Promise链异常处理修改为使用async/await的同步代码的方式

3.3.2.5 Promise中的then中的组合写法

可以通过Promise.all(), Promise.allSettled() ,Promise.any(), Promise.race()等4个组合方法并发地运行异步操作。

Promise.all([func1(), func2(), func3()]).then(([result1, result2, result3]) => {
  // use result1, result2 and result3
});

Demo26. Promise.all()组合方法的使用

如果数组中的一个promise被拒绝,Promise.all()直接拒绝了返回的promise并终止其它操作。这会引起不可预测状态和行为。Promise.allSettled()是另外一个组合方法,它保证了所有的操作在resolve之前全部完成。这些方法中promise的运行是并发的,一系列的promise同时开始运行,其中的一个promise不会等待另一个promise。promise组合序列化执行可按照Demo27中的写法。在Demo28中,我们遍历了要异步执行的函数,将其组合成了promise链,代码等同于Demo29。

[func1, func2, func3]
  .reduce((p, f) => p.then(f), Promise.resolve())
  .then((result3) => {
    /* use result3 */
  });

Demo27. promise组合序列化写法一

Promise.resolve()
  .then(func1)
  .then(func2)
  .then(func3)
  .then((result3) => {
    /* use result3 */
  });

Demo28. promise组合序列化写法二

promise组合序列化执行也可以使用async/await来实现,如Demo29所示。

let result;
for (const f of [func1, func2, func3]) {
  result = await f(result);
}
/* use last result (i.e. result3) */

Demo29. promise组合序列化写法三

composeAsync()函数接受任意数量的函数作为参数,并返回一个函数。这个函数接收通过管道函数传递的一个初始参数。管道函数是按顺序执行的。

const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);

Demo30. composeAsync的使用案例

当你将promise进行序列化组合时,请先考虑其是否必要。promise并发地运行是更高效的方式,一个promise不会阻塞另一个promise。

3.3.2.6 Promise中的执行顺序[18]

Promise的回调函数队列和这些函数何时被调用是由promise的实现决定的,API的开发者和使用者均遵循下面的语义:

1.通过then()添加的回调在当前javaScript事件循环结束后才会执行;

2.通过then()依次添加的多个回调函数会依次执行;

Demo30的执行结果是确定的。即使是已经resolved的promise的then()的回调函数也不会被同步执行。如Demo31,then()的回调函数会被添加到microtask队列,这些micortask在当前事件循环结束后(创建microtask的函数退出),在控制被返回到事件循环之前执行。而setTimeout()的任务被添加到task队列,这些task在下一个事件循环开始时执行,setTimeout()后的.then()方法会在setTimeout()任务执行后添加到microtask队列中。

Promise.resolve().then(() => console.log(2));
console.log(1);
// Logs: 1, 2

Demo30. promise中.then()的回调函数执行顺序案例一

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

wait(0).then(() => console.log(4));
Promise.resolve()
  .then(() => console.log(2))
  .then(() => console.log(3));
console.log(1); // 1, 2, 3, 4

Demo31. promise中.then()的回调函数执行顺序案例二

const promise = new Promise((resolve, reject) => {
  console.log("Promise callback");
  resolve();
}).then((result) => {
  console.log("Promise callback (.then)");
});

setTimeout(() => {
  console.log("event-loop cycle: Promise (fulfilled)", promise);
}, 0);

console.log("Promise (pending)", promise);

/*Promise callback
Promise (pending) Promise {<pending>}
Promise callback (.then)
event-loop cycle: Promise (fulfilled) Promise {<fulfilled>}

Demo32. promise中.then()的回调函数执行顺序案例三

3.3.3 Fetch请求跨域时的浏览器策略

同jQuery.ajax()一样,fetch请求也会有跨域问题。如Demo33,由初始域名为localhost:8080向目标域名为127.0.0.1:8080发送fetch请求时,浏览器会报出如图12的“CORS error”错误,错误的详细信息如图13。可以通过在CORSFilter对应答进行拦截,设置应答头”Access-Control-Allow-Origin:*“,再次发送跨域请求,浏览器不再报出”CORS error“的错误。

//这是localhost:8080/day05_lzs请求响应的index.html页面,即初始域名为localhost:8080

async function logJSONData() {
    //向目标域名127.0.0.1:8080发送跨域请求
	const response = await fetch("http://127.0.0.1:8080/day05_lzs/?                   						fname=Bill&lname=Gates");
	const jsonData = await response.json();
	console.log(jsonData);
}

Demo33 初始域名为localhost:8080,向目标域名为127.0.0.1:8080发送fetch请求

1389306-20230610223417790-77382708.png

图12 跨域发送fetch请求时,浏览器报出“CORS error”

1389306-20230610223437663-128088467.png

图13. 跨域发送fetch请求时,浏览器控制台输出的详细错误信息

3.4 重定向[19]

3.4.1 HTTP重定向

URL重定向(URL redirection),也称为URL forwarding,它使一个页面、表单或网站拥有多个URL地址。HTTP重定向是HTTP的一种应答类型。HTTP重定向是在浏览器向服务端发送请求后,当服务端进行重定向应答时触发的。重定向应答的状态码以“3”开头,应答头“Location”的值是要重定向到的URL。当浏览器接收到重定向应答后,会立马加载应答头“Location”中的URL。如图14,在浏览器地址输入"http://localhost:8080/day05_lzs/redirectTest"并打开后,浏览器向服务端发送请求,服务端接收到请求后进行了重定向应答,状态码为302,应答头”Location“为”http://127.0.0.1:8080/day05_lzs/redirectTest2“。浏览器接收到重定向应答后,会立马加载应答头“Location”中的URL。浏览器的地址栏也变为重定向的URL。

//路径“redirectTest”映射到的servlet
public class RedirectTestServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.sendRedirect("http://127.0.0.1:8080/day05_lzs/redirectTest2");
    }
}

Demo34. 路径“redirectTest”映射到的servlet

1389306-20230610223511282-1303794558.png

图14. chrome浏览器地址栏发起请求的HTTP重定向

在xhr/fetch请求进行HTTP重定向时,浏览器的地址栏不会变为重定向后的URL。如Demo35、Demo36,当我们点击按钮触发sendAjax()函数后,会发起URL为“redirectTest”的XMLHttpRequest请求;服务端的应答重定向到了“ http://127.0.0.1:8080/day05_lzs/redirectTest2”;URL为“redirectTest2”的重定向请求发起后,服务端的应答又重定向到了“http://127.0.0.1:8080/day05_lzs/redirectTest3”。一个xhr/fetch请求的重定向请求可视为ajax/fetch请求的延续。一方面如果XMLHttpRequest设置为同步执行,则在XMLHttpRequest请求及该请求的所有重定向请求执行完成后,才会继续执行Demo35中的“window.location”代码;这点在图15的请求顺序中可以看出。另一方面xhr/fetch请求不会改变浏览器地址栏的URL,xhr/fetch请求的重定向请求也不会改变浏览器地址栏的URL。

<!--index.html-->
<head>
	<script>
        function sendAjax(){
            var xhttp = new XMLHttpRequest();
            //将请求设置为同步
			xhttp.open("POST", "redirectTest", false);
			xhttp.send()
			window.location = "http://localhost:8080/day05_lzs/redirectTestAfter"
        }
	</script>
</head>
<body>
    <button onclick="sendAjax()">发起ajax请求</button>
</body>	

demo35. xhr请求的HTTP重定向案例前端代码

//路径“redirectTest”映射到的servlet
public class RedirectTestServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.sendRedirect("http://127.0.0.1:8080/day05_lzs/redirectTest2");
    }
}

//路径“redirectTest2”映射到的servlet
public class RedirectTestServlet2 extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        response.sendRedirect("http://127.0.0.1:8080/day05_lzs/redirectTest3");
    }
}

Demo36. xhr请求的HTTP重定向案例后端代码

1389306-20230610223535648-452042798.png

图15. xhr请求的HTTP重定向案例在chrome浏览器开发这工具中的请求记录

重定向分为2种类型,永久重定向和临时重定向。永久重定向表示原始的URL不应继续被使用,应使用新的URL替代。搜索引擎机器人、RSS阅读器或其它爬虫会更新资源中的原始URL。有时请求的资源所在的规范URL不可用,但可以通过其它的URL访问到,此时可以使用临时重定向。搜索引擎机器人、RSS阅读器或其它爬虫不会记住临时的URL。临时重定向也可以用于在创建、更新和删除资源时,显示临时进度页面。

如表1,状态码301和308是永久重定向,302、303、307是临时重定向。永久重定向为何有2个状态码,临时重定向为何有3个状态码,它们的区别是什么?以临时重定向的3个状态码为例进行说明。在HTTP/1.0时临时重定向只有302这一个状态码,http规范规定重定向时不允许改变请求方法;且当请求方法不是GET/HEAD时,在重定向前浏览器需要询问客户是否继续,非GET/HEAD请求可能会改变请求发出时的状态。第一条重定向不允许改变请求方法的规定,很多浏览器的用户代理[20]并没有遵守,它们将302视为303对待。为了消除302状态码的歧义,在HTTP/1.1将302拆分成了303和307。第二条非GET/HEAD的重定向请求需要用户确认的规定,浏览器也没有实现[21]。

状态码 文本 http规范的要求[22] 用户代理的实现 典型使用场景
301 Moved Permanently HTTP/1.0重定向时不允许改变请求方法 GET方法不变。其它方法可能会也可能不会改变为GET方法。 网站重组
308 Permanent Redirect HTTP/1.1由301拆分出来 请求方法和请求体不变。 网站重组,非GET方法的链接或操作
302 Found HTTP/1.0重定向时不允许改变请求方法 GET方法不变。其它方法可能会也可能不会改变为GET方法。 web页因为不可预见的原因临时不可用
303 See Other HTTP/1.1由302拆分出来 GET方法不变。其它方法改变为GET方法。 在PUT和POST方法后进行重定向,刷新页面不会重复触发已执行的操作
307 Temporary Redirect HTTP/1.1由302拆分出来 请求方法和请求体不变。 web页因为不可预见的原因临时不可用。在使用非GET方法时使用,比302更好

表1. http重定向的状态码

如图16,浏览器的用户代理会以请求头User_Agent发送到服务端,用户代理是一个特殊的字符串,服务端可通过该字符串解析出客户使用的操作系统及版本、CPU类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。图中User-Agent字符串各部分的解释如下:

Mozilla/5.0: Netscape Communications 开发了 web 浏览器 Mozilla。凡是基于 WebKit 的浏览器都将自己伪装成了 Mozilla 5.0;

(Windows NT 10.0; Win64; x64): 操作系统windows 10;

AppleWebKit/537.36 (KHTML, like Gecko) : Apple 宣布发布首款他们自主开发的 web 浏览器:Safari。它的呈现引擎叫 WebKit;Apple公司担心用户不知道WebKit的兼容性,使用(KHTML, like Gecko)让开发者知道WebKit像Gecko一样,是兼容Mozilla浏览器的;

Chrome/95.0.4638.54: Google Chrome 浏览器及其版本号。Chrome以 WebKit 作为呈现引擎;

Safari/537.36: User-Agent中包括的信息既有 Apple WebKit 的版本,也有 Safari 的版本。

1389306-20230610223552237-941002048.png

图16. Chrome浏览器中请求头User-Agent

3.4.2 HTML重定向

HTTP重定向是最好的重定向方式,但有时你无法控制服务端。你可以通过HTML中a标签的href属性来实现重定向。HTML重定向是document类型的请求,请求方法式GET,如图17、18。

<a href="/day05_lzs/redirectTestAfter">重定向测试</a></br>
<a href="http://localhost:8080/day05_lzs/redirectTestAfter">重定向测试</a></br>

Demo37. HTML重定向

1389306-20230610223613820-229677102.png

图17. HTML重定向的请求方式

1389306-20230610223625223-607331611.png

图18. HTML重定向的请求方法

3.4.3 JavaScript重定向

JavaScript的重定向通过为“window.location”设置URL来实现的。 JavaScript重定向是document类型的请求,请求方法式GET,如图19、20。

//相对URL
window.location = "/day05_lzs/redirectTestAfter"
//绝对URL
window.location= "http://localhost:8080/day05_lzs/redirectTestAfter"
//window.location.href的方式
window.location.href = "http://localhost:8080/day05_lzs/redirectTestAfter"

Demo38. JavaScript重定向

1389306-20230610223637066-590150435.png

图19. JavaScript重定向的请求方式

1389306-20230610223646300-897288757.png

图20. JavaScript重定向的请求方式

[1] https://developer.mozilla.org/en-US/docs/Web/HTTP

[2]https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods

[3] https://developer.mozilla.org/en-US/docs/Web/HTTP/Status

[4] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers

[5] https://www.orcode.com/question/1049930_kae50a.html

[6]https://developer.mozilla.org/en-US/docs/Web/API/Request/body

[7] https://www.w3school.com.cn/js/js_ajax_http_send.asp

[8] https://www.cnblogs.com/oxspirt/p/13096737.html

[9] https://www.w3school.com.cn/js/js_ajax_intro.asp

[10] https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

[11] https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch

[12] https://blog.csdn.net/Ed7zgeE9X/article/details/124937789

[13] https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API

[14] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

[15] https://web.dev/introduction-to-fetch/#why_is_no-cors_supported_in_service_workers_but_not_the_window

[16] https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_response_header_name

[17] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#advanced_example

[18]https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises#composition

[19] https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections

[20] https://baike.baidu.com/item/用户代理/1471005?fr=aladdin

[21] https://www.cnblogs.com/OpenCoder/p/16265950.html

[22] https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK