搞懂 axios 核心原理

827 阅读11分钟

从入口开始

查看 axios 源码,找到 package.json main 属性值 index.js,也就是说入口文件是根目录下的 index.js

分析入口文件可以看到,引入了 ./lib/axios 模块,接着导出成员 axios 作为默认导出成员:

import axios from './lib/axios.js';

export {
  axios as default,
  ...
}

也就是说,我们平时引入使用的 import axios from 'axios' 就是 ./lib/axios 模块里导出的成员。

所以,分析 axios 核心源码,我们便可以从 ./lib/axios 入手。

axios 实例

大概过一遍该文件,不用细究细节,可以了解到一个大概的代码情况,一般我会先看最后导出的成员,然后接着往上看。

该模块导出了一个默认成员 axios

export default axios;

往上找,axios 是什么呢?分析代码发现其实是一个创建的实例,然后在该实例上绑定了很多的属性、方法

// Create the default instance to be exported
const axios = createInstance(defaults);

// Expose Axios class to allow class inheritance
axios.Axios = Axios;

// Expose all/spread
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.default = axios; // 这个应该在项目中经常看到,做一些默认配置用
...

// this module should only have a default export
export default axios

顺着找, createInstance 这里,这个函数是专门用来新建一个 axios 实例的。

createInstance

// ./lib/axios.js
function createInstance(defaultConfig) {
  // 
  const context = new Axios(defaultConfig);
  const instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});

  // Copy context to instance
  utils.extend(instance, context, null, {allOwnKeys: true});

  // Factory for creating new instances
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };

  return instance;
}

// ./lib/helpers/bind.js
export default function bind(fn, thisArg) {
  return function wrap() {
    return fn.apply(thisArg, arguments);
  };
}

在这里做了几件事:

  • 基于 Axios 类新建了一个 context 实例
  • 把 context 绑定给 Axios 原型的 request 方法,这样 request 调用的时候,里面的 this 才会指向这个内部新建的 context 实例,这样即使是创建多个 axios 实例,它们互相之间是隔离不干扰的
  • bind 函数在这里的作用就是修改 Axios.prototype.requestthis 指向。instance 可以理解为 Axios.prototype.request,这里虽然有点绕,但还是很好理解,instance 是一个 wrap包装函数,只要 instance 被调用,也就是里面的包装函数 wrap()执行,也就是 Axios.prototype.request 被立即调用
  • 换句话说,instance 指的是 Axios.prototype.request(不正确但好理解)
import axios from 'axios'

// 这里我们平时是这样调用 axios 的,
// 而实际上,是调用了内部的 Axios.prototype.request 方法
axios({
    url: '/api',
    method: 'get'
})

// 平时我们使用的创建 axios 实例的用法:
// 其实是因为源码内部 instance.create 方法里,内部调用的也是 createInstance
// 也就是说,官方导出的默认 axios 实例,和我们自己使用 axios.create 新创建的 axiosInstance 本质上是一样的创建逻辑
const axiosInstance = axios.create({})
  • Axios 的原型拷贝到 instance,为什么?下方解释
  • context 拷贝到 instance,为什么?下方解释

Axios 类

Axios 的原型拷贝到 instance

作用是让 instance 共享 Axios 原型对象上的属性、方法,比如使用 axios.get、axios.post 等 API 发送请求

原因是 我们在使用 axios.get(url)axios.post(url) 等请求时,在 instance 上面是没有绑定这些方法的,所以需要手动绑定这些方法到 instance。

在源码中,这部分代码是在 Axios.js 模块实现的:

'use strict';

import utils from './../utils.js';

const validators = validator.validators;

class Axios {
  constructor(instanceConfig) {
  }

  async request(configOrUrl, config) {
  }
  
}

// Provide aliases for supported request methods
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config || {}, {
      method,
      url,
      data: (config || {}).data
    }));
  };
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  /*eslint func-names:0*/

  function generateHTTPMethod(isForm) {
    return function httpMethod(url, data, config) {
      return this.request(mergeConfig(config || {}, {
        method,
        headers: isForm ? {
          'Content-Type': 'multipart/form-data'
        } : {},
        url,
        data
      }));
    };
  }

  Axios.prototype[method] = generateHTTPMethod();

  Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
});

