【源码共读】学习 axios 源码整体架构 (Ⅰ)

452 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

【若川视野 x 源码共读】学习 axios 源码整体架构,打造属于自己的请求库 点击了解本期详情一起参与

今天阅读的是:axios

相信大家对这个库都比较熟悉,这是常用的发送请求的库

Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests。

我们一起来看一下它是怎么实现的

注意:本文先对axios入口文件进行粗略的分析,并没有涉及到Axios的类核心实现,核心代码在下一篇文章分析

文章可能存在理解上的误差,欢迎各位大佬勘误。

源码分析


首先,我们查看package.json

  • 入口文件index.js

image-20221205100418534

image-20221205102602644

可以看到,入口文件为./lib/axios.js,我们需要在vscode中进行调试,首先先设置配置文件

// .vscode/lauch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "program": "${workspaceFolder}/axios/sandbox/client.js",
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}
  • 打上断点,开始调试

image-20221205145448016

image-20221205145526892

后面步骤就不赘述,在注释中进行解释

入口文件

  • 创建实例
function createInstance(defaultConfig) {
    const context = new Axios(defaultConfig)

    // 通过bind函数,将context绑定到axios
    // axios = Axios.prototype.request
    const instance = bind(Axios.prototype.request, context)

    // Copy axios.prototype to instance
    // 将axios.prototype 中实现的方法挂载至 axios实例中
    // axios.get / axios.post ...
    utils.extend(instance, Axios.prototype, context, { allOwnKeys: true })

    // Copy context to instance
    // 复制context 到实例中
    // 如果是function,修改this指向
    // 如果不是function,直接赋值给instance
    // 这就解释了为什么axios.defaults,axios.interceptors可以直接使用
    utils.extend(instance, context, null, { allOwnKeys: true })

    // Factory for creating new instances
    // 工厂模式
    // 只会返回一个axios对象,并且对输入的config与默认的合并
    instance.create = function create(instanceConfig) {
        return createInstance(mergeConfig(defaultConfig, instanceConfig))
    }

    // 返回实例
    return instance
}
  • 暴露出相关的接口
// 创建导出的实例
const axios = createInstance(defaults)

// Expose Axios class to allow class inheritance
// 暴露 Axios 对象
// 允许类继承
axios.Axios = Axios

// Expose Cancel & CancelToken
// 暴露取消请求的接口
axios.CanceledError = CanceledError
axios.CancelToken = CancelToken
axios.isCancel = isCancel
axios.VERSION = VERSION
axios.toFormData = toFormData

// Expose AxiosError class
axios.AxiosError = AxiosError

// alias for CanceledError for backward compatibility
axios.Cancel = axios.CanceledError

// Expose all/spread
// axios.all 即 使用Promise.all 包裹返回
axios.all = function all(promises) {
    return Promise.all(promises)
}

axios.spread = spread

// Expose isAxiosError
axios.isAxiosError = isAxiosError

// Expose mergeConfig
axios.mergeConfig = mergeConfig

axios.AxiosHeaders = AxiosHeaders

axios.formToJSON = (thing) =>
formDataToJSON(utils.isHTMLForm(thing) ? new FormData(thing) : thing)

axios.default = axios

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

相关的工具函数

bind

export default function bind(fn, thisArg) {
    return function wrap() {
        // 传递参数到fn中
        return fn.apply(thisArg, arguments);
    };
}
  • 疑问:为什么需要包裹一层warp函数?

    create方法意味着返回Axios工厂,而不是作为一个类 AxiosInstance 的实例去调用
    https://github.com/axios/axios/issues/4365
    

extend


/**
 * 将b对象中的参数拓展至a对象中
 *
 * @参数 {Object} a 需要拓展的对象
 * @参数 {Object} b 需要复制的对象
 * @参数 {Object} thisArg 需要绑定的函数对象
 *
 * @参数 {Boolean} [allOwnKeys] 只赋值指定的参数
 * @返回值 {Object} 返回拓展后的a
 */
const extend = (a, b, thisArg, {allOwnKeys}= {}) => {
  forEach(b, (val, key) => {
    if (thisArg && isFunction(val)) {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  }, {allOwnKeys});
  return a;
}

forEach

function forEach(obj, fn, { allOwnKeys = false } = {}) {
    // Don't bother if no value provided
    // obj没有值则返回
    if (obj === null || typeof obj === 'undefined') {
        return
    }

    let i
    let l

    // Force an array if not already something iterable
    // 不是对象,使用数组包裹
    if (typeof obj !== 'object') {
        /*eslint no-param-reassign:0*/
        obj = [obj]
    }

    if (isArray(obj)) {
        // 如果是数组,则循环调用fn函数
        // Iterate over array values
        for (i = 0, l = obj.length; i < l; i++) {
            fn.call(null, obj[i], i, obj)
        }
    } else {
        // Iterate over object keys
        // allOwnKeys有值:返回Obj自身所有属性
        // allOwnKeys不存在或为空:过滤原型链上可遍历的属性
        const keys = allOwnKeys ? Object.getOwnPropertyNames(obj) : Object.keys(obj)
        const len = keys.length
        let key

        for (i = 0; i < len; i++) {
            key = keys[i]
            fn.call(null, obj[key], key, obj)
        }
    }
}

spread

/****************************
    假如需要实现这样的效果
    function f(x,y,z){}
    const args = [1,2,3]
    f.apply(null,args)
*****************************/
export default function spread(callback) {
    return function wrap(arr) {
        return callback.apply(null, arr);
    };
}

小结


通过对axios的入口文件分析,我们可以知道:

  • 为什么axios.defaults,axios.interceptors可以直接使用
  • 工厂模式
  • 对象拓展
  • 为什么axios即使对象,也是函数

在下一篇文章,我们将对Axios类进行探讨和分析。