你应该掌握的JavaScript高阶技能(四)

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 来模拟支付,返回支付的结果。
  • 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.3 启动服务
    • json-server --watch db.json
    • 在终端窗口下运行该命令,注意:需要与 db.json 所在目录保持一致
  • 运行成功

在这里插入图片描述

  • 1.2.4 接口测试
    • 在浏览器输入 http://localhost:3000 可以进入到主页。
  • 查询

    • 在浏览器输入http://localhost:3000/posts 可以查看到对应的返回值(其他同理)

在这里插入图片描述


1.3 json-server 基本使用

  • 1.3.1 路由
    • 基于db.json文件,默认路由如下。同时还可以添加其他路由。
//复数路由
GET    /posts
GET    /posts/1
POST   /posts
PUT    /posts/1
PATCH  /posts/1
DELETE /posts/1
//单数路由
GET    /profile
POST   /profile
PUT    /profile
PATCH  /profile
  • 1.3.2 过滤
    • 使用查询字符串参数可以查询对应的数据
    • url 参数中常见特殊符号转义码
    • 可以指定过滤字段,使用.号可以查询更深层次属性
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.1.1 AJAX 工作原理

在这里插入图片描述

  • 2.1.2 AJAX 应用
    • 运用 JavaScript 操作 DOM 来执行动态效果
    • 运用 XHR 或新的 Fetch API 与网页服务器进行异步数据交换;

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 方法不会实际发送请求,只是为发送请求做好准备
  • send 方法
    • 发送请求做好准备,就可以调用 send 方法
xhr.open("get","xxx.com",false);
xhr.send(null);
  • send 方法接收一个参数,作为请求体发送的数据,如果不需要发送请求体,则必须传入 null,调用 send 方法以后,请求就会发送到服务器。
  • 上述请求是同步的,意味着 JavaScript 代码会等待服务器响应之后再继续执行。收到响应后,XHR 对象的以下属性会被填充上数据
    • responseText :作为响应体返回文本
    • responseXML :如果响应的内容类型是 text/xmlapplication/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

  • 2.3.1 使用 $.ajax() 发起 GET 请求

  • 2.3.2 使用 $.ajax() 发起 POST 请求

// 入口函数
$(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 : 合并 URL
    • cookie.js : 暴露 cookie 处理函数
    • deprecatedMethod.js : 控制台提供不赞成的方法
    • isAbsoluteURL.js : 检测是否为绝对路径的 URL
    • isURLSameOrigin.js : 检测是否为同源的 URL
    • normalizeHeaderName.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 函数
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.jsuse 方法实际上是在保存成功和失败的回调函数
// 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 对象

  • 4.8.2 回顾取消请求步骤
  • 首先,添加配置对象的属性 cancelToken

  • 其次,声明全局变量 cancel

  • 再次,将 c 的值赋值给 cancel

  • 最后,执行取消请求 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);
  • 4.9.1 取消请求
  • CancelToken 构造函数

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);
  • 4.9.1 取消请求
  • CancelToken 构造函数

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 基础,后续更新相关内容!如果您觉得内容不错的话,望您能关注🤞点赞👍收藏❤️一键三连!

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值