export default Axios;

context 拷贝到 instance

作用是让 instance 共享创建的 Axios 实例即 context 的属性、方法,比如 inteceptors、request方法

根据 axios 的官方 API,我们可以看到这些常见的 API:

import axios from 'axios'

axios.request(url)
axios.interceptors.request.use()
axios.interceptors.response.use()

这些方法都是 Axios 类自身需要实现的,request 就是 Axios 实例 context 的方法,而 interceptors 拦截器的实现则是分离出去抽象实现了一个 InterceptorManager 类,封装了处理拦截器相关的操作。拦截器又分为请求拦截器、响应拦截器。

// ./lib/core/Axios.js
class Axios {
    constructor(defaultConfig) {
        this.defaults = defaultConfig;
        this.interceptors = {
            request: new InterceptorManager(),
            response: new InterceptorManager(),
        }
    }
    
    async request(configOrUrl, config) {
        await this._request(configOrUrl, config);
    }
    
    // 重要的实现在这里
    _request(configOrUrl, config) {
        // TODO
    }
}

// ./lib/core/InterceptorManager.js
class InterceptorManager {
    constructor() {
        this.handlers = [];
    }
    
    use(fulfilled, rejected) {
        this.handlers.push({
            fulfilled,
            rejected,
        })
    }
    
    forEach(fn) {
        utils.forEach(this.handlers, function forEachHandler(h) {
          if (h !== null) {
            fn(h);
          }
        });
    }
}

拦截器的实现思路

  _request(configOrUrl, config) {
    // 处理配置信息
    if (typeof configOrUrl === 'string') {
      config = config || {};
      config.url = configOrUrl;
    } else {
      config = configOrUrl || {};
    }
    // 合并配置
    config = mergeConfig(this.defaults, config);
    
    ...

    // 存放请求拦截器
    const requestInterceptorChain = [];
    let synchronousRequestInterceptors = true; // 判断是否同步执行拦截器(主要控制请求拦截器)
    // forEach 是 axios 自身实现的,遍历拦截器
    this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {// interceptor 实则是对应到 InterceptorManager 的一个 handler
       
      // 只有在所有的拦截器的 synchronus 为 true,才是同步(默认为false,即异步)
      synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
    
      // 后添加的先执行,fulfilled 和 rejected 成对添加
      requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
    });
    
    // 存放响应拦截器
    const responseInterceptorChain = [];
    this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
      // 响应拦截器按书写的先后顺序执行,fulfilled 和 rejected 成对添加
      responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
    });

    let promise;
    let i = 0;
    let len;

    // 异步执行请求拦截器
    if (!synchronousRequestInterceptors) {
      // dispatchRequest 内部封装了真正的发起请求功能
      const chain = [dispatchRequest.bind(this), undefined]; // 默认的调用链,undefined 占位
      chain.unshift.apply(chain, requestInterceptorChain);// 请求拦截器放到请求前
      chain.push.apply(chain, responseInterceptorChain); // 响应拦截器放到请求完成后
      len = chain.length;

      promise = Promise.resolve(config);

      while (i < len) {
        // 在这里体现了异步的作用:
        // 出现错误时,异步模式的错误处理类似分支,错误捕获的是之前节点最近的一次错误
        promise = promise.then(chain[i++], chain[i++]);// 链式调用
      }

      return promise;
    }
    
    len = requestInterceptorChain.length;
    let newConfig = config;
    i = 0;

    // 同步执行请求拦截器
    while (i < len) {
      // 把请求拦截器中的 fulfilled、reject取出
      const onFulfilled = requestInterceptorChain[i++];
      const onRejected = requestInterceptorChain[i++];
      try {
        newConfig = onFulfilled(newConfig); // 执行
      } catch (error) {
        // 捕获到错误,执行错误回调,且 break 终止下一个请求拦截器
        // 与异步的区别:同步模式的错误处理针对与当前执行函数
        onRejected.call(this, error);
        break;
      }
    }

    try {
      promise = dispatchRequest.call(this, newConfig); // 发起请求
    } catch (error) {
      return Promise.reject(error);
    }

    i = 0;
    len = responseInterceptorChain.length;

    // 请求完成后,处理响应拦截器
    while (i < len) {
      promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]);
    }

    return promise;
  }

