在浏览器不重新加载任何窗口或窗体内容操控HTTP的需要使用JavaScript异步技术。
HTTP规定了WEB浏览器如何从WEB服务器获取文档和像WEB服务器提交表单内容,以及WEB服务器如何响应这些请求和提交。 WEB浏览器会处理大量的HTTP。通常,HTTP并不在JavaScript脚本的控制下,只有当用户单击链接、提交表单和输入URL时才发生。 但是,用JavaScript代码操纵HTTP是可行的。 用脚本设置window对象的location属性或者调用表单对象的submit()方法,都会初始化HTTP请求。在这两种情况下,浏览器会重新加载页面。————《JavaScript权威指南》
一、HTTP的一些知识
(一)请求报文
1、请求报文组成部分
- 起始行
- 首部
- 主体
2、请求报文起始行简单介绍
(1)method(方法):用来告诉服务端做什么事。
- GET: 从服务器获取一份文档
- HEAD:只从服务器获取文档的首部
- POST:向服务器发送需要处理的数据
- PUT:将请求的主体部分存储在服务器上
- TRACE:对可能经过代理服务器传送到服务器上去的报文进行追踪
- OPTIONS:决定从服务器上执行哪些方法
- DELETE:从服务器删除一份文档
(2) request-URL(请求URL) 所请求资源在服务器的路径。
通用格式为<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<qeury>#<frag>
,HTTP协议请求资源时,为http,默认80 。
(二)响应报文
1、响应报文组成部分
- 起始行
- 首部
- 主体
2、响应报文起始行简单介绍
(1)status(状态码):用来告诉客户端发生了什么事
- 100-199(已定义100-101):信息提示
- 200-299(已定义200-206):成功
- 300-399(已定义300-305):重定向
- 400-499(已定义400-415):客户端错误
- 500-599(一定义500-505):服务器错误
(2)reason-phrase(原因短语) 为状态码提供文本形式的解释,和状态码成对出现。
(三)报文流
一、AJAX
AJAX:使用脚本操纵HTTP和WEB服务器进行交换数据,而不会导致页面重新加载的通信协议。
参考:developer.mozilla.org/zh-CN/docs/…
使用AJAX做两件事:
- 在不重新加载页面的情况下发送请求给服务器。
- 接受并使用从服务器发来的数据。
(一)使用AJAX与服务器通信过程
步骤一:首先要有一个完成这些事的对象
// Old compatibility code, no longer needed.
if (window.XMLHttpRequest) { // Mozilla, Safari, IE7+ ...
httpRequest = new XMLHttpRequest();
} else if (window.ActiveXObject) { // IE 6 and older
httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
}
步骤二:遵循先订阅后发布的原则,定义响应状态改变后回调哪个函数处理响应
httpRequest.onreadystatechange = function(){
// Process the server response here.
};
步骤三:初始化并发送请求
//初始化请求
httpRequest.open('GET', 'http://www.example.org/some.file', true); //true表示异步
//发送请求
httpRequest.send();
1、XMLHttpRequest.open(method, url, async)函数
我们知道请求报文的起始行包含method和request-URL参数,调用open()函数时需要指定这些内容,对应第一个参数和第二个参数,第三个参数表示是同步还是异步请求,一般设置为异步,默认也是异步。
2、XMLHttpRequest.send(body)函数
body是一个可选的参数,其作为请求主体;如果请求方法是 GET 或者 HEAD,则应将请求主体设置为 null,没有赋值也默认是null。如果请求方法是POST,则需要指定请求主体。
(二)步骤二处理响应的函数怎么写
1、如何知道通信过程到哪一步:XMLHttpRequest.readyState
也可以表述为:0 (请求还未初始化) ,1 (已建立服务器链接),2 (请求已接受),3 (正在处理请求) ,4 (请求已完成并且响应已准备好)。
XMLHttpRequest.readyState取值为XMLHttpRequest.DONE或者4就是通信完成服务器响应接收到的时候
2、如何得知通信结果——响应的状态码:XMLHttpRequest.status
取值是整数,是标准的HTTP的状态码,2xx范围的表示成功。在请求完成前,status值为0。值得注意的是,如果 XMLHttpRequest 出错,浏览器返回的 status 也为0。
3、通信成功如何获得响应报文主体:XMLHttpRequest.responseText和XMLHttpRequest.responseXML。
XMLHttpRequest.responseText
– 服务器以文本字符的形式返回。XMLHttpRequest.responseXML
– 以 XMLDocument 对象方式返回,之后就可以使用JavaScript来处理。
综上,处理响应的函数第一步应该判断通信是否结束能获得响应,然后判断服务端处理结果(状态码)是怎样,如果是处理成功的就去读取想要的报文,框架如下
if (httpRequest.readyState === XMLHttpRequest.DONE) {
// Everything is good, the response was received.
if (httpRequest.status === 200) {
// Perfect!
} else {
// There was a problem with the request.
// For example, the response may have a 404 (Not Found)
// or 500 (Internal Server Error) response code.
}} else {
// Not ready yet.
}
在通信错误的事件中(例如服务器宕机),在访问响应状态 onreadystatechange 方法中会抛出一个例外。为了缓和这种情况,则可以使用 try...catch
把上面的 if...else
语句包裹起来。
(三)发送post请求时的请求报文的首部和主体
实体首部提供有关实体及其内容的大量信息,可以告诉报文接收者(服务器)它在对什么进行处理。实体首部中有一部分是内容首部,其中的Content-Type首部,表示请求主体的对象类型,即客户端告诉服务器实际发送的数据类型。HTTP POST 方法 发送数据给服务器时,其请求主体的类型由 Content-Type 首部指定。
1、请求首部:XMLHTTPRequest(XHR)设置Content-Type的方法
XMLHttpRequest.setRequestHeader(header,value) :设置 HTTP 请求头的值。必须在 open() 之后、send() 之前调用 setRequestHeader() 方法。设置Content-Type则header设置为"Content-Type"。
2、请求主体:XMLHTTPRequest的请求主体作为send()方法的参数
3、XHR发送不同主体时首部的设置
- 请求主题是简单的文本:Content-Type设置为'text/plain;charset=utf-8',可以不设置,请求主体传入会自动设置。
- 请求主题是表单数据(a=1&b=2&c=3的形式的表单数据):Content-Type应设置为'application/x-www-form-urlencoded',可以不设置,请求主体传入会自动设置。
- 请求主题是复杂的表单数据(包含上传文件表单数据,FormData数据):Content-Type设置为'multipart/form-data',FromData请求主体传入会自动设置developer.mozilla.org/zh-CN/docs/…
- 请求主题是JSON数据(JSON.stringfy()序列化Jason对象):Content-Type设置为'application/json'。
4、Content-Type的语法
Content-Type:<media-type>;<charset>;<boundary>
- media-type:资源或数据的媒体类型(MIME type)
- charset:字符编码标准 ,us-ascii:ASCII字符编码;ios-8859-x:x可取值为1,2,5,6,7,8,15,是一套欧洲字符编码。常用的是ios-8859-1,是对ASCII的8位扩展,以支持西欧的多种语言;utf-8:UTF-8字符编码。(默认编码)
- boundary:多部份实体中是必须的,用来封装消息的多个部分的边界。
二、axios
可以使用json-server搭建一个服务器来学习,只需三步,
axios发送请求后返回的是promise对象,发送请求的方法列举如下:
(一)aixos发送请求
1、Axios API发送请求
-
axios(config)
//config中通过method属性指定不同请求方法 axios({ method:"get", url: "http://localhost:3000/posts/1" }).then((response)=>{ console.log(response.data) })
-
axios(url[,config])
axios('/user/12345'); 默认方法就是get,不用指定config了
2、请求方法别名API发送请求
-
axios.request(config)——发送各类请求
-
axios.get(url[, config])——GET请求
-
axios.delete(url[, config])——DELETE请求
-
axios.head(url[, config])——HEAD请求
-
axios.options(url[, config])——OPTION请求
-
axios.post(url[, data[, config]])——POST请求
-
axios.put(url[, data[, config]])——PUT请求
-
axios.patch(url[, data[, config]])
//request发送请求,config中通过method属性指定不同请求方法 axios.request({ method:"get", url: "http://localhost:3000/comments/1" }).then((response)=>{ console.log(response) }) //get发送请求 axios.get("http://localhost:3000/comments/1").then((response)=>{ console.log(response) }) //post发送请求axios.post("http://localhost:3000/comments",{ "body": "你好啊", "postId": 1 }).then((response)=>{ console.log(response) })
3、创建axios实例,实例发送请求
- axios.create([config])
axios实例创建出来后,这个实例发请求的用法和(一)中axios的使用是一样的。
创建实例可以复用一些基本的config配置,还可以创建不同的实例连接到不同的服务器。
const instance=axios.create({
baseURL:"http://localhost:3000",
timeout:3000
});
instance({
method:"GET",
url: "/posts/1"
}).then((response)=>{
console.log(response.data)
})
4、关于参数传递
可以根据业务需要,比如路由配置,后台接口要求,选取不同的传参方式。
(1)query方式传参(这个记忆的时候可以联想数据库查询时拼接查询参数)
axios.get('/user?ID=12345')
(2)params方式传参
方式一:
axios.get('/user/12345');
方式二:
axios.get('/user', {
params: {
ID: 12345
}
})
(二)aixos特色功能
1、对请求和响应进行拦截
可以在then/catch方法处理响应数据前对请求或者响应进行拦截。比如,根据后台接口返回的响应,做一个判断是否已经登录的处理,登录与否走不同的逻辑,就可以对响应进行拦截。
响应拦截:我们知道"2xx”都是服务端处理成功的状态码,其他则是出错的状态码,拦截器可以对这些情况都做一些拦截处理。
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
return response;
}, function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(error);
});
一个响应拦截的例子:
这个改造过程中经常会用到以下两个函数:Promise.resolve()
和 Promise.reject()
,是手动创建一个已经 resolve 或者 reject 的 Promise 快捷方法。
axios.interceptors.response.use(function (response) {
let res= response.data;
if(res.status==0){
//假设后台正常返回状态码是0
return res.data; //假设res.data是请求成功时后台返回的数据
}else if(res.status==10){
//假设后台接口返回的用户未登录的错误码是10
window.location.href="/#/login";
return Promise.reject(res);
}else{
console.log(res.msg)
return Promise.reject(res);
}
}, function (error) {
console.log(error); //处理2xx以外的错误码
return Promise.reject(error);
});
2、取消请求:有需要时参看官方文档
三、Promise
B站有一个讲解promise很不错的视频:
MDN参考文档
developer.mozilla.org/zh-CN/docs/…
developer.mozilla.org/zh-CN/docs/…
(一)Promise是什么?为什么要用它?
Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。 是异步编程的一种解决方案。一个Promise对象有两个很重要的属性:[[PromiseState]]状态和[[PromiseResult]]值。
使用Promise,可以更优雅的写出异步任务成功和失败的处理过程。特别是对于一个异步任务执行完成后再继续下一个异步任务的场景,使用promise会让代码可读性更高。看下面的代码,是一个“回调地狱”:
doFirstSomething(function(firstResult) {
//第一个异步任务执行成功,执行第二个异步任务
doSecondThing(firstResult, function(secondResult) {
//第二个异步任务执行成功,执行第三个异步任务
doThirdThing(secondResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
让doFirstSomething返回一个promise对象,使用promise就可以很优雅的写成这样,这就是promise的链式调用。
doSomethingFirst().then(firstResult=>{
return doSecondThing(firstResult)
}).then(secondResult=>{
return doThirdThing(secondeThing)
}).catch(failureCallback);
(二)Promise对象的状态
Promise对象怎么去代表异步任务的成功和失败?写代码,在创建Promise时就让他根据异步任务调用结果指定成功/失败的条件。
- 待定(pending): 初始状态,既没有被兑现,也没有被拒绝。
- 已兑现(fulfilled): 意味着操作成功完成。 ——异步任务成功
- 已拒绝(rejected): 意味着操作失败。——异步任务失败
1、改变Promise状态的方法
let p=new Promise((resolve,reject)=>{
//1、resolve,pending=>fullfiled(resolved)
//resolve("执行成功")
//2、reject,reject("执行失败")
//pending=>reject
//3、抛出错误 ,pending=>reject
throw '出问题了'
})
2、一个创建Promise的例子,例子中根据异步任务的结果n的取值处理为成功或失败。
let p=new Promise((resolve,reject)=>{
setTimeout(()=>{
let n=rand(1,100);
console.log(n);
if(n<=30){
resolve(n); //将promise对象的状态置为成功,函数的参数值回传递给then中的成功回调方法
}else{
reject(n); //将promise对象的状态置为失败,函数的参数值回传递给then中的失败回调方法
}
},3000)
});
(三)成功回调&失败回调&成功或失败都进行的回调
1、Promise.prototype.then():返回一个Promise**。**这个函数可以有两个参数,第一个参数指定异步任务成功时的回调,第二个参数执行异步任务失败时的回调。语法:
p.then(onFulfilled[, onRejected]);
p.then(value => {
// fulfillment
}, reason => {
// rejection
})
2、Promise.prototype.catch():返回一个Promise。这个函数指定异步任务失败时的回调,相当于then()方法只指定失败回调,不指定成功回调的场景。语法
p.catch(onRejected);
p.catch(function(reason) {
// 拒绝
});
为什么有了then()方法,还有有一个catch()方法呢,强大之处就是“链式调用”的时候,可以链式穿透,一个地方捕获进行n个promise的失败回调,比使用then一个个定义失败回调少写了不少代码。
let p=new Promise((resolve,reject)=>{
//reject("NO");
resolve("YES")
})
p.then(value=>{
throw("出错了")
}).then(value=>{
console.log(222);
}).then(value=>{
console.log(333);
}).catch(error=>{
//catch能捕获以上任一个promise抛出的异常
console.log(error);
})
3、Promise.prototype.finally(),返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。语法:
p.finally(onFinally);
p.finally(function() {
// 返回状态为(resolved 或 rejected)
});
综上,假设doSomething()返回一个Promise对象,一个比较完善的Promise使用框架如下:
doSomething().then(function(response) {
//success,process response
}) .catch(function(error) {
//fail,process error}).finally(function() {
//do something whether success or fail
});
(四)怎么理解链式调用
链式调用:一个异步任务执行完成后在继续下一个异步任务
第n(n>=2)个异步任务的执行与否有什么决定?由其前一个then()返回的Promise的状态决定,如果时成功的就执行下一个then()的成功回调,如果是失败就执行下一个then()的失败回调或者是catch()的回调,如果是未确定则跳出链式调用。
回调函数有以下几种执行结果:
let p=new Promise((resolve,reject)=>{
resolve("OK");
})
let result=p.then(value=>{
//1、抛出错误:状态是rejected
//throw "出了问题"
//2、返回的是非promise的任意值:状态时resolved ,值是"hello world"。
//return "hello world"
//3、返回的是promise,状态由这个promise的状态决定
return new Promise((resolve,reject)=>{
//resolve("oh yes");
reject("oh no");
})
},error=>{
alert(error)
})
then方法返回的promise的状态由then指定的回调函数执返回结果决定。
(1)如果抛出异常,新的promise变为rejected,reason为抛出的异常 ;
(2)如果返回的是非promise的任意值,新promise变为resolved,value为返回的值 ;
(3)如果返回的是另一个新的promise,此promise的结果就会变为新promise的结果。
(五)中断Promise
即终端链式调用,当且仅当then()方法中返回一个pending状态的promise对象时,才能终端链式调用。
let p=new Promise((resolve,reject)=>{
resolve("YES")
})
p.then(value=>{
console.log(111);
return new Promise(()=>{}) //中断Promise
}).then(value=>{
console.log(222); //不会输出
}).then(value=>{
console.log(333); //不会输出
}).catch(error=>{
console.log(error);
})
(六)将一些旧式回调API改造成Promise
1、setTimeout函数封装
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
//从右往左赋值,wait是一个函数
wait(10000)
.then(() => {
saySomething("10 seconds")
}).catch(
failureCallback
);
2、Ajax封装
function sendAJAX(url){
const p=new Promise((resolve,reject)=>{
let xmlhttp=new XMLHttpRequest();
xmlhttp.open("GET",url);
xmlhttp.send();
xmlhttp.onreadystatechange=function(){
if(xmlhttp.readyState==4&&xmlhttp.status==200){
resolve(xmlhttp.responseText); //将promise对象的状态置为成功,函数的参数值回传递给then中的成功回调方法
}
else{
reject(xmlhttp.status+","+xmlhttp.readyState); //将promise对象的状态置为失败,函数的参数值回传递给then中的失败回调方法
}
}
});
return p;
}
sendAJAX("https://api.apiopen.top/getJoke").then((value)=>{
console.log(value);
},(err)=>{
console.log(err)
})
四、async&await
async和await是基于Promise的语法糖,让我们可以用一种更简洁的方式写出基于Promise的异步行为,而无需刻意地链式调用Promise。
(一)async——返回的是Promise
async关键字声明的函数,返回值的是一个Promise对象。Promise的状态由这个函数的返回值决定:
1、返回值是非Promise对象,则为fullfiled,结果值就是返回的值
2、返回值是Promise对象,由这个promise的执行结果决定
async function test(){
// return "abc"; //等价于return Promise.resolve("abc")
return new Promise((resolve,reject)=>{
//resolve("OK") //成功
//reject("ERR") //失败
throw("hhhh") //失败
})
}
(二)await——返回等待的Promise的[[PromiseResult]]值
await 操作符用于等待一个Promise 对象。它只能在异步函数 async function 中使用。await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。语法:
[返回值] = await 表达式;
await 右侧的表达式一般为Promise对象,但也可以是其他类型的值
1、如果表达式是Promise对象,若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行 async function。若 Promise 处理异常(rejected/throw error),await 表达式会把 Promise 的异常原因抛出,需要通过try...catch捕获处理。
2、如果表达式是其他值,直接将此值作为await的返回值 。
async function test(){
let p= new Promise((resolve,reject)=>{
//resolve("yeah ok")
reject("oh no")
});
//1、await右侧是promise表达式
//let res=await p;
//2、await右侧是其它类型的数据
//let res="abc"
try{
let res=await p;
}catch(e){
console.log(e); /* 3、捕获promise异常 */
}
}
(三)async和await怎么简洁Promise链式调用
MDN参考文档:developer.mozilla.org/zh-CN/docs/…
"await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成",基于此,链式代码中,除最后一个then之外,链中其他等待Promise执行完成的操作都放在await函数中,一个Promise对应一个await。这就实现了链式调用的效果。
1、Promise链式调用形式
fetch('coffee.jpg')
.then(response => response.blob())
.then(myBlob => {
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
})
.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});
2、async和await形式
async function myFetch() {
let response = await fetch('coffee.jpg');
let myBlob = await response.blob();
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
}
myFetch()
.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});
由于await本身也是返回一个Promise对象,所以上面的代码还可以结合Promise做进一步的优化。
async function myFetch() {
let response = await fetch('coffee.jpg');
let myBlob = await response.blob();
return myBlob;
}
myFetch().then(myBlob=>{ let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);}).catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message);
});
五、等待多个异步任务完成再继续
1、Promise.all():这个方法的参数是promise的iterable类型(注:Array,Map,Set都属于ES6的iterable类型),一般来说传一个数组,数组里面的元素是Promise对象。返回值是一个Promise对象;
(1)参数中n个Promise对象状态都是fulfilled或者这个参数是空的(比如,[]),那么返回的那个Promise对象的状态就是fulfilled,值是数组。
(2)参数中存在一个Promise对象状态都是rejected,那么返回的那个Promise对象就是rejected;
(3)其他情况就是pending状态。
MDN参考文档:developer.mozilla.org/zh-CN/docs/…
下面是来自MDN很典型的例子:
const promise1 = Promise.resolve(3);
const promise2 = 42; //非Promise类型就是fullfilled的Promise对象
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
// expected output: Array [3, 42, "foo"]