Axios 中的公共方法
1、关键点(思路)
如果要取消请求的话,可以通过调用 XMLHttpRequest 对象上的 abort 方法来取消请求:
let xhr = new XMLHttpRequest();
xhr.open("GET", "https://developer.mozilla.org/", true);
xhr.send();
setTimeout(() => xhr.abort(), 300);
取消请求用xhr.abort(),那么什么时候执行这个取消请求的操作呢?
得有一个信号告诉 xhr 对象什么时候执行取消操作。
取消请求就是未来某个时候要做的事情,那就想到是 Promise。
Promise 的 then 方法只有 Promise 对象的状态变为 resolved 的时候才会执行。
利用这个特点,在 Promise 对象的 then 方法中执行取消请求的操作。
2、使用
而对于 Axios 来说,我们可以通过 Axios 内部提供的 CancelToken 来取消请求:
两种方法
一:
const cancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) { // 源码有,判断是否取消请求
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
二:还可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token:
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});
// cancel the request
cancel();
3、源码
axios.js中引入
let defaults = require('./defaults')
let utils = require('./utils')
function createInstance(defaultConfig) {
// 创建 axios 实例
var context = new Axios(defaultConfig);
// 把 instance 指向 Axios.prototype.request 方法
// bind返回一个新的函数,内部去执行request
var instance = bind(Axios.prototype.request, context);
// extend 将第二个参数对象的方法拷贝到第一个对象上去
// 把 Axios.prototype(原型对象) 上的方法扩展到 instance 上,指定上下文是 context
// 方法有:request()/get()/post()/put()/delete()
utils.extend(instance, Axios.prototype, context);
// 把 context 上的方法扩展到 instance 上
// 把 Axios实例对象 上的方法扩展到 instance 上,有defaults 和 interceptors属性
utils.extend(instance, context);
// 导出 instance 对象
return instance;
}
// 实例一个 axios
var axios = createInstance(defaults);
// 添加 create 方法,返回 createInstance 函数,参数为自定义配置 + 默认配置
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// ...
// 向这个实例添加 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
module.exports = axios;
// Allow use of default import syntax in TypeScript
module.exports.default = axios;
CancelToken.js
CancelToken
挂载 source
方法用于创建自身实例,并且返回 {token, cancel}
token
是构造函数 CancelToken
的实例,cancel
方法接收构造函数 CancelToken
内部的一个 cancel
函数,用于取消请求
创建实例中,有一步是创建处于 pengding
状态的 promise
,并挂在实例方法上,外部通过参数 cancelToken
将实例传递进 axios
内部,内部调用 cancelToken.promise.then
等待状态改变
当外部调用方法 cancel
取消请求,pendding
状态就变为 resolve
,即取消请求并且抛出 reject(message)
export class CancelToken {
// new的时候执行构造函数
constructor(exactor) {
if(typeof exactor !== 'function') {
throw new TypeError('executor must be a function.')
}
// 将promise的决定权交给了cancel函数
// 同时做了防止多次cancel,
// promise里将resolve存到外部,即外部也可以调用
const resolvePromise;
this.promise = new promise(resolve => {
resolvePromise = resolve
})
this.reason = undefined
const cancel = (message) => {
// 如果有reason说明已经取消过了,直接return
if (this.reason) {
// 取消过的直接返回
return
}
// 外部调用 cancel 取消请求方法,Cancel 实例化,保存 message 并增加已取消请求标示
// new Cancel(message) 后等于 { message, __CANCEL__ : true}
this.reason = new Cancel(message)
// 将取消请求的promise指定为成功,值为reason
resolvePromise(this.reason)
}
// 立即执行接受的执行器函数,并传入用于取消请求的cancel函数
exactor(cancel)
}
// source 其实本质上是一个语法糖 里面做了封装
static source () {
/**
* 构造函数 CancelToken 实例化,用回调函数做参数,并且回调函数
* 接收 CancelToken 内部的函数 c,保存在变量 cancel 中,
* 后面调用 cancel 即取消请求
*/
const cancel;
const token = new CancelToken(function executor(c) {
cancel = c
})
return {
cancel: cancel,
token: token
}
}
}
Cancel.js
'use strict';
/**
* 当取消一个请求时, 需要将Cancel对象作为一个error抛出
* A `Cancel` is an object that is thrown when an operation is canceled.
*
* @class
* @param {string=} message The message.
*/
function Cancel(message) {
this.message = message;
}
Cancel.prototype.toString = function toString() {
return 'Cancel' + (this.message ? ': ' + this.message : '');
};
// 用于标识是一个取消的error
Cancel.prototype.__CANCEL__ = true;
module.exports = Cancel;
isCancel.js
'use strict';
/*
用于判断一个error是不是一个cancel错误
*/
module.exports = function isCancel(value) {
return !!(value && value.__CANCEL__);
};
xhr.js
当我们执行 const source = CancelToken.source()的时候,source 对象有两个字段,
一个是 token 对象,另一个是 cancel 函数。
module.exports = function xhrAdapter (config) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
// 设置完整路径,拼接url,例如:https://www.baidu,com + /api/test
var fullPath = buildFullPath(config.baseURL, config.url);
// 初始化一个请求,拼接url,例如:https://www.baidu,com/api/test + ?a=10&b=20
xhr.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// 超时断开,默认 0 永不超时
xhr.timeout = config.timeout;
xhr.onreadystatechange = function() {
// request不存在或请求状态不是4, 直接结束
if (!request || request.readyState !== 4) {
return;
}
// The request errored out and we didn't get a response, this will be
// handled by onerror instead
// With one exception: request that using file: protocol, most browsers
// will return status as 0 even though it's a successful request
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}
// 准备response对象
var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request
}
// 根据响应状态码来确定请求的promise的结果状态(成功/失败)
settle(resolve, reject, response);
// 将请求对象赋空
request = null;
};
// 绑定请求中断监听
request.onabort = function handleAbort() {
if (!request) {
return;
}
// reject promise, 指定aborted的error
reject(createError('Request aborted', config, 'ECONNABORTED', request));
// Clean up request
request = null;
};
// Handle low level network errors
request.onerror = function handleError() {
// Real errors are hidden from us by the browser
// onerror should only fire if it's a network error
reject(createError('Network Error', config, null, request));
// Clean up request
request = null;
};
// Handle timeout
request.ontimeout = function handleTimeout() {
reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED',
request));
// Clean up request
request = null;
};
// 标准浏览器(有 window 和 document 对象)
if (utils.isStandardBrowserEnv()) {
// 非同源请求,需要设置 withCredentials = true,才会带上 cookie
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
// request对象携带 headers 去请求
if ('setRequestHeader' in xhr) {
utils.forEach(requestHeaders, function setRequestHeader(val, key) {
if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
// data 为 undefined 时,移除 content-type,即不是 post/put/patch 等请求
delete requestHeaders[key];
} else {
xhr.setRequestHeader(key, val);
}
});
}
// 如果有配置config.cancelToken
if(config.cancelToken) {
// 处理取消的 config.cancelToken.promise. --> CancelToken.js里
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!xhr) {
return
}
// 中断请求
xhr.abort()
// 让请求的promise失败
reject(cancel)
// clean up xhr,请求结束要把xhr置为null, 不为null说明请求还没有完成
xhr = null
})
}
// 发送请求
xhr.send(requestData)
})
}
这里只做了get处理,主要的目的就是为了axios是如何取消请求的
CancelToken 的构造函数中需要传入一个函数,
而这个函数的作用其实是为了将能控制内部 Promise 的 resolve 函数暴露出去,暴露给 source 的 cancel 函数。
这样内部的 Promise 状态就可以通过 source.cancel() 方法来控制啦