从拦截器源码中,可以总结以下几点重要的点:

  1. 请求拦截器分为同步执行和异步执行
  2. 只有所有的请求拦截器配置了synchronus=true才是同步,不然异步
  3. 同步、异步的区别在于错误捕获的处理,异步捕获的错误是之前节点最近的一次错误;同步捕获的错误是当前请求拦截器的错误,www.jianshu.com/p/710636bd7…
  4. 在发出请求前,执行请求拦截器,需要在请求拦截器执行函数最后返回 config,且后添加的先执行;请求完成后,把 response 响应结果返回传给响应拦截器,执行响应拦截器函数,也需要把 response 传给下一个响应拦截器

发送 XHR 请求

在上面我们看到有一个 dispatchRequest 函数,这个方法就是用来发起请求的,内部的实现兼容了浏览器端的 xhr 和 node 端的 http。

// lib/core/dispatchRequest.js
export default function dispatchRequest(config) {
  ...
  
  // 在这里调用了getAdapter
  const adapter = adapters.getAdapter(config.adapter || defaults.adapter);

  return adapter(config).then(function onAdapterResolution(response) {
    ...

    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      ...
    }

    return Promise.reject(reason);
  });
}

getAdapter

// ./lib/adapters/adapters.js
import httpAdapter from './http.js';
import xhrAdapter from './xhr.js';

const knownAdapters = {
  http: httpAdapter, // 用于 node 端发送 http 请求
  xhr: xhrAdapter // 用于浏览器端发送 XMLHttpRequest 请求
}
const isResolvedHandle = (adapter) => utils.isFunction(adapter) || adapter === null || adapter === false;

export default {
  getAdapter: (adapters) => {
    adapters = utils.isArray(adapters) ? adapters : [adapters];

    const {length} = adapters;
    let nameOrAdapter;
    let adapter;

    const rejectedReasons = {};

    for (let i = 0; i < length; i++) {
      nameOrAdapter = adapters[i];
      let id;

      adapter = nameOrAdapter;

      if (!isResolvedHandle(nameOrAdapter)) {
        // 在这里根据 knownAdapters 映射得出对应的 adapter
        adapter = knownAdapters[(id = String(nameOrAdapter)).toLowerCase()];
      }
    }
    return adapter;
  },
  adapters: knownAdapters
}

发送 xhr 请求

// ./lib/adapters/xhr.js
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';

export default isXHRAdapterSupported && function (config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {

    let request = new XMLHttpRequest();

    const fullPath = buildFullPath(config.baseURL, config.url);

    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

    // Set the request timeout in MS
    request.timeout = config.timeout;

    function onloadend() {
      if (!request) {
        return;
      }

      const responseData = !responseType || responseType === 'text' || responseType === 'json' ?
        request.responseText : request.response;
      const response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config,
        request
      };

      settle(function _resolve(value) {
        resolve(value);
        done();
      }, function _reject(err) {
        reject(err);
        done();
      }, response);

      // Clean up request
      request = null;
    }

    if ('onloadend' in request) {
      // Use onloadend if available
      request.onloadend = onloadend;
    } else {
      // Listen for ready state to emulate onloadend
      request.onreadystatechange = function handleLoad() {
        if (!request || request.readyState !== 4) {
          return;
        }

        if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
          return;
        }
        setTimeout(onloadend);
      };
    }
    
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }
      
      ...

      // Clean up request
      request = null;
    };

    // Handle low level network errors
    request.onerror = function handleError() {
      ...
      // Clean up request
      request = null;
    };

    // Handle timeout
    request.ontimeout = function handleTimeout() {
      ...
      // Clean up request
      request = null;
    };

    // Add responseType to request if needed
    if (responseType && responseType !== 'json') {
      request.responseType = config.responseType;
    }
  
    // Send the request
    request.send(requestData || null);
  });
}

