AJAX 与 Axios 学习笔记
朋友们,大家好!本期内容是《你应该掌握的 JavaScript 高阶技能》的第四期,内容主要为
AJAX 基础和 Axios 基础、进阶与源码分析
,关于第一和第二期遗留内容typeScript 实战项目贪吃蛇和手写 Promise
会在后续更新!本期内容量较多,望大家能反复食用哦☀️
本期内容参考 (特别鸣谢❤️)
- JavaScript 高级程序设计(第四版)第24章
- 阮一峰老师的《ECMAScript 6 标准入门》
- 李强老师的《axios 入门与源码解析》
- Axios 中文文档
学习 axios 前置基础 ✍️
- JavaScript
- AJAX
- Promise
- axios 源码下载:
- 方式一:(借助
git
工具)打开GIt Bash
命令行窗口,输入命令$ git clone https://github.com/axios/axios
- 方式二:源码地址,点击
Code
,再点击Download ZIP
即可
1. json-server 的介绍和服务搭建
1.1 json-server 概述
-
json-server 是一款小巧的
Mock
工具,它可以不写一行代码在30秒内创建一套Restful
风格的 api,适合3人及以下的前端团队做迅速 mock 后台逻辑,也可以接口测试中使用。 -
1.1.1 mock 是什么?
- mock 中文含义为
模拟
。 - mock 测试:在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。
- 简单来说,就是通过某种技术手段模拟测试对象的行为,返回预先设计的结果。比如,需要测试微信支付,实际上不是真实支付,那么就可以使用 Mock 来模拟支付,返回支付的结果。
- mock 中文含义为
-
1.1.2 json-server 作用
- 可以快速搭建 http 服务
- 后续需要 axios 发送请求,需要服务端与 axios 结合,就可以使用 json-server 来模拟接口
- 其他选择:
mock.js
…
1.2 使用 json-server 前需要三个步骤
-
1.2.1 安装 JSON-Server
- 安装提前安装
Node.js
(安装步骤略) npm install -g json-server
- 安装提前安装
-
1.2.2 创建一个名为 db.json 文件
-
(默认数据如下)
{
//文章
"posts": [
{ "id": 1, "title": "json-server", "author": "typicode" }
],
//评论
"comments": [
{ "id": 1, "body": "some comment", "postId": 1 }
],
//信息
"profile": { "name": "typicode" }
}
-
1.2.4 接口测试
- 在浏览器输入
http://localhost:3000
可以进入到主页。
- 在浏览器输入
-
查询
- 在浏览器输入
http://localhost:3000/posts
可以查看到对应的返回值(其他同理)
- 在浏览器输入
1.3 json-server 基本使用
//复数路由
GET /posts
GET /posts/1
POST /posts
PUT /posts/1
PATCH /posts/1
DELETE /posts/1
//单数路由
GET /profile
POST /profile
PUT /profile
PATCH /profile
GET /posts?title=json-server&author=typicode
GET /posts?id=1&id=2
GET /comments?author.name=typicode
// '/' 分隔目录和子目录
// '?' 分隔实际的 URL 和参数
// '&' URL 中指定的参数间的分隔符
// '=' URL 中指定参数的值
-
1.3.3 其他
- 分页;排序;切片;操作;全局搜索;查看 db 等等操作可查看官方文档进行使用,这里不多赘述。
2. AJAX 基础
2.1 AJAX 概述
AJAX
全称 “Asynchronous JavaScript and XML”(异步的 JavaScript 和 XML)。- AJAX 技术可以在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。
- AJAX 关键技术:**XMLHttpRequest(XHR)**对象。XHR 为发送服务器请求和获取响应提供了合理的接口,这个接口可以异步从服务器获取额外数据,意味着用户点击不用刷新也可以获取数据,给用户带来便利。
2.2 XMLHttpRequest 对象
- 所有现代浏览器(IE7+、Firefox、Chrome、Safari 以及 Opera)都可以通过 XMLHttpRequest 构造函数原生支持 XHR 对象
let xhr = new XMLHttpRequest()
- 老版本的 Internet Explorer (IE5 和 IE6)使用 ActiveX 对象:
let xhr = new ActiveXObject("Microsoft.XMLHTTP");
让我看看谁还在用 IE !!! 😡
- 处理兼容性问题
let xhr;
if (window.XMLHttpRequest){
// IE7+, Firefox, Chrome, Opera, Safari 浏览器执行代码
xhr = new XMLHttpRequest();
}
else{
// IE6, IE5 浏览器执行代码
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
-
2.2.1 使用 XHR
-
请求发送到服务器,我们使用到 XMLHttpRequest 对象的
open()
和send()
方法 -
open 方法
- 使用 XHR 对象首先调用 open 方法,该方法接收三个参数:
- 请求类型(“get” “post” “delete” …)
- 请求 URL (“http://localhost:3000”)
- 请求是否异步的布尔值
xhr.open("get","xxx.com",false)
表示可以向xxx.com
发送一个同步的GET
请求,url 可以是绝对路径或是相对路径。- 注意:调用 open 方法不会实际发送请求,只是为发送请求做好准备。
- 使用 XHR 对象首先调用 open 方法,该方法接收三个参数:
-
send 方法
- 发送请求做好准备,就可以调用 send 方法
xhr.open("get","xxx.com",false);
xhr.send(null);
- send 方法接收一个参数,作为请求体发送的数据,如果不需要发送请求体,则必须传入 null,调用 send 方法以后,请求就会发送到服务器。
- 上述请求是同步的,意味着 JavaScript 代码会等待服务器响应之后再继续执行。收到响应后,XHR 对象的以下属性会被填充上数据
- responseText :作为响应体返回文本
- responseXML :如果响应的内容类型是
text/xml
或application/xml
,那就是包含响应数据的XML DOM
文档 - status :响应的 Http 状态
- statusText :响应的 Http 状态描述
- 收到响应后,第一步要检查 status 属性以确保响应成功返回。一般来说,Http 状态码为 2xx 表示成功。此时,responseText 或 responseXML 属性会有内容。如果 Http 状态码为 304 表示资源未修改过,是从浏览器缓存中直接拿取,当然也意味着响应有效。
服务器[常用]的状态码及其对应的含义如下:
200:服务器响应正常。
304:该资源在上次请求之后没有任何修改(这通常用于浏览器的缓存机制,使用GET请求时尤其需要注意)。
400:无法找到请求的资源。
401:访问资源的权限不够。
403:没有权限访问资源。
404:需要访问的资源不存在。
405:需要访问的资源被禁止。
407:访问的资源需要代理身份验证。
414:请求的URL太长。
500:服务器内部错误。
------------------
1xx: 信息响应类,表示接收到请求并且继续处理
2xx: 处理成功响应类,表示动作被成功接收、理解和接受
3xx: 重定向响应类,为了完成指定的动作,必须接受进一步处理
4xx: 客户端错误,客户请求包含语法错误或者是不能正确执行
5xx: 服务端错误,服务器不能正确执行一个正确的请求
为确保收到正确的响应,应该进行如下操作:
//...todo
//简化
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
}else{
alert('fail ' + xhr.status);
}
- 以上代码可能显示服务器返回的内容,也可能显示错误消息,取决于 HTTP 响应的状态码。
- 为确定下一步该执行什么操作,最好检查 status 而不是 statusText 属性,因为后者已经被证明在跨浏览器的情况下不可靠。
- 无论是什么响应内容类型,responseText 属性始终会保存响应体,而 responseXML 则对于非 XML 数据是 null。(可以参考上述 xhr 实例对象输出)
-
readState 属性
-
多数情况下最好使用异步请求,这样可以不阻塞 JavaScript 代码继续执行。XHR 对象有一个 readyState 属性,表示当前处在请求/响应过程的哪个阶段这个属性有如下可能的值。
- 0 : 未初始化(Uninitialized)。尚未调用 open 方法。
- 1 : 已打开(Open)。已调用 open 方法,尚未调用 send()方法。
- 2 : 已发送(Sent)。已调用 send 方法,尚未收到响应。
- 3 : 接收中(Receiving)。已经收到部分响应。
- 4 : 完成(Complete)。已经收到所有响应,可以使用了。
-
注意看,每次
readyState
从一个值变成另一个值,都会触发readystatechange
事件。可以借此机会检查readyState 的值。一般来说,我们唯一关心的 readystate 值是 4,表示数据已就绪。 -
为保证跨浏览器兼容,onreadystatechange 事件处理程序应该在调用 open 方法之前赋值。
let xhr = new XMLHttpRequest();
//...
xhr.onreadystateChange = function() {
if(xhr.readystate == 4){
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
}else{
alert('fail ' + xhr.status);
}
}
}
- 在收到响应之前,如果想取消异步请求,可以调用
abort()
方法。 xhr.abort()
调用这个方法之后,该对象会停止触发事件,并阻止访问这个对象上的任何与响应相关的属性。而且中断请求后,应该取消该对象的引用。
-
2.2.2 GET 请求
-
GET 请求是最为常见的请求方法,用于向服务器查询某些信息。
-
必要时,需要在 GET 请求的 URL 后面添加查询字符串参数。对 XHR 而言,查询字符串中的每个名和值都必须使用
encodeURIComponent()
编码,查询字符串必须正确编码后添加到 URL 后面,然后再传给 open()方法。 -
发送 GET 请求最常见的一个错误是查询字符串格式不对。查询字符串中的所有名/值对必须以(
&
)分隔
xhr.open("get", "/posts?title=json-server&author=typicode", true);
- 可以定义一个将查询字符串参数添加到现有的 URL 末尾的函数:
function addURLParam(url, name, value) {
url += (url.indexOf("?") == -1 ? "?" : "&");
url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
return url;
}
//- 首先,这个函数会检查 URL 中是否已经包含问号(以确定是否已经存在其他参数)。
//- 如果没有, 则加上一个'?';否则就加上一个'&'。然后,分别对参数名和参数值进行编码,并添加到 URL 末尾。 最后一步是返回更新后的 URL
-
addURLParam
方法接收 3 个参数:- 要添加查询字符串的 URL、查询参数和其值。
- 使用 该方法可以保证通过 XHR 发送请求的 URL 格式正确
-
2.2.3 POST 请求
-
POST 请求也是最为常见的请求方法,用于向服务器发送保存的数据。
-
每个 POST 请求都应该在请求体中携带提交的数据,而 GET 请求则不需要。POST 请求的请求体可以包含非常多的数据,而且数据可以是任意格式。
-
需要设置
Content-Type
头部为application/x-www-form-urlencoded
//todo...
xhr.open("post", "/posts", true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
//setRequestHeader() 设置响应头部
//这个方法接收两个参数:头部字段的名称和值。
//为保证请求头部被发送,必须在 open()之后、send()之前调用 setRequestHeader()
xhr.send('title=我是帅哥&author=我很帅');
xhr.onreadystatechange = function() {
//todo...
}
-
注意:POST 请求方式主要是用于向服务器发送和提交数据的,必然会携带参数,参数是作为 send() 的形参传递的,而参数的格式是查询字符串格式,所以如果数据是以对象形式存储的,就需要使用到
JSON.stringify()
进行序列化操作将对象转换为 JSON 字符串格式 -
JSON 字符串转换为对象(反序列化操作)
JSON.parse(JsonStr)
-
2.2.4 GET 和 POST 请求区别?
-
与 POST 相比,GET 更简单也更快,并且大部分情况下都能用。
然而,在以下情况中,请使用 POST 请求:
- 不愿使用缓存文件(更新服务器上的文件或数据库)
- 向服务器发送大量数据( POST 没有数据量限制)
- 发送包含未知字符的用户输入时,POST 比 GET 更稳定也更可靠
2.3 使用 jQuery 封装的 AJAX
// 入口函数
$(function () {
$.ajax({
type: 'GET', //'POST'
// type/method
// 指定请求方式(大小写均可)
url: 'http://localhost:3000', // URL地址
dataType:'json',
// 期望的数据类型,如果为json,会将后端返回的json串,自动解析;如果不为json,后端传什么,就接收什么数据,
data: {
// 此次请求所携带的参数(GET请求可省略),以对象形式存储
id: 1,
a: 10,
b: 20
},
success: function (res) { // 请求成功时执行的回调函数
console.log(res);
}
})
})
/*
其他参数
async:设定是否异步,默认为true,异步执行ajax请求,
error:function(e){}请求错误时执行的函数,
timeout:设定时间,单位ms,在异步请求的前提下,如果请求时间超过设定时间,则认为请求失败,
cache:设定是否缓存请求结果,默认为true,缓存请求结果
context:指定执行函数中,this的指向,默认是指向ajax对象
*/
$.get({
//...
});
$.post({
//...
})
tips:AJAX 进阶 和 Fetch API 于新一期讲解!
3. axios 的介绍与页面配置
3.1 axios 概述
-
axios 是基于
promise
可以用于浏览器(客户端)和node.js
的网络请求库 -
同一套代码可以运行在浏览器和 node.js 中,在服务端它使用原生 node.js 的
http
模块, 而在浏览端则使用XMLHttpRequests
。 -
3.1.1 aixos 与 ajax 区别?
- axios 是通过 Promise 实现对 ajax 技术的一种封装。类似于 jquery 对 ajax 的封装。
- ajax 是适用于 MVC (Model-View-Controller)的编程,不符合现在前端 MVVM (Model-View-ViewModel)的趋势,而 axios 符合现在前端 MVVM 的浪潮。
3.2 axios 特性
- 从浏览器创建
XMLHttpRequests
对象 - 从 node.js 创建 http 请求
- 支持 Promise 的 API
- 可以拦截请求和响应
- 转换请求和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
3.3 安装 axios
-
使用 npm:
npm install axios
-
使用 bower:
bower install aixos
-
使用 yarn:
yarn add axios
-
使用 CDN
-
jsDelivr CDN
-
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
-
unpkg CDN
-
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
-
3.4 axios 基本使用
3.4.1 axios 引入
<!-- todo... -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<!-- todo... -->
<script>
console.log(axios);
//ƒ (){return e.apply(t,arguments)}
</script>
3.4.2 axios api 说明图
3.4.3 使用 axios 发送多种请求
-
axios(config): 通用/最本质的发任意类型请求的方式 axios(url[, config]): 可以只指定 url 发 get 请求
<link crossorigin="anonymous" href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js"></script>
<!-- 页面结构 -->
<div class="container">
<h2 class="page-header">基本使用</h2>
<button class="btn btn-primary"> 发送GET请求 </button>
<button class="btn btn-warning"> 发送POST请求 </button>
<button class="btn btn-success"> 发送 PUT 请求 </button>
<button class="btn btn-danger"> 发送 DELETE 请求 </button>
</div>
//获取按钮
const btns = document.querySelectorAll('button');
//对于第一个按钮 获取文章
btns[0].onclick = function () {
//axios(config) 返回的是一个 promise 对象
axios({
// 请求类型
method: 'GET',
// URL
url: 'http://localhost:3000/posts/2'
}).then(response => {
console.log(response);
})
}
//对于第二个按钮 添加新的文章
btns[1].onclick = function () {
axios({
// 请求类型
method: 'POST',
// URL
url: 'http://localhost:3000/posts',
//设置请求体
data: {
title: '我是点击按钮新增的',
author: 'button'
}
}).then(response => {
console.log(response);
})
}
//对于第三个按钮 更新文章数据
btns[2].onclick = function () {
axios({
// 请求类型
method: 'PUT',
// URL
url: 'http://localhost:3000/posts/3',
//设置请求体
data: {
title: '更新啦',
author: 'btn[2]'
}
}).then(response => {
console.log(response);
})
}
//对于第四个按钮 删除数据
btns[3].onclick = function () {
axios({
// 请求类型
method: 'DELETE',
// URL
url: 'http://localhost:3000/posts/3',
}).then(response => {
console.log(response);
})
}
3.4.4 axios 其他使用
-
axios.get(url[, config]): 发 get 请求 axios.delete(url[, config]): 发 delete 请求 axios.post(url[, data, config]): 发 post 请求 axios.put(url[, data, config]): 发 put 请求 axios.request(config): 等同于 axios(config)
btns[0].onclick = function () {
//axios()
//发送 GET 请求
axios.request({
method: 'GET',
url: 'http://localhost:3000/comments',
}).then(response => {
console.log(response);
});
}
//发送 POST 请求
btns[1].onclick = function () {
axios.post('http://localhost:3000/comments',
{
body: '新增的comment',
postId: 2
}
).then(response => {
console.log(response);
})
}
//...
axios 成功结果输出
-
config:请求配置对象
-
data :响应体,是一个对象 JSON 解析
-
headers:响应头信息
-
request:原生的 AJAX 请求对象
- axios 发送 ajax 请求,需要使用底层的 XMLHttpRequest 对象,其保存了当前 axios 发送请求的 ajax 对象 (XMLHttpRequest 实例)
-
status:响应状态码
-
statusText:响应状态字符串
3.4.5 Request Config 请求配置
{
// `url` 是用于请求的服务器 URL
url: '/user',
// `method` 是创建请求时使用的方法
method: 'get', // 默认值
// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
// 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
baseURL: 'https://some-domain.com/api/',
// `transformRequest` 允许在向服务器发送前,修改请求数据
// 它只能用于 'PUT', 'POST' 和 'PATCH' 这几个请求方法
// 数组中最后一个函数必须返回一个字符串, 一个Buffer实例,ArrayBuffer,FormData,或 Stream
// 可以修改请求头。
transformRequest: [function (data, headers) {
// 对发送的 data 进行任意转换处理
return data;
}],
// `transformResponse` 在传递给 then/catch 前,允许修改响应数据
transformResponse: [function (data) {
// 对接收的 data 进行任意转换处理
return data;
}],
// 自定义请求头
headers: {'X-Requested-With': 'XMLHttpRequest'},
// `params` 是与请求一起发送的 URL 参数
// 必须是一个简单对象或 URLSearchParams 对象
params: {
ID: 12345
},
// `paramsSerializer`是可选方法,主要用于序列化`params`
// (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
paramsSerializer: function (params) {
return Qs.stringify(params, {arrayFormat: 'brackets'})
},
// `data` 是作为请求体被发送的数据
// 仅适用 'PUT', 'POST', 'DELETE 和 'PATCH' 请求方法
// 在没有设置 `transformRequest` 时,则必须是以下类型之一:
// - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
// - 浏览器专属: FormData, File, Blob
// - Node 专属: Stream, Buffer
data: {
firstName: 'Fred'
},
// 发送请求体数据的可选语法
// 请求方式 post
// 只有 value 会被发送,key 则不会
data: 'Country=Brasil&City=Belo Horizonte',
// `timeout` 指定请求超时的毫秒数。
// 如果请求时间超过 `timeout` 的值,则请求会被中断
timeout: 1000, // 默认值是 `0` (永不超时)
// `withCredentials` 表示跨域请求时是否需要使用凭证
withCredentials: false, // default
// `adapter` 允许自定义处理请求,这使测试更加容易。
// 返回一个 promise 并提供一个有效的响应 (参见 lib/adapters/README.md)。
adapter: function (config) {
/* ... */
},
// `auth` HTTP Basic Auth
auth: {
username: 'janedoe',
password: 's00pers3cret'
},
// `responseType` 表示浏览器将要响应的数据类型
// 选项包括: 'arraybuffer', 'document', 'json', 'text', 'stream'
// 浏览器专属:'blob'
responseType: 'json', // 默认值
// `responseEncoding` 表示用于解码响应的编码 (Node.js 专属)
// 注意:忽略 `responseType` 的值为 'stream',或者是客户端请求
// Note: Ignored for `responseType` of 'stream' or client-side requests
responseEncoding: 'utf8', // 默认值
// `xsrfCookieName` 是 xsrf token 的值,被用作 cookie 的名称
xsrfCookieName: 'XSRF-TOKEN', // 默认值
// `xsrfHeaderName` 是带有 xsrf token 值的http 请求头名称
xsrfHeaderName: 'X-XSRF-TOKEN', // 默认值
// `onUploadProgress` 允许为上传处理进度事件
// 浏览器专属
onUploadProgress: function (progressEvent) {
// 处理原生进度事件
},
// `onDownloadProgress` 允许为下载处理进度事件
// 浏览器专属
onDownloadProgress: function (progressEvent) {
// 处理原生进度事件
},
// `maxContentLength` 定义了node.js中允许的HTTP响应内容的最大字节数
maxContentLength: 2000,
// `maxBodyLength`(仅Node)定义允许的http请求内容的最大字节数
maxBodyLength: 2000,
// `validateStatus` 定义了对于给定的 HTTP状态码是 resolve 还是 reject promise。
// 如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),
// 则promise 将会 resolved,否则是 rejected。
validateStatus: function (status) {
return status >= 200 && status < 300; // 默认值
},
// `maxRedirects` 定义了在node.js中要遵循的最大重定向数。
// 如果设置为0,则不会进行重定向
maxRedirects: 5, // 默认值
// `socketPath` 定义了在node.js中使用的UNIX套接字。
// e.g. '/var/run/docker.sock' 发送请求到 docker 守护进程。
// 只能指定 `socketPath` 或 `proxy` 。
// 若都指定,这使用 `socketPath` 。
socketPath: null, // default
// `httpAgent` and `httpsAgent` define a custom agent to be used when performing http
// and https requests, respectively, in node.js. This allows options to be added like
// `keepAlive` that are not enabled by default.
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),
// `proxy` 定义了代理服务器的主机名,端口和协议。
// 您可以使用常规的`http_proxy` 和 `https_proxy` 环境变量。
// 使用 `false` 可以禁用代理功能,同时环境变量也会被忽略。
// `auth`表示应使用HTTP Basic auth连接到代理,并且提供凭据。
// 这将设置一个 `Proxy-Authorization` 请求头,它会覆盖 `headers` 中已存在的自定义 `Proxy-Authorization` 请求头。
// 如果代理服务器使用 HTTPS,则必须设置 protocol 为`https`
proxy: {
protocol: 'https',
host: '127.0.0.1',
port: 9000,
auth: {
username: 'mikeymike',
password: 'rapunz3l'
}
},
// see https://axios-http.com/zh/docs/cancellation
cancelToken: new CancelToken(function (cancel) {
}),
// `decompress` indicates whether or not the response body should be decompressed
// automatically. If set to `true` will also remove the 'content-encoding' header
// from the responses objects of all decompressed responses
// - Node only (XHR cannot turn off decompression)
decompress: true // 默认值
}
3.4.6 axios 请求的默认全局配置
axios.default.prop = baz
//获取按钮
const btns = document.querySelectorAll('button');
//默认配置
axios.defaults.method = 'GET';//设置默认的请求类型为 GET
axios.defaults.baseURL = 'http://localhost:3000';//设置基础 URL
axios.defaults.params = {id:100};
axios.defaults.timeout = 3000;
btns[0].onclick = function(){
axios({
url: '/posts'
}).then(response => {
console.log(response);
})
}
3.4.7 axios 创建实例对象发送请求
axios.create([config]): 创建一个新的 axios
1. 根据指定配置创建一个新的 axios, 也就就每个新 axios 都有自己的配置
2. 为什么要设计这个语法?
(1) 需求: 项目中有部分接口需要的配置与另一部分接口需要的配置不太一样, 如何处理?
(2) 解决: 创建 2 个新 axios, 每个都有自己特有的配置, 分别应用到不同要求的接口请求中
const AXIOS = axios.create({
baseURL: 'http://localhost:3000',
timeout: '2000',
//...
});
console.log(AXIOS);
AXIOS({
url: '/posts',
method: 'GET',
//...
}).then(response => {
console.log(response);
});
等同于下面代码⬇️
AXIOS.get('/posts').then(response => {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
});
3.4.8 axios 拦截器
- 拦截器分为:
- 请求拦截器
- 响应拦截器
/*
语法
axios.interceptors.request.use(): 添加请求拦截器
axios.interceptors.response.use(): 添加响应拦截器
*/
// Promise
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
console.log('请求拦截器(成功)');//1
return config;
}, function (error) {
// 对请求错误做些什么
console.log('请求拦截器(失败)');
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
console.log('响应拦截器(成功)');//2
return response;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
console.log('请求拦截器(失败)');
return Promise.reject(error);
});
//发送请求
axios({
method: 'GET',
url: 'http://localhost:3000/posts'
}).then(response => {
console.log('自定义回调处理成功的结果');//3
console.log(response);
}, error => {
console.log('自定义回调处理失败的结果');
console.log(error);
})
Q1
:在添加请求拦截器中将 return config
修改为 throw 'error'
或者 return Promise.reject('error')
,输出结果?
A1
:知识点:Promise
Q2
:如下图,如果有多个请求拦截器和响应拦截器,执行顺序?输出结果?< 图误
:请求拦截器 1 和 2中将 throw 'error'
均修改为 return config
>
A2
:
- 调用
axios()
并不是立即发送ajax 请求
, 而是需要经历一个较长的流程。 - 流程: 请求拦截器2 => 请求拦截器1 => 发ajax请求 => 响应拦截器1 => 响 应拦截器 2 => 请求的回调。
- 此流程是通过 promise 串连起来的, 请求拦截器传递的是 config, 响应拦截器传递的是 response
Q3
: 由图可知,请求拦截器 2 先于请求拦截器 1 执行的,为什么?
A3
: 源码后续解读。
修改 config
配置对象
// Promise
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
console.log('请求拦截器(成功)');
//修改 config 中的参数
config.params = { a: 10 };
return config;
},//...
);//...
处理 response
响应结果
axios.interceptors.response.use(function (response) {
console.log('响应拦截器(成功)');
//对响应结果进行处理
return response.data;
},//...
);//...
3.4.9 axios 取消请求
- 基本流程
- 配置 cancelToken 对象
- 缓存用于取消请求的 cancel 函数
- 在后面特定时机调用 cancel 函数取消请求
- 在错误回调中判断如果 error 是 cancel,做相应处理
/*
语法
axios.Cancel(): 用于创建取消请求的错误对象
axios.CancelToken(): 用于创建取消请求的 token 对象
axios.isCancel(): 是否是一个取消请求的错误
*/
//获取按钮
const btns = document.querySelectorAll('button');
//2.声明全局变量
let cancel = null;
//发送请求
btns[0].onclick = function () {
let cancelToken = new axios.CancelToken(function (c) {
//3.将 c 的值赋值给 cancel
cancel = c;
console.log(cancel);
});
axios({
method: 'GET',
url: 'http://localhost:3000/posts',
//1.添加配置对象的属性
cancelToken: cancelToken
}).then(response => {
console.log(response);
},error => {
console.log(error);
});
}
//取消请求
//点击按钮, 取消某个正在请求中的请求,在请求一个接口前, 取消前面一个未完成的请求
btns[1].onclick = function () {
cancel();
}
- 此时有个问题,看不到取消请求的效果,现在是发送请求,却还没等取消,结果已经返回。解决方法:将服务端进行延时响应
- 使用命令
json-server --watch db.json -d 2000
-d (delay)
//cancel 方法
function cancel(message) {
if(token.reason) {
//已申请取消
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
}
//后续源码会分析
- 点击发送请求按钮,2s 之内点击取消请求按钮
-
问题:如果用户连续点击,触发多次请求,造成服务器压力过大等问题,解决方法:防抖
-
思路:如果用户点击按钮,就检测上一次请求是否完成, 未完成就取消上一次请求,发送本次请求
//todo...
btns[0].onclick = function () {
//检测上一次的请求是否已经完成
if (cancel !== null) {
//取消上一次的请求
cancel();
}
//创建 cancelToken 的值
//todo...
let cancelToken = ...;
axios({
method: 'GET',
url: 'http://localhost:3000/posts',
//1. 添加配置对象的属性
cancelToken: cancelToken
}).then(response => {
console.log(response);
//将 cancel 的值初始化
cancel = null;
},//todo...
);
}
//取消请求
//todo...
3.4.10 axios 更多使用
axios.all(promises): 用于批量执行多个异步请求
axios.spread(): 用来指定接收所有成功数据的回调函数的方法
//... have a try!
4. axios 源码分析
4.1 源码目录
├── /dist/ # 项目输出目录
├── /lib/ # 项目源码目录
│ ├── /adapters/ # 定义请求的适配器 xhr、http
│ │ ├── http.js # 实现 http 适配器(包装 http 包)
│ │ └── xhr.js # 实现 xhr 适配器(包装 xhr 对象)
│ ├── /cancel/ # 定义取消功能
│ ├── /core/ # 一些核心功能
│ │ ├── Axios.js # axios 的核心主类
│ │ ├── dispatchRequest.js # 用来调用 http 请求适配器方法发送请求的函数
│ │ ├── InterceptorManager.js # 拦截器的管理器
│ │ └── settle.js # 根据 http 响应状态,改变 Promise 的状态
│ ├── /helpers/ # 一些辅助方法
│ ├── axios.js # 对外暴露接口
│ ├── defaults.js # axios 的默认配置
│ └── utils.js # 公用工具
├── package.json # 项目信息
├── index.d.ts # 配置 TypeScript 的声明文件
└── index.js # 入口文件
4.1.1 源码目录分析
大局意识,了解为主
/lib/adapters
定义请求适配器http.js
:nodejs
向远端服务发送http
请求xhr.js
: 前端发送AJAX
请求
/lib/cancel
定义取消功能cancel.js
: 定义Cancel
构造函数,用来创建取消时的错误对象CancelToken
: 定义取消请求的构造函数isCancel.js
: 检测参数是否为取消对象
/lib/core
定义核心功能Axios.js
: axios 构造函数buildFullPath.js
: 构建完整url
createError.js
: 创建指定信息的 Error 对象dispatchRequest.js
: 调用 http 或 xhr 适配器发送请求enhanceError
: 更新错误对象InterceptorManager.js
: 拦截器管理器的构造函数,实例化构造器对象mergeConfig.js
: 合并两个配置对象的数据,并返回一个新的对象settle.js
: 根据响应状态码改变 promise 状态transformData.js
: 数据格式转化
/lib/helper
bind.js
: 跟 bind 方法类似,返回一个新的函数对象,并将新函数 this 绑定到一个对象上buildURL.js
: 创建一个 URL 将参数防止 url 后,并返回格式化的内容combineURL.js
: 合并 URLcookie.js
: 暴露 cookie 处理函数deprecatedMethod.js
: 控制台提供不赞成的方法isAbsoluteURL.js
: 检测是否为绝对路径的 URLisURLSameOrigin.js
: 检测是否为同源的 URLnormalizeHeaderName.js
: 统一化头信息,统一大写spread.js
: 扩展参数的数组并调用函数。
/lib/axios.js
: axios 入口文件default.js
: axios 默认配置文件( 默认配置对象)util.js
: 工具函数文件index.js
: 整个项目入口文件
4.2 axios 的创建过程
4.2.1 axios 运行的整体流程
//...
//script 标签外部引入 axios
//axios
console.log(axios);
/*
ƒ wrap() {
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
return fn.apply(thisArg, args);
}
*/
- 首先从整个项目入口文件
index.js
文件开始
//将模块导入到当前文件
module.exports = require('./lib/axios');
/lib/axios.js
文件源码
'use strict';
// axios 入口文件
//引入工具
var utils = require('./utils');
//引入绑定函数 创建函数
var bind = require('./helpers/bind');// 创建函数的
//引入 Axios 主文件
var Axios = require('./core/Axios');
// 引入合并配置的函数
var mergeConfig = require('./core/mergeConfig');
// 导入默认配置
var defaults = require('./defaults');
/**
* Create an instance of Axios
* 创建一个 Axios 的实例对象
* @param {Object} defaultConfig The default config for the instance
* @return {Axios} A new instance of Axios
*/
function createInstance(defaultConfig) {
//创建一个实例对象 context 可以调用 get post put delete request
var context = new Axios(defaultConfig);// context 不能当函数使用
// 将 request 方法的 this 指向 context 并返回新函数 instance 可以用作函数使用, 且返回的是一个 promise 对象
var instance = bind(Axios.prototype.request, context);// instance 与 Axios.prototype.request 代码一致
// instance({method:'get'}); instance.get() .post()
// Copy axios.prototype to instance
// 将 Axios.prototype 和实例对象的方法都添加到 instance 函数身上
utils.extend(instance, Axios.prototype, context);// instance.get instance.post ...
// instance() instance.get()
// 将实例对象的方法和属性扩展到 instance 函数身上
utils.extend(instance, context);
return instance;
}
// axios.interceptors
// Create the default instance to be exported
// 通过配置创建 axios 函数
var axios = createInstance(defaults);
// Expose Axios class to allow class inheritance
// axios 添加 Axios 属性, 属性值为构造函数对象 axios.CancelToken = CancelToken new axios.Axios();
axios.Axios = Axios;
// Factory for creating new instances
// 工厂函数 用来返回创建实例对象的函数
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// Expose Cancel & CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
// Expose all/spread
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
module.exports = axios;
//手动全局暴露 axios
//window.axios = axios;
// Allow use of default import syntax in TypeScript
module.exports.default = axios;
-
4.2.2 Axios 和 axios 区别
- 从语法上来说:axios 不是 Axios 的实例,axios 是
createInstance
构造函数创建的实例对象。 - 从功能上来说:axios 是 Axios 的实例,因为在
createInstance
构造函数中Axios.prototype
和实例对象的方法
都添加到instance
函数身上。 - axios 是
Axios.prototype.request
函数bind()
返回函数。 - axios 作为对象有 Axios 原型对象上的所有方法,有 Axios 对象上所有属性。
- 从语法上来说:axios 不是 Axios 的实例,axios 是
重点关注 axios 如何创建:
// 通过配置创建 axios 函数
var axios = createInstance(defaults);
// 断点测试
- 调用
createInstance
方法,对下图方框代码逐一分析
-
var content = new Axios(defaultConfig)
- 创建一个实例对象
context
,其中参数defaultConfig
就是createInstance
方法传入参数defaults
- defaults 参数的值需要查看
default.js
- 创建一个实例对象
-
defaults 是我们使用 axios 的默认配置
axios.defaults.baseURL = 'http://localhost:3000';//默认配置
axios.defaults.method = 'GET';
axios.defaults.baseURL = 'http://localhost:3000';
axios.defaults.params = { id: 2 };
axios.defaults.timeout = 3000;
//...
- 我们可以看到
defaultConfig
身上有很多属性,其中对配置属性进行处理操作
嘿!不用着急地下面看源码,首先需要知道 defaults 有哪些属性!
defaults: [ adapter,header,transformResponse,transformRequest,timeout... ]
// axios.js
// 导入默认配置
var defaults = require('./defaults');
// ------------------------------------
// default.js 部分源码
var defaults = {
//适配器
adapter: getDefaultAdapter(),
//请求数据转换函数
transformRequest: [function transformRequest(data, headers) {
normalizeHeaderName(headers, 'Accept');
normalizeHeaderName(headers, 'Content-Type');
if (utils.isFormData(data) ||
utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data)
) {
return data;
}
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
if (utils.isObject(data)) {
setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
return JSON.stringify(data);
}
return data;
}],
//响应数据转换函数
transformResponse: [function transformResponse(data) {
/*eslint no-param-reassign:0*/
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) { /* Ignore */
}
}
return data;
}],
/**
* A timeout in milliseconds to abort a request. If set to 0 (default) a
* timeout is not created.
* 超时时间设置
*/
timeout: 0,
//防止攻击的检测字符串
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
//请求为成功的条件
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
}
};
- 调用
new Axios
“转入”Axios.js
文件
Axios.js
文件部分源码
function Axios(instanceConfig) {
//实例对象上的 defaults 属性为配置对象
this.defaults = instanceConfig;
//实例对象上有 interceptors 属性用来设置请求和响应拦截器
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
this.interceptors = {...}
:为 Axios 实例化对象添加拦截器属性- 正好对应我们在使用 axios 时添加拦截器:
//添加请求拦截器
axios.interceptors.request.use(function (config) {
//...
}
//添加响应拦截器
axios.interceptors.response.use(function (response) {
//...
}
- 可以看到 Axios.js 文件中在 Axios 原型对象上配置方法,意味着在 Axios 实例对象可以调用这些方法。
- 注意:9-17行和19-27行可以发现,在调用
get、post
等方法实际上调用的是request
方法 。
//源码
Axios.prototype.request = function request(config) {
//...
}
Axios.prototype.getUri = function getUri(config) {
//...
};
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
Axios.prototype[method] = function (url, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url,
data: data
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
Axios.prototype[method] = function (url, data, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url,
data: data
}));
};
});
- 使用示例:
//example
//get
axios.get('/user').then(
//...
);
//request
axios.request({
method: 'GET',
url: 'http://localhost:3000/posts',
//...
})
// post delete patch ...
var instance = bind(Axios.prototype.request, context)
//bind方法
//返回一个新的函数,并将新函数 this 绑定到对象身上
function bind(fn, thisArg) {
return function wrap() {
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
return fn.apply(thisArg, args);
};
};
- 上述代码将
request
方法的this
指向context
并返回新函数instance
可以用作函数使用, 且返回的是一个promise
对象
util.extend()
方法
function extend(a, b, thisArg) {
forEach(b, function assignValue(val, key) {
if (thisArg && typeof val === 'function') {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
}
});
return a;
}
util.extend(instance,Axios.prototype,context)
- 其作用:将
Axios.prototype
和实例对象的方法都复制到instance
函数身上,此时instance
不再是一个“单纯”的函数,身上有许多方法
- 其作用:将
util.extend(instance,context)
- 其作用:将实例对象的方法和属性扩展到 instance 函数身上
var axios = createInstace(defaults)
执行完成后, 查看axios
跟我们分析一样,没问题!
axios.xxx = ...
本质上是往 axios 身上添加函数方法
4.3 axios 对象创建过程模拟实现
根据 4.2 节内容仿写部分实现
- Axios 构造函数
function Axios(config) {
// 初始化
// 属性 -- 默认配置与拦截器
this.defaults = config;
this.interceptors = {
request: {},
response: {}
}
}
- Axios 原型上添加相应的方法
Axios.prototype.request = function (config) {
console.log('发送 AJAX 请求,请求类型为 ' +
config.method);
}
Axios.prototype.get = function (config) {
return this.request({
method: 'GET'
});
}
Axios.prototype.post = function (config) {
return this.request({
method: 'POST'
});
}
//问:为什么 post get 方法内部返回 request 方法?
//答:4.2节源码分析
- 声明函数
createInstance
function createInstance(config) {
//实例化对象
let context = new Axios(config);
// context.get() context.post()
// 但是不能当作函数使用 context() 会报错
// 创建请求函数
let instance = Axios.prototype.request.bind
(context);
// instance 是个函数并且可以使用这样形式 instance({}) 此时 instance 不能当作对象使用 instance.get()
//将 Axios.prototype 对象中的方法添加到 instance 函数对象上
//遍历 这里可以不使用 Object.keys() 其他方式 for in 等...
Object.keys(Axios.prototype).forEach(key => {
instance[key] = Axios.prototype[key].bind
(context);
})
//为 instance 函数对象添加属性 default 与 interceptors
Object.keys(context).forEach(key => {
instance[key] = context[key];
})
console.dir(instance);
return instance;
}
- instance 与 axios 的区别?
- 相同点:
- 都是一个能发任意请求的函数:
request(config)
- 都有发特定请求的各种方法:
get()/post()/put()/delete()
- 都有默认配置和拦截器的属性:
defaults/interceptors
- 都是一个能发任意请求的函数:
- 不同点:
- 默认配置很可能不一样
- instance 没有 axios 后面添加的一些方法:
create()/CancelToken()/all()
- 相同点:
4.4 axios 发送请求过程
- 深入理解
Axios.prototype.request ...
axios({
method: 'GET',
url: 'http://localhost:3000/posts'
}).then(response => {
console.log(response);
});
debugger 来理解执行流程
Axios.prototype.request = function request(config) {
//此时 config = { method: 'GET', url: 'http://localhost:3000/posts'} 是 Object
if (typeof config === 'string') {
//如果传入参数是字符串,第一个参数作为 url 处理
//应对这种形式 axios("url",{config})
config = arguments[1] || {};
config.url = arguments[0];
} else {
// config 没有传入参数则为空对象
config = config || {};
}
//将默认配置与用户调用时传入的配置进行合并
config = mergeConfig(this.defaults, config);
//注意用户调用时传入的配置优先级比默认配置更高
//config = { adapter: ... , headers: {}, timeout: 0,...}
// Set config.method
// 设定请求方法
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
//无参,默认是 get 请求
config.method = 'get';
}
// Hook up interceptors middleware
// 创建拦截器中间件 第一个参数用来发送请求, 第二个为 undefined 用来补位
// dispatchRequest 是一个函数,用来发送请求,去调用适配器 adapter [http,xhr]
var chain = [dispatchRequest, undefined];
// 创建一个成功的 promise 且成功的值为合并后的请求配置
var promise = Promise.resolve(config);// promise 是一个成功的Promise
//这里可以先不用看拦截器 因为我们没有设置请求拦截器,可以跳过,直接看47行
// 遍历实例对象的请求拦截器
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
//将请求拦截器压入数组的最前面
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
//将相应拦截器压入数组的最尾部
chain.push(interceptor.fulfilled, interceptor.rejected);
});
//如果链条长度不为 0
while (chain.length) {
//依次取出 chain 的回调函数, 并执行
promise = promise.then(chain.shift(), chain.shift());
//此时是一个成功的 promise,调用 then 方法会执行第一个回调函数,执行 dispatchRequest() 同时,dispatchRequest 的返回结果决定了 promise 的值,所以我们只需要关注 dispatchRequest
}
return promise;//返回成功的结果,此时 axios({...})执行完毕,调用then方法当然会执行成功的回调
};
//调用 request 方法,会调用 dispatchRequest,然后会调用 xhr 方法,然后一层一层返回
4.5 模拟实现 axios 发送请求
- 根据 4.4 节分析我们具体实现一下!<简化版>
- 发送请求整体流程:
request(config) ==> dispatchRequest(config) ==> xhrAdapter(config)
request(config)
:- 将请求拦截器、
dispatchRequest()
和响应拦截器通过 promise 链串连起来, 返回 promise
- 将请求拦截器、
dispatchRequest(config)
:- 转换请求数据 ==> 调用
xhrAdapter()
发请求 ==> 请求返回后转换响应数据,返回 promise
- 转换请求数据 ==> 调用
xhrAdapter(config)
: 创建 XHR 对象, 根据 config 进行相应设置, 发送特定请求, 并接收响应数据, 返回 promise- 搭建结构
// axios 发送请求
// 1.声明构造函数
function Axios(config) {
this.config = config;
//简写
}
Axios.prototype.request = function (config) {
// 发送请求
// todo...
}
// 2.dispatchRequest 函数
function dispatchRequest(config) {
// todo...
}
// 3.adapter 适配器 (xhr)
function xhrAdapter(config) {
// todo...
}
// 4.创建 axios 函数
let axios = Axios.prototype.request.bind(null);
//此时 axios 与 request 方法是一致的
axios({
method: 'GET',
url: 'http://localhost:3000/posts'
}).then(response => {
console.log(response);
}
Axios.prototype.request
Axios.prototype.request = function (config) {
// 发送请求
// 1.创建一个 promise 对象
let promise = Promise.resolve(config);
// 声明数组
let chains = [dispatchRequest,undefined];
// undefined 作用:占位 至于为什么?接着看!
// 调用 then 方法指定回调
let result = promise.then(chains[0],chains[1]);
// 返回 promise 结果
return result;
}
dispatchRequest
// 2.dispatchRequest 函数
function dispatchRequest(config) {
// 调用适配器发送请求
return xhrAdapter(config).then(response => {
// 响应结果处理进行转换处理
// todo...
// console.log(response);
return response;
}, error => {
throw error;
})
}
xhrAdapter
// 3.adapter 适配器 (xhr)
function xhrAdapter(config) {
return new Promise((resolve,reject) => {
// 发送 AJAX 请求
let xhr = new XMLHttpRequest();
xhr.open(config.method,config.url,true);
xhr.send(null);
xhr.onreadystatechange = function() {
if(xhr.readyState === 4){
if(xhr.status >= 200 && xhr.status < 300){
// 成功
resolve({
config: config,
data: xhr.response,
// 字符串 axios 内部做了解析 变成对象 --> parseHeader(),这里省略了该步骤
header: xhr.getAllResponseHeader(),
request: xhr,
status: xhr.status,
statusText: xhr.statusText
});
}else{
reject(new Error('fail ' + xhr.status));
}
}
}
});
}
4.6 axios 拦截器工作原理
-
使用拦截器(请求/响应)
axios.interceptors.request.use(成功回调,失败回调)
axios.interceptors.response.use(...)
-
3.4.8节拦截器输出结果分析
function Axios(instanceConfig) {
// 实例对象上有 interceptors 属性用来设置请求和响应拦截器
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
- 在
InterceptorManager.js
中use
方法实际上是在保存成功和失败的回调函数
// InterceptorManager 构造函数
function InterceptorManager() {
// 创建一个属性
this.handlers = [];
}
// fulfilled 成功回调函数
// rejected 失败回调函数
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1;
};
- 重点关注
Axios.js
,此时会执行下面代码
// 遍历实例对象的请求拦截器,
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// 将请求拦截器压入数组的最前面
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
// 将相应拦截器压入数组的最尾部
chain.push(interceptor.fulfilled, interceptor.rejected);
});
// this.interceptors.request 和 (...).response 是 InterceptorManager 实例对象
// forEach 方法是 挂载到 InterceptorManager 原型对象上的方法
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
};
//暂且不管 utils.forEach 做了什么事情,只需知道其作为遍历处理了 this.handlers
-
分析
this.interceptors.request
实例对象属性handlers
-
InterceptorManager.prototype.use
实现如下操作,handlers
数组末尾添加了请求拦截器的成功和失败的回调函数。
- 执行
forEach
前后,chain
发生改变
unshift
可将请求拦截器放入数组最前面。先放入请求拦截器1号的 fulfilled 和 rejected ;- 此时数组
chain = [0:f one(config),1: f one(error),2: f dispatchRequest(config),3: undefined]
- 后放入请求拦截器2号的 fulfilled 和 rejected ;
- 此时数组
chain = [0:f two(config),1: f two(error),2:f one(config),3: f one(error),4: f dispatchRequest(config),5: undefined]
this.interceptors.response
分析同理,不同的是将响应拦截器放入数组末尾。chain 数组最终为:
- 3.4.8 节输出结果的问题也就迎然而解!
- 后续执行
// todo...
while (chain.length) {
// 依次取出 chain 的回调函数, 并执行
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
// todo...
-
注意:
shift
是变异方法,会改变原数组,也就是从chain
数组头部依次取出回调函数,每次循环,都会取出 2 个回调函数分别对应promise.then(取出第一个回调,取出第二个回调)
,根据promise
的状态来执行具体哪一个回调函数 。 -
所以说
let chains = [dispatchRequest,undefined]
中undefined
的作用就是占位。
4.7 模拟实现 axios 拦截器功能
- 实现
Axios
构造函数
// 构造函数
function Axios(config) {
this.config = config;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
}
}
// 发送请求
Axios.prototype.request = function(config) {
// 返回一个 promise 对象
return new Promise((resolve,reject) => {
resolve({
status: 200,
statusText: 'OK',
});
})
}
- 实现
InterceptorManager
构造函数
// 拦截器管理器构造函数
function InterceptorManager() {
this.handlers = [];
}
InterceptorManager.prototype.use = function(fulfilled,rejected) {
this.handlers.push({
fulfilled,
rejected,
});
}
- 创建 axios 函数
// 创建实例
let context = new Axios({});
let axios = Axios.prototype.request.bind(context);
// 将 context 属性 config 和 interceptors 添加至 axios 函数对象上
Object.keys(context).forEach(key => {
axios[key] = context[key];
});
- 但是我们发现拦截器不会执行,需要对
Axios.prototype.request
进行完善
// 发送请求
Axios.prototype.request = function(config) {
// 创建一个 promise 对象 [成功]
let promise = Promise.resolve(config);
// 创建一个数值 chain
let chains = [dispatchRequest,undefined];
// 处理拦截器
// 请求拦截器 将请求拦截器的回调压入到 chains 的前面 request.handlers = []
//console.log(this.interceptors.request);
this.interceptors.request.handlers.forEach(item => {
chains.unshift(item.fulfilled,item.rejected);
})
console.log(chains);
}
this.interceptors.response.handlers.forEach(item => {
chains.push(item.fulfilled,item.rejected);
})
console.log(chains);
}
function dispathRequest(config) {
}
this.interceptors.request
输出结果
- 处理请求拦截器后,
chains
输出结果
- 处理响应拦截器后,
chains
输出结果
- 数组准备就绪,遍历
while(chains.length) {
promise = promise.then(chains.shift(),chains.shift());
}
return promise;
4.8 axios 取消请求工作原理
-
4.8.1如何取消未完成的请求?
- 当配置了
cancelToken
对象时, 保存cancel
函数- 创建一个用于将来中断请求的
cancelPromise
- 并定义了一个用于取消请求的
cancel
函数 - 将 cancel 函数传递出来
- 创建一个用于将来中断请求的
- 调用
cancel()
取消请求- 执行 cacel 函数, 传入错误信息 message
- 内部会让 cancelPromise 变为成功, 且成功的值为一个 Cancel 对象
- 在 cancelPromise 的成功回调中中断请求, 并让发请求的 proimse 失败, 失败的 reason 为 Cancel 对象
- 当配置了
// 2.
let cancel = null;
// todo...
// 1.(1)
let cancelToken = new axios.CancelToken(function (c) {
// 3.
cancel = c;
});
axios({
// todo...
// 1.(2)
cancelToken: cancelToken,
})//todo...
// 4.
cancel();
-
我们需要关心的是
new axios.CancelToken(function (c) {});
转入CancelToken.js
进行分析CancelToken
构造函数如下:
function CancelToken(executor) {
// 执行器函数必须是一个函数
// 否则会报错
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
//声明一个变量
var resolvePromise; // resolvePromise() 可以改变 this.promise 状态
//实例对象身上添加 promise 属性
/*new Promise((resolve,reject)=>{
})*/
this.promise = new Promise(function promiseExecutor(resolve) {
//将修改 promise 对象成功状态的函数暴露出去
//resolve 改变状态 --> 成功
resolvePromise = resolve;
});
// token 指向当前的实例对象
var token = this;
// 将修改 promise 状态的函数暴露出去, 通过 cancel = c 可以将函数赋值给 cancel
// executor 相当于上述的传入参数 function (c) { cancel = c }
// executor 的参数 function cancel(message){...} 传给 c
// 此时赋值操作完成
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
// c 调用 resolvePromise 也调用
resolvePromise(token.reason);
});
}
- 尽管如此,我们如何确定调用 c 就能取消请求呢?如果想要取消请求,应是调用
XMLHttpRequest
对象身上方法abort
方法。 - 来到
xhr.js
abort
方法
//如果配置了 cancelToken 则调用 then 方法设置成功的回调
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
//取消请求
request.abort();
reject(cancel);
// Clean up request
}
- 如果请求配置中设置
cancelToken
属性,则会调用config.cancelToken.promise.then
。 config.cancelToken.promise
正好对应CancelToken
构造函数中的this.promise
,所以可以调用 then 方法- 注意看,
this.promise
的状态是pending
,then 方法中定义回调函数会在状态改变的时候执行,执行回调函数意味着执行abort()
来取消请求。 - 现在所要关注的是何时改变 promise 的状态?
- 调用
cancel
方法,也就是调用c
方法,也就会调用resolvePromise
,相当于调用resolve
,状态变为fulfilled
,然后 then 方法中执行成功的回调函数来取消请求。
- 调用
4.9 模拟实现 axios 取消请求功能
- 搭建 axios 发送请求
// 构造函数 Axios
function Axios(config) {
this.config = config;
}
// 原型 request 方法
Axios.prototype.request = function(config) {
return dispatchRequest(config);
}
// dispatchRequest 函数
function dispatchRequest(config) {
return xhrAdapter(config);
}
// xhrAdapter 函数
function xhrAdapter(config) {
// 发送 Ajax 请求
return new Promise((resolve,reject) => {
// 实例化对象
let xhr = new XMLHttpRequest();
xhr.open(config.method,config.url,true);
xhr.send();
xhr.onreadystatechange = function() {
if(xhr.readyState === 4){
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
resolve({
status: xhr.status,
statusText: xhr.statusText,
})
}else {
reject(new Error(xhr.status));
}
}
}
})
}
let context = new Axios({});
// 创建 axios 函数
let axios = Axios.prototype.request.bind(context);
function CancelToken(executor) {
//声明变量
var resolvePromise;
//为实例对象添加属性
this.promise = new Promise((resolve) => {
//将 resolve 赋值给 resolvePromise
resolvePromise = resolve;
});
//调用 executor 函数 ==> c 赋值给 cancel
executor(function() {
//执行 resolvePromise 函数
resovlePromise();
})
}
- 关于对取消请求的处理
// xhrAdapter 函数
function xhrAdapter(config) {
return new Promise((resolve,reject) => {
// 发送 Ajax 请求
// todo...
if(config.cancelToken) {
//对 cancelToken 对象身上的 promise 对象指定成功的回调
config.cancelToken.promise.then(value => {
xhr.abort();
reject(new Error('取消请求'));
})
}
})
}
的时候执行,执行回调函数意味着执行 abort()
来取消请求。
- 现在所要关注的是何时改变 promise 的状态?
- 调用
cancel
方法,也就是调用c
方法,也就会调用resolvePromise
,相当于调用resolve
,状态变为fulfilled
,然后 then 方法中执行成功的回调函数来取消请求。
- 调用
4.9 模拟实现 axios 取消请求功能
- 搭建 axios 发送请求
// 构造函数 Axios
function Axios(config) {
this.config = config;
}
// 原型 request 方法
Axios.prototype.request = function(config) {
return dispatchRequest(config);
}
// dispatchRequest 函数
function dispatchRequest(config) {
return xhrAdapter(config);
}
// xhrAdapter 函数
function xhrAdapter(config) {
// 发送 Ajax 请求
return new Promise((resolve,reject) => {
// 实例化对象
let xhr = new XMLHttpRequest();
xhr.open(config.method,config.url,true);
xhr.send();
xhr.onreadystatechange = function() {
if(xhr.readyState === 4){
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
resolve({
status: xhr.status,
statusText: xhr.statusText,
})
}else {
reject(new Error(xhr.status));
}
}
}
})
}
let context = new Axios({});
// 创建 axios 函数
let axios = Axios.prototype.request.bind(context);
function CancelToken(executor) {
//声明变量
var resolvePromise;
//为实例对象添加属性
this.promise = new Promise((resolve) => {
//将 resolve 赋值给 resolvePromise
resolvePromise = resolve;
});
//调用 executor 函数 ==> c 赋值给 cancel
executor(function() {
//执行 resolvePromise 函数
resovlePromise();
})
}
- 关于对取消请求的处理
// xhrAdapter 函数
function xhrAdapter(config) {
return new Promise((resolve,reject) => {
// 发送 Ajax 请求
// todo...
if(config.cancelToken) {
//对 cancelToken 对象身上的 promise 对象指定成功的回调
config.cancelToken.promise.then(value => {
xhr.abort();
reject(new Error('取消请求'));
})
}
})
}
第四期学习内容就这么多啦,源码学习是有一定的难度,需要扎实的 js 基础,后续更新相关内容!如果您觉得内容不错的话,望您能关注🤞点赞👍收藏❤️一键三连!