axios源码分析及设计思路

大家好我是zest,加入星球有一段时间了,之前说想分享一些自己写的源码分析,现在总算有时间写一写了。这是我最早的一篇草稿,比较粗糙,前几天看github上的axios源码早就添加了很多代码逻辑,比如fetch的适配器。但是大体设计没有太多的改变,也就懒得重头再改啦。

欢迎各位交流和指点!

先行知识

注意可能因为版本不同,导致源码的某些细节不一致,但是只要设计思路是一致的就不会有太割裂的改动。
另外在源码分析中只会聚焦于实现某一目的的相关代码,不会贴出全部代码。这样更加精简也便于理解。

XMLHttpRequest和Fetch

XMLHttpRequest是一个早期的浏览器 API,用于执行异步请求。

兼容性:XHR 在所有现代浏览器中都得到了支持,并且对于旧版浏览器的支持要比 fetch 好。
事件处理:XHR 提供了一个基于事件的接口,可以监听多个事件,如 progress、error、load 等。
状态信息:XHR 在请求过程中提供了更多的状态信息,例如可以通过 readyState 属性了解请求的当前状态。
复杂性:XHR 的 API 相对较老,使用起来比较繁琐,需要处理状态变化、事件监听等。
可读性:XHR 的代码往往不够直观,尤其是在处理错误和异步逻辑时。
JSON支持:XHR 不直接支持 JSON,需要手动将响应文本转换为 JSON。

Fetch 是一个现代的、基于 Promise 的 API,用于替代 XMLHttpRequest。

Promise 语法: fetch 使用 Promise,这使得异步代码更容易编写和理解。
语法简洁: 提供了一个更为简洁和直观的 API。
流式数据:支持流式数据,可以在数据到达时逐块处理,而不必等待全部数据。
默认异步:默认就是异步的,不需要额外的设置。
更好的 JSON 支持:直接将响应解析为 JSON。
兼容性:在旧版浏览器中可能不被支持(往往实际中的打包会做降级处理)。
默认不发送或接收 cookies: 除非明确设置。
没有timeout:需要手写代码

文件结构

    adapters/:适配器文件夹,用于存放适配器处理文件
    i. xhr.js

    core/:核心业务文件夹
    i. Axios.js
    ii. dispatchRequest.js
    iii. interceptorManager.js

    helper/:辅助用文件夹,存放工具函数

    axios.js:对外暴露的实例化入口,使用了工厂设计模式来创建实例

    defaults.js:Axios类的初始化配置内容

    utils.js:常用的工具函数

adapters/xhr.js 分析

对XMLHttpRequest进行封装,实现一个xhr的适配器文件。
返回一个promise,函数参数为config配置对象。
在这个返回的promise中进行XMLHttpRequest的请求处理。