虽然源码很繁琐,但是实现一个 XMLHttpRequest 请求必须的三个是 o o sopen onload send,以下是使用 Promise 实现 XHR 请求的简单例子:

export default isXHRAdapterSupported && function (config) {
    return new Promise((resolve) => {
        const { url, method, data = {} } = config;
        
        const xhr = new XMLHttpRequest();
        
        xhr.open(url, method, true);
        
        xhr.onload = function() {
            resolve(xhr.responseText);
        }
        
        xhr.send(data)
    })
}

项目中二次封装 axios

在实际项目中,我们往往需要二次封装一下 axios 再使用,这样的好处是:

  1. 能够统一设置请求头、请求超时时间、根据项目环境判断使用哪个请求地址、错误处理等
  2. 封装统一的 API 方便业务调用,从而提高代码质量
  3. 可以在请求拦截设置一些 token,在响应拦截器中判断响应码做一些特定页面跳转

如何封装

在 axios 库 API 中,发现 get、delete、headpost、put、patch 请求的入参不一样,而想二次封装的请求入参实现一致,怎么做呢?

设想实现 get、post、delete、patch、put、head 方法的入参是一致的,那么如何做到兼容呢?

import axios from '@/utils/axios'

// 例如想保持入参一致,url 是请求路由,params 就是请求参数,config 是请求配置
axios.get(url, params?, config?)
axios.post(url, params?, config?)
axios.delete(url, params?, config?)

我想到了一种实现方式,就是参考源码中的在 Axios 类的原型上扩展 Axios.prototype[method] 的方式,自定义实现,在处理 参数是,修改 config 后再传给 axiosInstance 实现。

封装请求方法

// src/utils/axios.ts
import axios, { AxiosInstance } from 'axios';

class Ajax {
  axiosInstance: AxiosInstance;
  [x: string]: any;

  constructor() {
    // Ajax 内部维护一个 axiosInstance
    this.axiosInstance = axios.create({
      baseURL: '/api',
      timeout: 60000,
    })

    // 请求拦截器
    this.axiosInstance.interceptors.request.use(
      (config) => {
        const token = '';
        config.header.authorization = token; // 统一为每个请求带上 token
        return config;
      },
      (error) => {
        return Promise.reject(error);
      }
    );

    // 响应拦截器
    this.axiosInstance.interceptors.response.use(
      (response) => {
        const { status, data } = response;
        
        // 接口请求失败
        if(status !== 200) {
            return Promise.reject(response);
        }
        
        // 未登录
        if(data.code === 2001) {
            // 跳转登录页
        
        // 未授权
        } else if(data.code === 2002) {
            // 未授权相关操作
        } else {
            return Promise.resolve(response);
        }
        return response;
      },
      (error) => {
        return Promise.reject(error);
      }
    )
  }
}

const methods = ['get', 'post', 'patch', 'put', 'delete', 'head'];

// 在 Ajax 原型上扩展请求方法,外部调用
methods.forEach((method) => {
  Ajax.prototype[method] = async function(url: string, params?: any, config?: any) {
    if(
      method === 'get' ||
      method === 'head' ||
      method === 'delete' ||
      method === 'options'
    ) {
      config = {
        params,
        ...config
      }
    } else {
      config = {
        data: params,
        method,
        ...config,
      }
    }

    return new Promise((resolve, reject) => {
      this.axiosInstance(url, config).then(
        (data) => {
          resolve(data);
        },
        (error) => {
          reject(error)
        }
      )
    })
  }
})

// 向外暴露的是一个 Ajax 单例
export default new Ajax();

本地服务器代理转发实现跨域

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    }
  },
  // 解决跨域,方便本地调试,一般和后端本地联调存在跨域,上线后是同域,
  // 所以本地的话不需要后端做cors处理,而是直接前端本地代理到目标服务器
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
})

api 层

// src/api/index.ts
export * as userApi from './user'
// src/api/user.ts
import axios from '@/utils/axios'

async function getUserInfo(params: any) {
  const data = await axios.get('/users/data', params)

  return data
}

