前言
这是
学习源码整体架构系列
第六篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。
学习源码整体架构系列
文章如下:
1.学习 jQuery 源码整体架构,打造属于自己的 js 类库
2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库
3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库
4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK
5.学习 vuex 源码整体架构,打造属于自己的状态管理库
感兴趣的读者可以点击阅读。下一篇可能是vue-router
源码。
本文比较长,手机上阅读,可以直接文中的几张图即可。建议收藏后在电脑上阅读,按照文中调试方式自己调试或许更容易吸收消化。
导读
文章详细介绍了 axios 调试方法。详细介绍了 axios 构造函数,拦截器,取消等功能的实现。最后还对比了其他请求库。
本文学习的版本是v0.19.0
。克隆的官方仓库的master
分支。
截至目前(2019年12月10日),最新一次commit
是2019-12-09 15:52 ZhaoXC
dc4bc49673943e352
,fix: fix ignore set withCredentials false (#2582)
。
本文仓库在这里若川的 axios-analysis github 仓库。求个star
呀。
如果你是求职者,项目写了运用了axios
,面试官可能会问你:
- 为什么
axios
既可以当函数调用,也可以当对象使用,比如axios.get
。- 简述
axios
调用流程。- 有用过拦截器吗?
- 有使用
axios
的取消功能吗?是怎么实现的- 为什么支持浏览器中发送请求也支持
node
发送请求
诸如这类问题。
chrome 和 vscode 调试 axios 源码方法
前不久,笔者在知乎回答了一个问题一年内的前端看不懂前端框架源码怎么办?
推荐了一些资料,阅读量还不错,大家有兴趣可以看看。主要有四点:
- 借助调试
- 搜索查阅相关高赞文章
- 把不懂的地方记录下来,查阅相关文档
- 总结
看源码,调试很重要,所以笔者详细写下 axios
源码调试方法,帮助一些可能不知道如何调试的读者。
chrome 调试浏览器环境 的 axios
调试方法
axios
打包后有sourcemap
文件。
# 可以克隆笔者的这个仓库代码
git clone https://github.com/lxchuan12/axios-analysis.git
cd axios-analaysis/axios
npm install
npm start
# open [http://localhost:3000](http://localhost:3000)
# chrome F12 source 控制面板 webpack// . lib 目录下,根据情况自行断点调试
复制代码
本文就是通过上述的例子axios/sandbox/client.html
来调试的。
顺便简单提下调试example
的例子,虽然文章最开始时写了这部分,后来又删了,最后想想还是写下。
找到文件axios/examples/server.js
,修改代码如下:
server = http.createServer(function (req, res) {
var url = req.url;
// 调试 examples
console.log(url);
// Process axios itself
if (/axios\.min\.js$/.test(url)) {
// 原来的代码 是 axios.min.js
// pipeFileToResponse(res, '../dist/axios.min.js', 'text/javascript');
pipeFileToResponse(res, '../dist/axios.js', 'text/javascript');
return;
}
// 原来的代码 是 axios.min.map
// if (/axios\.min.map$/.test(url)) {
if (/axios\.map$/.test(url)) {
// 原来的代码 是 axios.min.map
// pipeFileToResponse(res, '../dist/axios.min.map', 'text/javascript');
pipeFileToResponse(res, '../dist/axios.map', 'text/javascript');
return;
}
}
复制代码
# 上述安装好依赖后
# npm run examples 不能同时开启,默认都是3000端口
# 可以指定端口 5000
# npm run examples === node ./examples/server.js
node ./examples/server.js -p 5000
复制代码
打开http://localhost:5000,然后就可以开心的在Chrome
浏览器中调试examples
里的例子了。
axios
是支持 node
环境发送请求的。接下来看如何用 vscode
调试 node
环境下的axios
。
vscode 调试 node 环境的 axios
在根目录下 axios-analysis/
创建.vscode/launch
文件如下:
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}/axios/sandbox/client.js",
"skipFiles": [
"<node_internals>/**"
]
},
]
}
复制代码
按F5
开始调试即可,按照自己的情况,单步跳过(F10)
、单步调试(F11)
断点调试。
其实开源项目一般都有贡献指南axios/CONTRIBUTING.md
,笔者只是把这个指南的基础上修改为引用sourcemap
的文件可调试。
先看 axios 结构是怎样的
git clone https://github.com/lxchuan12/axios-analysis.git
cd axios-analaysis/axios
npm install
npm start
复制代码
按照上文说的调试方法, npm start
后,直接在 chrome
浏览器中调试。
打开 http://localhost:3000,在控制台打印出axios
,估计很多人都没打印出来看过。
console.log({axios: axios});
复制代码
层层点开来看,axios
的结构是怎样的,先有一个大概印象。
笔者画了一张比较详细的图表示。
看完结构图,如果看过jQuery
、underscore
和lodash
源码,会发现其实跟axios
源码设计类似。
jQuery
别名 $
,underscore``loadsh
别名(_)
也既是函数,也是对象。比如jQuery
使用方式。$('#id')
, $.ajax
。
接下来看具体源码的实现。可以跟着断点调试一下。
断点调试要领:
赋值语句可以一步跳过,看返回值即可,后续详细再看。
函数执行需要断点跟着看,也可以结合注释和上下文倒推这个函数做了什么。
axios 源码 初始化
看源码第一步,先看package.json
。一般都会申明 main
主入口文件。
// package.json
{
"name": "axios",
"version": "0.19.0",
"description": "Promise based HTTP client for the browser and node.js",
"main": "index.js",
// ...
}
复制代码
主入口文件
// index.js
module.exports = require('./lib/axios');
复制代码
lib/axios.js
主文件
axios.js
文件 代码相对比较多。分为三部分展开叙述。
- 第一部分:引入一些工具函数
utils
、Axios
构造函数、默认配置defaults
等。- 第二部分:是生成实例对象
axios
、axios.Axios
、axios.create
等。- 第三部分取消相关API实现,还有
all
、spread
、导出等实现。
第一部分
引入一些工具函数utils
、Axios
构造函数、默认配置defaults
等。
// 第一部分:
// lib/axios
// 严格模式
'use strict';
// 引入 utils 对象,有很多工具方法。
var utils = require('./utils');
// 引入 bind 方法
var bind = require('./helpers/bind');
// 核心构造函数 Axios
var Axios = require('./core/Axios');
// 合并配置方法
var mergeConfig = require('./core/mergeConfig');
// 引入默认配置
var defaults = require('./defaults');
复制代码
第二部分
是生成实例对象 axios
、axios.Axios
、axios.create
等。
/**
* Create an instance of Axios
*
* @param {Object} defaultConfig The default config for the instance
* @return {Axios} A new instance of Axios
*/
function createInstance(defaultConfig) {
// new 一个 Axios 生成实例对象
var context = new Axios(defaultConfig);
// bind 返回一个新的 wrap 函数,
// 也就是为什么调用axios是调用Axios.prototype.request 函数的原因
var instance = bind(Axios.prototype.request, context);
// Copy axios.prototype to instance
// 复制 Axios.prototype 到实例上。
// 也就是为什么 有 axios.get 等别名方法,
// 且调用的是 Axios.prototype.get 等别名方法。
utils.extend(instance, Axios.prototype, context);
// Copy context to instance
// 复制 context 到 intance 实例
// 也就是为什么默认配置 axios.defaults 和拦截器 axios.interceptors 可以使用的原因
// 其实是new Axios().defaults 和 new Axios().interceptors
utils.extend(instance, context);
// 最后返回实例对象,以上代码,在上文的图中都有体现。这时可以仔细看下上图。
return instance;
}
// Create the default instance to be exported
// 导出 创建默认实例
var axios = createInstance(defaults);
// Expose Axios class to allow class inheritance
// 暴露 Axios class 允许 class 继承 也就是可以 new axios.Axios()
// 但 axios 文档中 并没有提到这个,我们平时也用得少。
axios.Axios = Axios;
// Factory for creating new instances
// 工厂模式 创建新的实例 用户可以自定义一些参数
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
复制代码
看完第二部分,里面涉及几个工具函数,如bind
、extend
。接下来讲述这几个工具方法。
工具方法之 bind
./helpers/bind
'use strict';
// 返回一个新的函数 wrap
module.exports = 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];
}
// 把 argument 对象放在数组 args 里
return fn.apply(thisArg, args);
};
};
复制代码
传递两个参数函数和thisArg
指向。
把参数arguments
生成数组,最后调用返回参数结构。
其实现在 apply
支持 arguments
这样的类数组对象了,不需要手动转数组。
那么为啥作者要转数组,为了性能?当时不支持?抑或是作者不知道?这就不得而知了。有读者知道欢迎评论区告诉笔者呀。
关于apply
、call
和bind
等不是很熟悉的读者,可以看笔者的另一个面试官问系列
。
面试官问:能否模拟实现JS的bind方法
举个例子
function fn(){
console.log.apply(console.log, arguments);
}
fn(1,2,3,4,5,6, '若川');
// 1 2 3 4 5 6 '若川'
复制代码
工具方法之 utils.extend
/**
* Extends object a by mutably adding to it the properties of object b.
*
* @param {Object} a The object to be extended
* @param {Object} b The object to copy properties from
* @param {Object} thisArg The object to bind function to
* @return {Object} The resulting value of object a
*/
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;
}
复制代码
其实就是遍历参数 b
对象,复制到 a
对象上,如果是函数就是则用 bind
调用。
工具方法之 utils.forEach
遍历数组和对象。设计模式称之为迭代器模式。很多源码都有类似这样的遍历函数。比如大家熟知的jQuery
$.each
。
/**
* @param {Object|Array} obj The object to iterate
* @param {Function} fn The callback to invoke for each item
*/
function forEach(obj, fn) {
// Don't bother if no value provided
// 判断 null 和 undefined 直接返回
if (obj === null || typeof obj === 'undefined') {
return;
}
// Force an array if not already something iterable
// 如果不是对象,放在数组里。
if (typeof obj !== 'object') {
/*eslint no-param-reassign:0*/
obj = [obj];
}
// 是数组 则用for 循环,调用 fn 函数。参数类似 Array.prototype.forEach 的前三个参数。
if (isArray(obj)) {
// Iterate over array values
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
// Iterate over object keys
// 用 for in 遍历对象,但 for in 会遍历原型链上可遍历的属性。
// 所以用 hasOwnProperty 来过滤自身属性了。
// 其实也可以用Object.keys来遍历,它不遍历原型链上可遍历的属性。
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj);
}
}
}
}
复制代码
如果对Object
相关的API
不熟悉,可以查看笔者之前写过的一篇文章。JavaScript 对象所有API解析
第三部分
取消相关API实现,还有all
、spread
、导出等实现。
// Expose Cancel & CancelToken
// 导出 Cancel 和 CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
// Expose all/spread
// 导出 all 和 spread API
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
module.exports = axios;
// Allow use of default import syntax in TypeScript
// 也就是可以以下方式引入
// import axios from 'axios';
module.exports.default = axios;
复制代码
这里介绍下 spread
,取消的API
暂时不做分析。
假设你有这样的需求。
function f(x, y, z) {}
var args = [1, 2, 3];
f.apply(null, args);
复制代码
那么可以用spread
方法。用法:
axios.spread(function(x, y, z) {})([1, 2, 3]);
复制代码
实现也比较简单。源码实现:
/**
* @param {Function} callback
* @returns {Function}
*/
module.exports = function spread(callback) {
return function wrap(arr) {
return callback.apply(null, arr);
};
};
复制代码
上文var context = new Axios(defaultConfig);
,接下来介绍核心构造函数Axios
。
核心构造函数 Axios
lib/core/Axios.js
构造函数Axios
。
function Axios(instanceConfig) {
// 默认参数
this.defaults = instanceConfig;
// 拦截器 请求和响应拦截器
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
复制代码
Axios.prototype.request = function(config){
// 省略,这个是核心方法,后文结合例子详细描述
// code ...
var promise = Promise.resolve(config);
// code ...
return promise;
}
// 这是获取Uri的函数,这里省略
Axios.prototype.getUri = function(){}
// 提供一些请求方法的别名
// Provide aliases for supported request methods
// 遍历执行
// 也就是为啥我们可以 axios.get 等别名的方式调用,而且调用的是 Axios.prototype.request 方法
// 这个也在上面的 axios 结构图上有所体现。
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, data, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url,
data: data
}));
};
});
module.exports = Axios;
复制代码
接下来看拦截器部分。
拦截器管理构造函数 InterceptorManager
请求前拦截,和请求后拦截。
在Axios.prototype.request
函数里使用,具体怎么实现的拦截的,后文配合例子详细讲述。
如何使用:
// Add a request interceptor
// 添加请求前拦截器
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// 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);
});
复制代码
如果用完拦截器想移除,用eject
方法。
const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);
复制代码
拦截器也可以添加自定义的实例上。
const instance = axios.create();
instance.interceptors.request.use(function () {/*...*/});
复制代码
源码实现:
构造函数,handles
存储拦截器函数。
function InterceptorManager() {
this.handlers = [];
}
复制代码
接下来声明了三个方法:使用、移除、遍历。
InterceptorManager.prototype.use 使用
传递两个函数作为参数,返回数字 ID,用于移除拦截器
/**
* @param {Function} fulfilled The function to handle `then` for a `Promise`
* @param {Function} rejected The function to handle `reject` for a `Promise`
*
* @return {Number} An ID used to remove interceptor later
*/
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1;
};
复制代码
InterceptorManager.prototype.eject 移除
根据 use 返回的 ID 移除 拦截器
/**
* @param {Number} id The ID that was returned by `use`
*/
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};
复制代码
InterceptorManager.prototype.forEach 遍历
遍历执行 拦截器
/**
* @param {Function} fn The function to call for each interceptor
*/
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
};
复制代码
实例结合
上文叙述的调试时运行npm start
是用axios/sandbox/client.html
路径的文件作为示例的。
以下是一段这个文件中的代码。
axios(options)
.then(function (res) {
response.innerHTML = JSON.stringify(res.data, null, 2);
})
.catch(function (res) {
response.innerHTML = JSON.stringify(res.data, null, 2);
});
复制代码
先看调用栈流程
如果不想一步步调试,有个偷巧的方法。
知道 axios
使用了XMLHttpRequest
。
可以在项目中搜索:new XMLHttpRequest
。
定位到文件 axios/lib/adapters/xhr.js
在这条语句 var request = new XMLHttpRequest();
chrome
浏览器中 打个断点调试下,再根据调用栈来细看具体函数等实现。
Call Stack
dispatchXhrRequest (xhr.js:19)
xhrAdapter (xhr.js:12)
dispatchRequest (dispatchRequest.js:60)
Promise.then (async)
request (Axios.js:54)
wrap (bind.js:10)
submit.onclick ((index):138)
复制代码
简述下流程:
Send Request
按钮点击submit.onclick
- 调用
axios
函数实际上是调用Axios.prototype.request
函数,而这个函数使用bind
返回的一个名为wrap
的函数。 - 调用
Axios.prototype.request
- 执行拦截器
dispatchRequest
dispatchRequest
之后调用adapter (xhrAdapter)
- 最后调用
Promise
中的函数dispatchXhrRequest
如果仔细看了文章开始的axios 结构关系图
,其实对这个流程也有大概的了解。
接下来看 Axios.prototype.request
具体实现。
Axios.prototype.request 请求核心方法
Axios.prototype.request = function request(config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
// 这一段代码 其实就是 使 axios('lxchuan12.cn', [, config])
// config 参数可以省略
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
// 合并默认参数和用户传递的参数
config = mergeConfig(this.defaults, config);
// Set config.method
// 设置 请求方法,默认 get 。
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
// Hook up interceptors middleware
// 这段拆开到后文再讲述
};
复制代码
// Hook up interceptors middleware
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
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);
});
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
复制代码
很遗憾,在example
文件夹没有拦截器的例子。笔者在example
中在example/get
的基础上添加了一个拦截器的示例。axios/examples/interceptors
,便于读者调试。
node ./examples/server.js -p 5000
复制代码
promise = promise.then(chain.shift(), chain.shift());
这段代码打个断点。
会得到这样的这张图。
特别关注下,右侧,local
中的chain
数组。
也就是这样的结构。
var chain = [
'请求成功拦截2', '请求失败拦截2',
'请求成功拦截1', '请求失败拦截1',
dispatch, undefined,
'响应成功拦截1', '响应失败拦截1',
'响应成功拦截2', '响应失败拦截2',
]
复制代码
这段代码相对比较绕。
中间会调用dispatchRequest
方法。
promise.then('请求成功拦截2', '请求失败拦截2')
.then('请求成功拦截1', '请求失败拦截1')
.then(dispatchRequest, undefined)
.then('响应成功拦截1', '响应失败拦截1')
.then('响应成功拦截2', '响应失败拦截2')
复制代码
dispatchRequest(config) 这里的config
是请求成功拦截返回的。
dispatchRequest 最终派发请求
'use strict';
var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
/**
* Throws a `Cancel` if cancellation has been requested.
*/
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
/**
* Dispatch a request to the server using the configured adapter.
*
* @param {object} config The config that is to be used for the request
* @returns {Promise} The Promise to be fulfilled
*/
module.exports = function dispatchRequest(config) {
// 取消相关
throwIfCancellationRequested(config);
// Ensure headers exist
// 确保 headers 存在
config.headers = config.headers || {};
// Transform request data
// 转换请求的数据
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
// Flatten headers
// 拍平 headers
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers || {}
);
// 以下这些方法 删除 headers
utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);
// adapter 适配器部分 拆开 放在下文讲
};
复制代码
dispatchRequest 之 取消模块
可以使用cancel token
取消请求。
axios cancel token API 是基于撤销的 promise
取消提议。
The axios cancel token API is based on the withdrawn cancelable promises proposal.
文档上详细描述了两种使用方式。
很遗憾,在example
文件夹也没有取消的例子。笔者在example
中在example/get
的基础上添加了一个取消的示例。axios/examples/cancel
,便于读者调试。
node ./examples/server.js -p 5000
复制代码
request
中的拦截器和dispatch
中的取消这两个模块相对复杂,可以多调试调试,吸收消化。
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/get/server', {
cancelToken: source.token
}).catch(function (err) {
if (axios.isCancel(err)) {
console.log('Request canceled', err.message);
} else {
// handle error
}
});
// cancel the request (the message parameter is optional)
// 取消函数。
source.cancel('Operation canceled by the user.');
复制代码
// 通过 CancelToken 来取消请求操作
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
// 已经取消
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
/**
* 如果请求已经取消,抛出 Cancel 异常
*/
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
/**
* 通过 source 来返回 CancelToken 实例和取消 CancelToken 的函数
*/
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
module.exports = CancelToken;
复制代码
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
// 抛出异常
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
复制代码
发送请求的适配器里是这样使用的。
// xhr
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
复制代码
取消流程调用栈
1.source.cancel() 2.resolvePromise(token.reason); 3.config.cancelToken.promise.then(function onCanceled(cancel) {})
最后进入request.abort();``reject(cancel);
dispatchRequest 之 transformData 转换数据
上文的代码里有个函数 transformData
,这里解释下。其实就是遍历传递的函数数组 对数据操作,最后返回数据。
axios.defaults.transformResponse
数组中默认就有一个函数,所以使用concat
链接自定义的函数。
使用:
文件路径
axios/examples/transform-response/index.html
这段代码其实就是对时间格式的字符串转换成时间对象,可以直接调用getMonth
等方法。
var ISO_8601 = /(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})Z/;
function formatDate(d) {
return (d.getMonth() + 1) + '/' + d.getDate() + '/' + d.getFullYear();
}
axios.get('https://api.github.com/users/mzabriskie', {
transformResponse: axios.defaults.transformResponse.concat(function (data, headers) {
Object.keys(data).forEach(function (k) {
if (ISO_8601.test(data[k])) {
data[k] = new Date(Date.parse(data[k]));
}
});
return data;
})
})
.then(function (res) {
document.getElementById('created').innerHTML = formatDate(res.data.created_at);
});
复制代码
源码:
就是遍历数组,调用数组里的传递 data
和 headers
参数调用函数。
module.exports = function transformData(data, headers, fns) {
/*eslint no-param-reassign:0*/
utils.forEach(fns, function transform(fn) {
data = fn(data, headers);
});
return data;
};
复制代码
dispatchRequest 之 adapter 适配器执行部分
适配器,在设计模式中称之为适配器模式。讲个生活中简单的例子,大家就容易理解。
我们常用以前手机耳机孔都是圆孔,而现在基本是耳机孔和充电接口合二为一。统一为typec
。
这时我们需要需要一个typec转圆孔的转接口
,这就是适配器。
// adapter 适配器部分
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// Transform response data
// 转换响应的数据
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
// 取消相关
throwIfCancellationRequested(config);
// Transform response data
// 转换响应的数据
if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
});
复制代码
接下来看具体的 adapter
。
adapter 适配器 真正发送请求
var adapter = config.adapter || defaults.adapter;
复制代码
看了上文的 adapter
,可以知道支持用户自定义。比如可以通过微信小程序 wx.request
按照要求也写一个 adapter
。
接着来看下 defaults.ddapter
。
文件路径:axios/lib/defaults.js
根据当前环境引入,如果是浏览器环境引入xhr
,是node
环境则引入http
。
类似判断node
环境,也在sentry-javascript
源码中有看到。
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
}
var defaults = {
adapter: getDefaultAdapter(),
// ...
};
复制代码
xhr
接下来就是我们熟悉的 XMLHttpRequest
对象。
可能读者不了解可以参考XMLHttpRequest MDN 文档。
主要提醒下:onabort
是请求取消事件,withCredentials
是一个布尔值,用来指定跨域 Access-Control
请求是否应带有授权信息,如 cookie
或授权 header
头。
这块代码有删减,具体可以看axios 仓库 xhr.js,也可以克隆笔者的axios-analysis
仓库调试时具体分析。
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// 这块代码有删减
var request = new XMLHttpRequest();
request.open()
request.timeout = config.timeout;
// 监听 state 改变
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// ...
}
// 取消
request.onabort = function(){};
// 错误
request.onerror = function(){};
// 超时
request.ontimeout = function(){};
// cookies 跨域携带 cookies 面试官常喜欢考这个
// 一个布尔值,用来指定跨域 Access-Control 请求是否应带有授权信息,如 cookie 或授权 header 头。
// Add withCredentials to request if needed
if (!utils.isUndefined(config.withCredentials)) {
request.withCredentials = !!config.withCredentials;
}
// 上传下载进度相关
// Handle progress if needed
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', config.onDownloadProgress);
}
// Not all browsers support upload events
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress);
}
// Send the request
// 发送请求
request.send(requestData);
});
}
复制代码
而实际上现在 fetch
支持的很好了,阿里开源的 umi-request 请求库,就是用fetch
封装的,而不是用XMLHttpRequest
。
文章末尾,大概讲述下 umi-request
和 axios
的区别。
http
http
这里就不详细叙述了,感兴趣的读者可以自行查看。
module.exports = function httpAdapter(config) {
return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {
});
};
复制代码
能读到最后,说明你已经超过很多人啦^_^
文章写到这里就基本到接近尾声了。
对比其他请求库
KoAjax
FCC成都社区负责人水歌开源的KoAJAX。
如何用开源软件办一场技术大会? 以下这篇文章中摘抄的一段。
前端请求库 —— KoAJAX 国内前端同学最常用的 HTTP 请求库应该是 axios 了吧?虽然它的 Interceptor(拦截器)API 是 .use(),但和 Node.js 的 Express、Koa 等框架的中间件模式完全不同,相比 jQuery .ajaxPrefilter()、dataFilter() 并没什么实质改进;上传、下载进度比 jQuery.Deferred() 还简陋,只是两个专门的回调选项。所以,它还是要对特定的需求记忆特定的 API,不够简洁。
幸运的是,水歌在研究如何用 ES 2018 异步迭代器实现一个类 Koa 中间件引擎的过程中,做出了一个更有实际价值的上层应用 —— KoAJAX。它的整个执行过程基于 Koa 式的中间件,而且它自己就是一个中间件调用栈。除了 RESTful API 常用的 .get()、.post()、.put()、.delete() 等快捷方法外,开发者就只需记住 .use() 和 next(),其它都是 ES 标准语法和 TS 类型推导。
umi-request 阿里开源的请求库
umi-request
与 fetch
, axios
异同。
不得不说,umi-request
确实强大,有兴趣的读者可以阅读下其源码。
看懂axios
的基础上,看懂umi-request
源码应该不难。
比如 umi-request
取消模块代码几乎与axios
一模一样。
总结
文章详细介绍了 axios
调试方法。详细介绍了 axios 构造函数,拦截器,取消等功能的实现。最后还对比了其他请求库。
axios
源码相对不多,打包后一千多行,比较容易看完,非常值得学习。
建议 clone
若川的 axios-analysis github 仓库,按照文中方法自己调试
基于Promise
,request
中的拦截器和dispatch
中的取消这两个模块相对复杂,可以多调试调试,吸收消化。
axios 既是函数,又是对象。
axios
源码中使用了挺多设计模式。比如迭代器模式、适配器模式等。如果想系统学习设计模式,一般比较推荐豆瓣评分9.1的JavaScript设计模式与开发实践
如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持,非常感谢呀。
推荐阅读
写文章前,搜索了以下几篇文章泛读了一下。有兴趣在对比看看以下这几篇,有代码调试的基础上,看起来也快。
一直觉得多搜索几篇文章看,对自己学习知识更有用。有个词语叫主题阅读。大概意思就是一个主题一系列阅读。
@叫我小明呀:Axios 源码解析
@尼库尼库桑:深入浅出 axios 源码
@小贼先生_ronffy:Axios源码深度剖析 - AJAX新王者
逐行解析Axios源码
[译]axios 是如何封装 HTTP 请求的
知乎@Lee : TypeScript 重构 Axios 经验分享
笔者另一个系列
面试官问:JS的继承
面试官问:JS的this指向
面试官问:能否模拟实现JS的call和apply方法
面试官问:能否模拟实现JS的bind方法
面试官问:能否模拟实现JS的new操作符
关于
作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
若川的博客,使用vuepress
重构了,阅读体验可能更好些
掘金专栏,欢迎关注~
segmentfault
前端视野专栏,欢迎关注~
知乎前端视野专栏,欢迎关注~
github blog,相关源码和资源都放在这里,求个star
^_^~
欢迎加微信交流 微信公众号
可能比较有趣的微信公众号,长按扫码关注。欢迎加笔者微信lxchuan12
(注明来源,基本来者不拒),拉您进【前端视野交流群】,长期交流学习~