龙空技术网

一文深入浅出解析Axios源码

Echa攻城狮 129

前言:

今天大家对“ajax5分钟canceled”可能比较关注,你们都想要分析一些“ajax5分钟canceled”的相关内容。那么小编同时在网上搜集了一些有关“ajax5分钟canceled””的相关内容,希望咱们能喜欢,大家一起来了解一下吧!

作者:老道(专栏作者)

转发链接:

本文概要前言Axios整体代码结构分析Axios执行流程整体分析Axios执行文件分析前言

axios 是目前最常用的 http 请求库,可以用于浏览器和 node.js , 在 github 上已有 43k star 左右之多。

Axios 的主要特性包括:

基于 Promise支持浏览器和 node.js可添加拦截器和转换请求和响应数据请求可以取消自动转换 JSON 数据客户端支持防范 XSRF

本文 将 带大家一起阅读 axios 的源码, 按照执行流程解析当中的一些封装技巧,分析功能实现。

Axios整体代码结构分析

├── /dist/                     # 项目输出目录├── /lib/                      # 项目源码目录│ ├── /cancel/                 # 定义取消功能│ ├── /core/                   # 一些核心功能│ │ ├── Axios.js               # axios的核心主类│ │ ├── dispatchRequest.js     # 用来调用http请求适配器方法发送请求│ │ ├── InterceptorManager.js  # 拦截器构造函数│ │ └── settle.js              # 根据http响应状态,改变Promise的状态│ ├── /helpers/                # 一些辅助方法│ ├── /adapters/               # 定义请求的适配器 xhr、http│ │ ├── http.js                # 实现http适配器│ │ └── xhr.js                 # 实现xhr适配器│ ├── axios.js                 # 对外暴露接口│ ├── defaults.js              # 默认配置│ └── utils.js                 # 公用工具├── package.json               # 项目信息├── index.d.ts                 # 配置TypeScript的声明文件└── index.js                   # 入口文件
Axios执行流程整体分析Axios执行文件分析入口 /lib/axios.js
function createInstance(defaultConfig) {    var context = new Axios(defaultConfig);    // 自定义bind方法,等同于var instance = Axios.prototype.request.bind(context)    // 这句的作用是将request方法指向instance,上下文是context,可以使用instance(option)    // 调用  var instance = bind(Axios.prototype.request, context);    // 根据前面extend方法,使Axios.prototype的方法扩展到instance对象上,这样instance就有了get,post,put等方法    // 并且制定上细纹是context, 这样axios中方法, this执行context    // utils.extend(instance, Axios.prototype, context);    // 将context自身属性和方法扩展到instance上, 因为extend内部使用的forEach方法对对象做for in 遍历时, 只遍历对象本身的属性, 而不会遍历原型链上的属性    // 这样,instance 就有了  defaults、interceptors 属性。(这两个属性后面我们会介绍)    // Copy context to instance    utils.extend(instance, context);    return instance;}// 接收默认配置项作为参数(后面会介绍配置项),创建一个Axios实例,最终会被作为对象导出var axios = createInstance(defaults);

/lib/core/Axios.js是核心包,axios各种方式('delete', 'get', 'head', 'options','post', 'put', 'patch')都是通过request方法发出的。每个axios实例都有一个interceptors实例属性,interceptors对象上有两个属性request、response。InterceptorManager用来实现拦截器的,这个构造函数原型上有3个方法:use、eject、forEach。我们首先看下 InterceptorManager的实现:

拦截器包 /lib/core/InterceptorManager.js

function InterceptorManager() {    this.handlers = []; // 存放拦截器方法,数组内每一项都是有两个属性的对象,两个属性分别对应成功和失败后执行的函数。}// 往拦截器里添加拦截方法,使用 unshift,会导致use 后添加的先执行,先添加的后执行,use就是我们在http.js中书写的拦截器InterceptorManager.prototype.use = function use(fulfilled, rejected) {    this.handlers.push({        fulfilled: fulfilled,        rejected: rejected    });    return this.handlers.length - 1;};// 用来注销指定的拦截器InterceptorManager.prototype.eject = function eject(id) {    if (this.handlers[id]) {        this.handlers[id] = null;    }};// 遍历this.handlers,并将this.handlers里的每一项作为参数传给fn执行InterceptorManager.prototype.forEach = function forEach(fn) {    utils.forEach(this.handlers, function forEachHandler(h) {        if (h !== null) {            fn(h);        }    });};
核心包 /lib/core/Axios.js

Axios.js主要处理了两个部分,复用请求方法、实现拦截器。 当我们使用 Axios 的实例去发送请求,使用的方法get、post等都是复用了request方法,在request方法中通过 arguments 获取传入的参数,实现了传入参数的重载。 拦截器是axios的一大特色,它的实现原理其实不复杂,核心就是promise的链式调用。

function Axios(instanceConfig) {    this.defaults = instanceConfig;    this.interceptors = {        request: new InterceptorManager(),        response: new InterceptorManager()    };}Axios.prototype.request = function request(config) {    /*eslint no-param-reassign:0*/    // Allow for axios('example/url'[, config]) a la fetch API    if (typeof config === 'string') {        config = arguments[1] || {};        config.url = arguments[0];    } else {        config = config || {};    }    config = mergeConfig(this.defaults, config);    config.method = config.method ? config.method.toLowerCase() : 'get';    // Hook up interceptors middleware    // chain 是一个数组    var chain = [dispatchRequest, undefined];    var promise = Promise.resolve(config);    // 使用 use 添加 fulfilled 与 rejected 添加到队列中    // 添加 request 拦截函数的时候使用的是unshift, 这样会导致 use 后添加的先执行,先添加的后执行    this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {        chain.unshift(interceptor.fulfilled, interceptor.rejected);    });    // response使用的是push,添加拦截相应函数,这里是先添加先执行,后添加后执行。    this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {        chain.push(interceptor.fulfilled, interceptor.rejected);    });    // chain  [fulfilled, rejected, ... dispatchRequest, undefined ....,fulfilled, rejected]    // 这里补充一下 fulfilled, rejected 都是肯定是成对出现的, 具体原因可看 InterceptorManager.prototype.use    // promise.then(undefined, undefined) 中当传递的不是function时,会发生值穿。也就是说 use 中可以传入非function,    // 或者传入单个function    while (chain.length) {        promise = promise.then(chain.shift(), chain.shift());    }    return promise;};// 复用request 实现了 delete, get, head, optionsutils.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        }));    };});// 复用request 实现了 delete, get, head, optionsutils.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        }));    };});

Axios.prototype.request方法会调用dispatchRequest方法,而dispatchRequest方法会调用xhrAdapter方法,xhrAdapter方法返回的是还一个Promise对象,调用defaults的getDefaultAdapter方法,如果是浏览器端使用xhr,如果是服务端调用的是http。接下来我们一次解析dispatchRequest,xhrAdapter,xhr 文件。

/lib/core/dispatchRequest.js

dispatchRequest做了三件事

拿到config对象,对config进行传给http请求适配器前的最后处理http请求适配器根据config配置,发起请求http请求适配器请求完成后,如果成功则根据header、data、和config.transformResponse

module.exports = function dispatchRequest(config) {    throwIfCancellationRequested(config);    // 如果包含baseUrl, 并且不是config.url绝对路径,组合baseUrl以及config.url    if (config.baseURL && !isAbsoluteURL(config.url)) {        // 组合baseURL与url形成完整的请求路径        config.url = combineURLs(config.baseURL, config.url);    }    config.headers = config.headers || {};    // 使用/lib/defaults.js中的transformRequest方法,对config.headers和config.data进行格式化    // 比如将headers中的Accept,Content-Type统一处理成大写    // 比如如果请求正文是一个Object会格式化为JSON字符串,并添加application/json;charset=utf-8的Content-Type    // 等一系列操作    config.data = transformData(        config.data,        config.headers,        config.transformRequest    );    // 合并不同配置的headers,config.headers的配置优先级更高    config.headers = utils.merge(        config.headers.common || {},        config.headers[config.method] || {},        config.headers || {}    );    // 删除headers中的method属性    utils.forEach(        ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],        function cleanHeaderConfig(method) {            delete config.headers[method];        }    );    // 如果config配置了adapter,使用config中配置adapter的替代默认的请求方法    var adapter = config.adapter || defaults.adapter;    // 使用adapter方法发起请求(adapter根据浏览器环境或者Node环境会有不同,http请求适配器会优先使用config上自定义的适配器,没有配置时才会使用默认的XHR或http适配器,不过大部分时候,axios提供的默认适配器是能够满足我们的    return adapter(config).then(        // 请求正确返回的回调        function onAdapterResolution(response) {            // 判断是否以及取消了请求,如果取消了请求抛出以取消            throwIfCancellationRequested(config);            // 使用/lib/defaults.js中的transformResponse方法,对服务器返回的数据进行格式化            // 例如,使用JSON.parse对响应正文进行解析            response.data = transformData(                response.data,                response.headers,                config.transformResponse            );            return response;        },        // 请求失败的回调        function onAdapterRejection(reason) {            if (!isCancel(reason)) {                throwIfCancellationRequested(config);                if (reason && reason.response) {                    reason.response.data = transformData(                        reason.response.data,                        reason.response.headers,                        config.transformResponse                    );                }            }            return Promise.reject(reason);        }    );};

dispatchRequest方法内,首先得到xhrAdapter方法返回的Promise对象, 然后通过.then方法,对xhrAdapter返回的Promise对象的成功或失败结果再次加工, 成功的话,则将处理后的response返回, 失败的话,则返回一个状态为rejected的Promise对象. 用户调用axios()方法时,就可以直接调用Promise的.then或.catch进行业务处理了。

return adapter(config).then(function onAdapterResolution(response) {    // ...    return response;}, function onAdapterRejection(reason) {    // ...    return Promise.reject(reason);});};

可以看到adapter 若config没有配置adapter使用的是defaults的adapter,我们首先看下defaults中的adapter函数,稍后会详细介绍defaults源码,

function getDefaultAdapter() {    var adapter;    // Only Node.JS has a process variable that is of [[Class]] process    if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {        // For node use HTTP adapter        adapter = require('./adapters/http');    } else if (typeof XMLHttpRequest !== 'undefined') {        // For browsers use XHR adapter        adapter = require('./adapters/xhr');    }    return adapter;}

调用defaults的getDefaultAdapter方法,如果是浏览器端使用xhr,如果是服务端调用的是http。我们接下来看下xhr文件。

/lib/adapters/xhr.js

xhr.js导出的xhrAdapter方法是axios在浏览器环境下使用的默认请求方法。我们可以在配置中使用adapter配置项对默认请求方法进行替换。

module.exports = function xhrAdapter(config) {    return new Promise(function dispatchXhrRequest(resolve, reject) {        var requestData = config.data;        var requestHeaders = config.headers;        // 判断是否是FormData对象, 如果是, 删除header的Content-Type字段,让浏览器自动设置Content-Type字段        if (utils.isFormData(requestData)) {            delete requestHeaders['Content-Type'];        }        // 创建xtr对象        var request = new XMLHttpRequest();        // 设置http请求头中的Authorization字段        // 关于Authorization字段        // 更多内容参考        if (config.auth) {            var username = config.auth.username || '';            var password = config.auth.password || '';            // 使用btoa方法base64编码username和password            requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);        }        // 初始化请求方法        // open(method: 请求的http方法, url: 请求的url地址, 是否支持异步)        request.open(            config.method.toUpperCase(),            buildURL(config.url, config.params, config.paramsSerializer),            true        );        // 设置超时时间        request.timeout = config.timeout;        // 监听readyState状态的变化,当readyState状态为4的时候,表示ajax请求成功        request.onreadystatechange = function handleLoad() {            if (!request || request.readyState !== 4) {                return;            }            // request.status响应的数字状态码,在完成请求前数字状态码等于0            // 如果request.status出错返回的也是0,但是file协议除外,status等于0也是一个成功的请求            // 更多内容请参考             if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {                return;            }            // getAllResponseHeaders方法会返回所有的响应头            // 更多内容请参考             var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;            // 如果没有设置数据响应类型(默认为“json”)或者responseType设置为text时,获取request.responseText值否则是获取request.response            // responseType是一个枚举类型,手动设置返回数据的类型 更多请参考             // responseText是全部后端的返回数据为纯文本的值 更多请参考             // response为正文,response的类型取决于responseType 更多请参考             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            };            // status >= 200 && status < 300 resolve            // 否则reject            settle(resolve, reject, response);            request = null;        };        // ajax中断时触发        request.onabort = function handleAbort() {            if (!request) {                return;            }            // 抛出Request aborted错误            reject(createError('Request aborted', config, 'ECONNABORTED', request));            request = null;        };        // ajax失败时触发        request.onerror = function handleError() {            // 抛出Network Error错误            reject(createError('Network Error', config, null, request));            request = null;        };        // ajax请求超时时调用        request.ontimeout = function handleTimeout() {            // 抛出 timeout错误            reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED',                request));            request = null;        };        // 判断当前是为标准浏览器环境,如果是,添加xsrf头        // 什么是xsrf header?xsrf header是用来防御CSRF攻击        // 原理是服务端生成一个XSRF-TOKEN,并保存到浏览器的cookie中,在每次请求中ajax都会将XSRF-TOKEN设置到request header中        // 服务器会比较cookie中的XSRF-TOKEN与header中XSRF-TOKEN是否一致        // 根据同源策略,非同源的网站无法读取修改本源的网站cookie,避免了伪造cookie        if (utils.isStandardBrowserEnv()) {            var cookies = require('./../helpers/cookies');            // withCredentials设置跨域请求中是否应该使用cookie 更多请参考             // (设置了withCredentials为true或者是同源请求)并且设置xsrfCookieName            var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ?                // 读取cookie中XSRF-TOKEN                cookies.read(config.xsrfCookieName) :                undefined;            if (xsrfValue) {                // 在request header中设置XSRF-TOKEN                requestHeaders[config.xsrfHeaderName] = xsrfValue;            }        }        // setRequestHeader是用来设置请求头部的方法        if ('setRequestHeader' in request) {            // 将config中配置的requestHeaders,循环设置到请求头上            utils.forEach(requestHeaders, function setRequestHeader(val, key) {                if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {                    delete requestHeaders[key];                } else {                    request.setRequestHeader(key, val);                }            });        }        // 设置xhr对象的withCredentials属性,是否允许cookie进行跨域请求        if (config.withCredentials) {            request.withCredentials = true;        }        // 设置xhr对象的responseType属性        if (config.responseType) {            try {                request.responseType = config.responseType;            } catch (e) {                if (config.responseType !== 'json') {                    throw e;                }            }        }        // 下载进度        if (typeof config.onDownloadProgress === 'function') {            request.addEventListener('progress', config.onDownloadProgress);        }        // 上传进度        // request.upload XMLHttpRequest.upload 属性返回一个 XMLHttpRequestUpload对象,用来表示上传的进度        if (typeof config.onUploadProgress === 'function' && request.upload) {            request.upload.addEventListener('progress', config.onUploadProgress);        }        if (config.cancelToken) {            // 取消请求,在介绍/lib/cancel/CancelToken.js中以及介绍,这里不在赘述            config.cancelToken.promise.then(function onCanceled(cancel) {                if (!request) {                    return;                }                request.abort();                reject(cancel);                request = null;            });        }        if (requestData === undefined) {            requestData = null;        }        // 发送http请求        request.send(requestData);    });};

xhrAdapter内的XHR发送请求成功后会执行这个Promise对象的resolve方法,并将请求的数据传出去, 反之则执行reject方法,并将错误信息作为参数传出去。接下来走settle.js.

function settle(resolve, reject, response) {    var validateStatus = response.config.validateStatus;    if (!response.status || !validateStatus || validateStatus(response.status)) {        resolve(response);    } else {        reject( /**/ );    }};

dispatch 中 transformData对数据进行处理的,以及defaults的getDefaultAdapter方法,我们接下来看下/lib/defaults.js。

/lib/defaults.js

defaults.js文件中配置了,axios默认的请求头、不同的环境下axios默认使用的请求方法、格式化请求正文的方法,格式化响应正文方法等内容

var utils = require('./utils');var normalizeHeaderName = require('./helpers/normalizeHeaderName');// 默认Content-Typevar DEFAULT_CONTENT_TYPE = {    'Content-Type': 'application/x-www-form-urlencoded'};// 设置ContentType,在没有设置的情况下function setContentTypeIfUnset(headers, value) {    if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {        headers['Content-Type'] = value;    }}// 根据当前环境,获取默认的请求方法function getDefaultAdapter() {    var adapter;    // 判断当前环境是否存在process对象    if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {        // node环境        adapter = require('./adapters/http');    } else if (typeof XMLHttpRequest !== 'undefined') {        // 浏览器环境        adapter = require('./adapters/xhr');    }    return adapter;}var defaults = {    // 默认的请求方法    adapter: getDefaultAdapter(),    // 格式化请求requestData,这会请求发送前使用,在默认情况下,axios将会自动的将传入的data对象序列化为JSON字符串,    transformRequest: [        function transformRequest(data, headers) {            // 格式化header属性名,将header中不标准的属性名,格式化为Accept属性名            normalizeHeaderName(headers, 'Accept');            // 格式化header属性名,将header中不标准的属性名,格式化为Content-Type属性名            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;            }            // URLSearchParams提供了一些用来处理URL查询字符串接口            // 如果是URLSearchParams对象            if (utils.isURLSearchParams(data)) {                // Content-Type设置为application/x-www-form-urlencoded                // application/x-www-form-urlencoded,数据被编码成以&分隔的键值对                setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');                return data.toString();            }            // 如果是对象            if (utils.isObject(data)) {                // Content-Type设置为application/json                setContentTypeIfUnset(headers, 'application/json;charset=utf-8');                // 将请求正文格式化为JSON字符串,并返回                return JSON.stringify(data);            }            return data;        }    ],    // 格式化响应resposeData,这会响应接受后使用,将响应数据中的JSON字符串转换为JavaScript对象    transformResponse: [        function transformResponse(data) {            if (typeof data === 'string') {                try {                    data = JSON.parse(data);                } catch (e) { /* Ignore */ }            }            return data;        }    ],    // 默认超时时间    timeout: 0,    // xsrf设置的cookie的key    xsrfCookieName: 'XSRF-TOKEN',    // xsrf设置header的key    xsrfHeaderName: 'X-XSRF-TOKEN',    maxContentLength: -1,    // 验证请求的状态    // 在处理请求的Promise会被使用    validateStatus: function validateStatus(status) {        return status >= 200 && status < 300;    }};defaults.headers = {    // 通用的HTTP字段    // Accept告知客户端可以处理的类型    common: {        'Accept': 'application/json, text/plain, */*'    }};utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {    defaults.headers[method] = {};});// 为post,put,patch请求设置默认的Content-Typeutils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {    defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);});module.exports = defaults;

默认的defaults配置项里已经自定义了一个请求转换器和一个响应转换器,请求转换器的使用地方是http请求前,使用请求转换器对请求数据做处理, 然后传给http请求适配器使用,在默认情况下,axios将会自动的将传入的data对象序列化为JSON字符串,将响应数据中的JSON字符串转换为JavaScript对象,看下transformData方法的代码, 主要遍历转换器数组,分别执行每一个转换器,根据data和headers参数,返回新的data。响应转换器的使用地方是在http请求完成后,根据http请求适配器的返回值做数据转换处理。

/lib/core/transformData.js

function transformData(data, headers, fns) {    utils.forEach(fns, function transform(fn) {        data = fn(data, headers);    });    return data;};// /lib/core/dispatchRequest.jsreturn adapter(config).then(function onAdapterResolution(response) {    // ...    response.data = transformData(        response.data,        response.headers,        config.transformResponse    );    return response;}, function onAdapterRejection(reason) {    if (!isCancel(reason)) {        // ...        if (reason && reason.response) {            reason.response.data = transformData(                reason.response.data,                reason.response.headers,                config.transformResponse            );        }    }    return Promise.reject(reason);});

拦截器同样可以实现转换请求和响应数据的需求,但根据作者的设计和综合代码可以看出, 在请求时,拦截器主要负责修改config配置项,数据转换器主要负责转换请求体,比如转换对象为字符串 在请求响应后,拦截器可以拿到response,数据转换器主要负责处理响应体,比如转换字符串为对象。

axios 的大体流程 如上述般大体介绍完了。

作者:老道(专栏作者)

转发链接:

标签: #ajax5分钟canceled #ajaxtimeout报错 #js和xhr