async function postUserInfo(params: any) {
  const data = await axios.post('/users/data', params)

  return data
}

async function deleteUserInfo(params: any) {
  
  const data = await axios.delete('/users/data', params)

  return data
}


async function putUserInfo(params: any) {
  const data = await axios.put('/users/data', params)

  return data
}


async function patchUserInfo(params: any) {
  const data = await axios.patch('/users/data', params)

  return data
}

export {
  getUserInfo,
  postUserInfo,
  deleteUserInfo,
  putUserInfo,
  patchUserInfo,
}

page 业务层

<template>
  Hello World
  <div>{{ count }}</div>
  <button @click="handleClick">click</button>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { userApi } from '@/api';
const count = ref(1)

const handleClick = async() => {
  count.value++
  const data = await userApi.getUserInfo({
    age: 21
  });
}
</script>

node 搭建服务端

var express = require('express');
var router = express.Router();

router.get('/data', function(req, res, next) {
  res.send({
    name: 'jjy',
    age: 27,
    reqQuery: req.query,
  })
});

router.delete('/data', function(req, res, next) {
  res.send({
    name: 'jjy',
    age: 27,
    reqQuery: req.query,
    method: 'delete'
  })
});


router.post('/data', function(req, res, next) {
  console.log(req.body);
  res.send({
    name: 'jjy',
    age: 27,
    postParams: req.body.username,
    method: 'post'
  })
});


router.put('/data', function(req, res, next) {
  console.log(req.body);
  res.send({
    name: 'jjy',
    age: 27,
    postParams: req.body.username,
    method: 'put'
  })
});


router.patch('/data', function(req, res, next) {
  console.log(req.body);
  res.send({
    name: 'jjy',
    age: 27,
    postParams: req.body.username,
    method: 'patch'
  })
});

module.exports = router;

nodemon 启动本地服务器,只要服务端有修改,自动重启,不用每次手动重启那么麻烦

// package.json
"start": "nodemon ./bin/www"

服务器启动后,也启动前端项目,点击按钮发器请求,能响应成功便实现了跨域。

image.png

image.png

image.png

Ajax、axios、fetch 的区别

  1. Ajax 是传统意义上的基于 XMLHttpRequest 实现请求,是最早的向后端发送请求的技术;Ajax,即异步的 Javascript 和 XML,其核心是通过JavaScript的XMLHttpRequest对象,以一种异步的方式,向服务器发送数据请求,并且通过该对象接收请求返回的数据,从而实现客户端与服务器端的数据操作。

  2. 后来 JQuery 基于 XMLHttpReuqest 封装了自己的一套请求

    • 能实现JSONP跨域
    • JQuery 是一种 MVC 模式框架,JQuery ajax 基于 MVC 编程,与目前主流的 MVVM 模式框架存在不一致的理念
    • 在当前主流的 MVVM 框架中,没必要引进 JQuery ajax,一个是 JQuery 本身很庞大,一个是有更好的 axios 请求库可以使用
  3. axios 是一个基于 Javascript 实现的网络请求库

    • 支持浏览器端发送 xhr 请求、node 端发送 http 请求
    • 支持 Promise API
    • 提供并发请求接口
    • 提供请求拦截、响应拦截
    • 可以设置超时、可以终止请求
    • 客户端支持防止 XSRF,即让你的每个请求都带一个从cookie中拿到的key,根据同源策略,假冒网站拿不到 cookie 的 key,从而让后台辨别这个用户是否假冒
    • 可以创建多个 axios 实例,实现不同 URL 的请求配置
  4. fetch 是一个基于 ES 新规出现的一个原生 API,跟 XMLHttpRequest 对象无关

    • 语义简单
    • 基于标准 Promise 实现,支持 async/await
    • 更加底层,原生 JS API,脱离 XHR,提供丰富 API
    • 只对网络请求报错,会把 4XX、5XX 状态码看作成功请求
    • 默认不能携带 cookie,需要手动设置 fetch(url, {credentials: 'include'})
    • 不支持 abort、不支持超时控制

参考

  1. www.jianshu.com/p/710636bd7…
  2. juejin.cn/post/698125…