export default function xhrAdapter(config) { let xhr_promise = new Promise(function dispatchXhrRequest(resolve, reject) { let request = new XMLHttpRequest(); // XMLHttpRequest的请求 request.open(config.method.toUpperCase(), config.url, true); request.send(config.data) let responseData = request.responseText; resolve(response); request = null } return xhr_promise }

构建一个axios的默认配置对象

包含适配器和其他请求内容,用于给axios实例化时使用。对应文件为./defaults.js

import utils from './utils' function getDefaultAdapter() { let adapter = require('./adapters/xhr').default; return adapter } let defaults = { adapter: getDefaultAdapter(), transformRequest: [], transformResponse: [ function transformResponse(data) { if (typeof data === 'string') { try {data = JSON.parse(data);} catch (error) {console.log(error)} } return data } ], timeout: 5000, xsrfCookieName: 'XSRF-TOKEN', //xsrfCookieName 表示存储 token 的 cookie 名称 xsrfHeaderName: 'X-XSRF-TOKEN', //xsrfHeaderName 表示请求 headers 中 token 对应的 header 名称 maxContentLength: -1, // 表示ASP支持的最大请求大小 maxBodyLength: -1, // 允许的http请求内容的最大字节数 validateStatus: function validateStatus(status) { return status >= 200 && status <= 299; }, headers: { 'common': { 'Accept': 'application/json, text/plain, */*' } } } export default defaults

构建核心的Axios类,对应 ./core/Axios.js

    Axios类有两个属性,分别是defaults配置内容和拦截器

    构建一个request方法,调用了适配器中的xmh请求,并返回一个promise

    构建Http请求方法(get, post),所有的方法都是对request方法的回调

import utils from "../utils"; import { buildURL } from '../helpers/buildURL' import { mergeConfig } from './mergeConfig' import { dispatchRequest } from './dispatchRequest' import InterceptorManager from './InterceptorManager.js' function Axios(instanceConfig) { this.defaults = instanceConfig; this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager(), }; } Axios.prototype.request = function request(config) { if (typeof config === 'string') { config = arguments[1] || {}; config.url = arguments[0]; } else { config = config || {}; } config = mergeConfig(this.defaults, config) if (config.method) { config.method = config.method.toLowerCase(); } else if (this.defaults.method) { config.method = this.defaults.method.toLowerCase(); } else { config.method = 'get' } let chain = [dispatchRequest, undefined]; let promise = Promise.resolve(config); promise = promise.then(chain.shift(), chain.shift()); return promise; } utils.forEach( ['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) { Axios.prototype[method] = function(url, config) { return this.request(mergeConfig( config || {}, {method: method, url: url} )) } } ) utils.forEach( ['post', 'put', 'patch'], function forEachMethodWithData(method) { Axios.prototype[method] = function(url, data, config) { return this.request(mergeConfig( config || {}, { method: method, url: url, data: data, } )) } } ) export default Axios

构建axios.js 对应文件 ./axios.js

    按工厂模式暴露实例

    createInstance函数中的bind和extend内容,保证实例能以函数的方式调用,也能以对象的方式调用

import utils from './utils' import defaults from './defaults.js'; import Axios from './core/Axios.js'; import bind from './helpers/bind.js'; import {mergeConfig} from './core/mergeConfig.js'; function createInstance(defaultConfig) { let context = new Axios(defaultConfig); // 为了让实例既能像对象一样调用 axios.get 也能像函数一样调用 axios(config) let instance = bind(Axios.prototype.request, context); utils.extend(instance, Axios.prototype, context); utils.extend(instance, context); return instance } let axios = createInstance(defaults); axios.create = function create(inputConfig) { return createInstance(mergeConfig(axios.defaults, inputConfig)) } export default axios;

这里说一些我自己对为什么需要让axios既能像对象一样调用 axios.get 也能像函数一样调用 axios(config)的理解。我刚开始看的时候是不太理解,保持使用方式一致不应该很标准很统一吗。但作为一个开源项目,肯定需要考虑方方面面,因为开发者不知道使用者的实际环境是什么样的,所以需要一个更高更全面的视角来确保项目的适用性这一思维在很多开源项目中都有体现

构建拦截管理器文件 对应 ./core/InterceptorManager.js

    有一个handlers属性,数组类型,用于存储所有的拦截器处理函数

    设置了use, eject, forEach 方法

import utils from '../utils.js'; function InterceptorManager() { this.handlers = []; } 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 } } InterceptorManager.prototype.forEach = function forEach(fn) { utils.forEach(this.handlers, function forEachHandler(h) { if (h !== null) { fn(h) } }) } export default InterceptorManager

在 ./core/Acios.js中的Axior.request方法中,实现拦截器promise

    初始化一个处理链,有两个节点,分别是当前请求的适配器promise和undefined

    遍历请求拦截器,将其属性handlers中的fulfilled和rejected内容放入处理链的头部

    遍历响应拦截器,将其属性handlers中的fulfilled和rejected内容放入处理链的尾部

    每一次向处理链中取出两个头部节点,分别用于promise.then中的resolve和reject

let chain = [dispatchRequest, undefined]; let promise = Promise.resolve(config); this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { chain.unshift(interceptor.fulfilled, interceptor.rejected) }) this.interceptors.response.forEach(function pushRequestInterceptors(interceptor) { chain.push(interceptor.fulfilled, interceptor.rejected) }) while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); } return promise;

以上的内容基本就是axios的核心内容。
axios是对XMLHttpRequest的一层包装,但是又开放了adapter接口预留了后续的开发余地。
axios的拦截器本质上是一个数组,每次取出数组中的两个元素作为promise.then的fulfilled和rejected。
axios项目的utils.js文件值得一看,里面的工具函数不仅仅适合它本身,也适合我们自己拿来在自己的项目中使用。

最后补充一下我当时开发的基于fetch的axios适配器

import axios from 'axios'; const fetchAdapter = (config) => { config.body = config.body ? JSON.stringify(config.body) : config.data return fetch(config.url, { method: config.method, body: config.body, }) .then(response => { return response }) .catch(error => { return Promise.reject(error) }) } const fetch_service = axios.create({ adapter: fetchAdapter, }); fetch_service.interceptors.response.use( response => { if (response.ok) { // 处理兼容 response.data = await response.json(); return response } else { return {} } }, error => { console.log(error); return Promise.reject(); } ); export default fetch_service

axios体量比较小,逻辑也比较简单,但是里面涉及的思想是和其他开源项目相同的。后续我会更新vuex的源码,也会体现和axios相同的开发思想。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
zest
作者分享
最近业务需要,计划招多名短期兼职,时长半个月左右,做数据标注任务。 要求理解标注规则,(修改一下)按数量付钱,多劳多得,但是必须在某个规定时间内完成,需要有充足的时间。最好有标注经验。 可远程,有需要的结束后可开实习证明。 感兴趣的加我微信详谈。
7
有没有学习 Python 的鱼友,有用 Python 开发鱼皮项目的经验。 boss 上看了一堆都没匹配上,在这里随缘发一发。 应届或者毕业一年以内,计算机基础扎实,刷过题,有项目经验。 base 深圳南山区,薪资 15k-20k。 除了后端,还能接触到前端和大模型的方向,对职业发展非常有帮助。 感兴趣并且符合要求的加我微信 15773283322
5
新年伊始,招聘一名全栈(偏前端)工程师
5
站在面试官的角度,分析如何更好地应对面试
15
我又来招人了~ 这次是招一名实习生,做 AI 大模型 agent 应用开发。 基于上次招人的情况,我这里做了一些规定和要求,希望有意向的小伙伴能认真看完下面的内容并考虑清楚后再来投递简历。 岗位名称:大模型应用开发工程师 地点:深圳南山科技园 时间:至少实习3个月,周末双休 年限要求:应届实习 岗位要求: 1. 计算机相关专业,最好是大四或研二; 2. 熟悉 Python 语言,具有扎实的数据结构和算法功底; 3. 对基于AI大模型的应用开发感兴趣; 4. 时间比较充裕,能到公司坐班; 5. 良好的沟通能力、学习能力、和团队协作精神; 工作内容: 1. 基于大模型的 agent 应用开发; 2. 基于场景对 Prompt 进行设计和测评,提升大模型效果和稳定性; 3. 根据公司业务需求,与开发团队、产品运营团队紧密配置,持续改进应用; 感兴趣的小伙伴可以添加我微信 15773283322 --------------------------------------------------------------- 随便说一下,上次发布的招聘,也成功地在编程导航上找到了满意的候选人,目前已经入职半个月多了。 上次招人收到了很多人的简历,但是就简历质量而言,真的是一言难尽。 这周末我找时间输出一篇站在面试官的角度看待招聘过程的感受,写一些对焦虑难找工作的人的一些建议。 这里提一句就是真的要好好准备简历啊!
9
下载 APP