Axios 源码解析

1,300 阅读4分钟

日常开发离不开前后端数据交互,于是了解一个请求库至关重要。本文通过对axios请求库的用法及特性源码的解读,深入分析axios,带你彻底掌握axios。

Features

  • Make XMLHttpRequests from the browser
  • Make http requests from node.js
  • Supports the Promise API
  • Intercept request and response
  • Transform request and response data
  • Cancel requests
  • Automatic transforms for JSON data
  • Client side support for protecting against XSRF

Config order of precedence

以下优先级依次递增

axios.defaults.baseURL = 'https://api.example.com';
  • Custom instance defaults
const instance = axiso.create({
    baseURL: 'https://api.example.com'
});
  • request config
instance.get('/users', {
    baseURL: 'https://api.github.com'
})

Supports the Promise API

Usage

// Promise 
instance.get('/users').then(res => {
    console.log(res);
})
// Async | Await
const res = await instance.get('/users');
console.log(res);

Source

// 以 adapter/xhr.js举例
function xhrAdapter (config) {
    return new Promise(function dispatchXhrRequest(resolve, reject) {
        let requestData = config.data;
        let request = new XMLHttpRequest();
        request.open(config.method.toUpperCase(), URL, true); // 请求尚未发送
        request.timeout = config.timeout;
        request.onreadystatechange = function handleLoad() {
            if (!request || request.readyState !== 4) {
                return;
            }
            const response = {
                data,
                status: request.status,
                statusText: request.statusText,
                headers,
                config,
                request
            };
            settle(resolve, reject, response);
            request = null;
        }
        if (!requestData) {
            requestData = null;
        }
        // send the request
        request.send(requestData);
    });
}
// Resolve or reject a Promise based on response status
function settle(resolve, reject, response) {
    const validateStatus = response.config.validateStatus;
    if (!response.status || !validateStatus || validateStatus(response.status)) {
        resolve(response);
    } else {
        reject(createError('...'));
    }
}

Adapter

Node.js 基于V8 JavaScript Engine,顶层对象是global,不存在window对象和浏览器宿主,因此无法使用XMLHttpRequest对象或者fetch API。对于需要同时适配浏览器和Node.js环境的请求库,就需要在Node.js 环境使用http, 在浏览器使用XMLHttpRequest/Fetch。因此针对不同环境的适配器就应运而生。具体代码lib/adapters

Usage

const res = await instance.get('/users', {
    adapter: function customAdapter(config) {
        return fetch(config.url);
    }
});

Source

function dispatchRequest (config) {
    const adapter = config.adapter || defaults.adapter; // 配置中可以自定义适配器,优先级 自定义适配器 > 默认适配器
    return adapter(config).then(function onAdapterResolution(response) {
        // ...
    }, function onAdapterRejection(reason) {
    	// ...
    })
}
const defaults = {
    adapter: getDefaultAdapter()
};
// 根据环境,使用不同适配器
function getDefaultAdapter () {
    let adapter;
    if (typeof XMLHttpRequest !== 'undefined') {
        // For browsers use XHR adapter
        adapter = require('./adapter/xhr');    
    } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    	// For node use HTTP adapter
        adapter = require('./adapter/http');
    }
    return adapter;
}

Transform request and response data

Usage

const res = await instance.get('/user', {
    transformRequest: [function (data, headers) {
        // Do whatever you want to transform the data
        return data;
    }],
    transformResponse: function (data, headers) {
        // Do whatever you want to transform the data
    	return JSON.parse(data);
    }
});

Source

function dispatchRequest(config) {
    // Transform request data
    config.data = transformData(config.data, config.headers, config.transformRequest); // 自定义请求转换函数
    return adapter(config).then(function onAdapterResolution(response) {
        // Transform response data
        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);
            }
        })
    })
}
/*
 * @param {Array|Function} fns:A single function or Array of functions to transform data by data and headers
 */
function transformData(data, headers, fns) {
    utils.forEach(fns, function transform(fn) {
        data = fn(data, headers);
    });
    return data;
}

Automatic transforms for JSON data

Source

const defaults = {
    transformResponse: [function transformResponse(data) {
        // 自动进行JSON字符串解析
        if (typeof data === 'string') {
            data = JSON.parse(data);
        }
        return data;
    }]
};

Interceptors

Usage

// 拦截器注册
// 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) {
    // Do something with response data
    return response;
  }, function (error) {
    // Do something with response error
    return Promise.reject(error);
});

Source

// 拦截器收集 [lib/core/InterceptorManager.js](https://github.com/axios/axios/blob/master/lib/core/InterceptorManager.js)
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
    this.handlers.push({
        fulfilled: fulfilled,
        rejected: rejected
    });
    return this.handlers.length - 1;
};
// 拦截器触发
Axios.prototype.request = function request(config) {
    const chain = [dispatchRequest, undefined];
    const 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);
    });    
    // chain = [requestFulfilled, requestRejected, dispatchRequest, undefined, responseFulfilled, responseRejected]
    while(chain.length) {
        promise = promise.then(chain.shift(), chain.shift());
    }
    return promise;
}
// 拦截器顺序:注册 => 收集 => 触发
// 触发顺序:请求拦截器 => 请求 => 响应拦截器

Cancel Requests

axios 通过在config中传入一个cancelToken, cancelToken.promise 实际为一个PENDING状态的promise实例,当用户调用cancel方法,会使该promise 实例由PENDING状态变为RESOLVE状态,触发监听函数onCanceled,调用request.abort(),取消xhr。

Usage

// 发送请求时携带
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
instance.get('/user/12345', {
  cancelToken: source.token // 只能叫cancelToken 名字不能改
});
// 取消请求
source.cancel();

Source

function CancelToken(executor) {
    let resolvePromise;
    this.promise = new Promise(resolve => {
        resolvePromise = resolve;
    });
    const token = this;
    executor(function cancel(message) {
    	if (token.reason) return;
        token.reason = new Cancel(message);
        resolvePromise(token.reason)
    });
}
/**
 * Returns an object that contains a new `CancelToken` and a function that, when called,
 * cancels the `CancelToken`.
 */
CancelToken.source = function source() {
  let cancel; // cancel的值就是上述function cancel方法,执行cancel方法的时候,执行resolvePromise方法
  const token = new CancelToken(function executor(c) {
      cancel = c;
  });
  return {
      token: token,
      cancel: cancel
  };
};
// xhr.js 或者 http.js
if (config.cancelToken) {
      // Handle cancellation
      config.cancelToken.promise.then(function onCanceled(cancel) {
          if (!request) {
              return;
          }
          request.abort();
          reject(cancel);
          // Clean up request
          request = null;
      });
}

XSRF

XSRF又称CSRF(Cross-site request forgery),是一种劫持受信任用户向服务器发送非预期请求的攻击方式。通常情况下,CSRF 攻击是攻击者借助受害者的 Cookie 骗取服务器的信任,可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击服务器,从而在并未授权的情况下执行在权限保护之下的操作。一般通过以下三种方式防范:

  • 验证码
  • Referer Check
  • Token axios库采取了在请求中传入token,防范XSRF攻击。
// axios 默认配置,具体使用时传入自定义xsrfCookieName、xsrfHeaderName
const defaults = {
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN'
};
// add XSRF header
if (utils.isStandardBrowserEnv()) {
    const xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ? cookies.read(config.xsrfCookieName) : undefined;
    if (xsrfValue) {
    	requestHeaders[config.xsrfHeaderName] = xsrfValue;
    }
}

参考