梅开二度的 axios 源码阅读,三千字详细分享功能函数,帮助扩展开发思维

本文深入剖析 axios 源码中的功能函数,包括 standardBrowserEnv 中的 cookie 操作与 formDataToJSON 实现。通过详细解读函数内部逻辑,如 cookie 的读写删、日期转换、正则匹配以及 formData 与对象的转换,旨在拓展开发者对 axios 的理解及提升代码设计能力。
摘要由CSDN通过智能技术生成

前言

第一遍看 axios 源码,更多的是带着日常开发的习惯,时不时产生出点联想。

第二遍再看 axios 源码,目标明确,就是奔着函数来的。

当有了明确清晰的目标,阅读速度上来了,思绪也转的飞快

按图索骥,接下来,将和大家一起,找寻 axios 源码中的功能函数,扩展一下开发思路。

文章速读

阅读文章,可以有以下收获:

功能函数

一般基础的工具函数都会放到 utils.js 文件中。前一篇源码阅读中,介绍了关于类型判断的两种方法。

axios 中有许多模块,不同的模块下包含不同的功能。除了 utils.js 文件,这些模块下的文件也包含函数。我称之为功能函数。

功能函数,按照实际功能需要封装的函数,也许并不是常见功能需要的,但是了解之后,没准能帮助今后用到的时候,快速完成开发。

1.standardBrowserEnv 标准浏览器中对 cookie 的读写删操作

axios 拥有对 cookie 操作的能力,就像叶一一会讲故事一样理所当然。

看源码功能之前,我们先来复习关于 cookie 的知识点。

cookie 的格式

通过 document.cookie 获取 cookie 的值,是一串有特定格式的字符串。它的格式是

cookie1=value; cookie2=value; cookie3=value;

了解了 cookie 的格式,对于它的操作,也自然而然的可以接着看代码了。

cookie 的读写删

在标准浏览器中,对 cookie 的处理被封装在了一个立即执行函数里。

(function standardBrowserEnv() {return {
		/** * cookie 写入(创建)单个写入 * @param {string} name cookie 名 * @param {string} value cookie 值 * @param {*} expires 过期时间 * @param {*} path cookie所在的目录 * @param {*} domain cookie所在的域 * @param {*} secure 是否可以通过HTTP协议的URL设置布尔值 值为 true 时,表示创建的 cookie 只能用 HTTPS 协议发送 */ write: function write(name, value, expires, path, domain, secure) {	// 先将所有的数据存入数组const cookie = [];cookie.push(name + '=' + encodeURIComponent(value));

			// 如果过期时间有值且是数值型,则存入其转为根据格林威治时间 (GMT) 转成的字符串if (utils.isNumber(expires)) {cookie.push('expires=' + new Date(expires).toGMTString());}	// 当目录存在且为字符串,则存入cookieif (utils.isString(path)) {cookie.push('path=' + path);}

			// 当域存在且为字符串,则存入cookieif (utils.isString(domain)) {cookie.push('domain=' + domain);}

			// 是否可以通过HTTP协议的URL设置布尔值为 true 时,则存入cookieif (secure === true) {cookie.push('secure');}	// 将数组转为字符串,并通过 document.cookie 属性来创建 cookiedocument.cookie = cookie.join('; ');},

		/** * cookie 读取 单个读取 * @param {string} name cookie 名 */read: function read(name) {	// 正则匹配得到数组const match = document.cookie.match(new RegExp('(^|;\s*)(' + name + ')=([^;]*)'));	// match 匹配的返回值中第四个元素为需要的值return match ? decodeURIComponent(match[3]) : null;},	/** * cookie 删除 单个删除 * @param {string} name cookie 名 */remove: function remove(name) {
			// 将过期时间设置为过去的时间,即可删除 cookie, 这里设置成了前一天(86400000为1天的毫秒数)this.write(name, '', Date.now() - 86400000);},};
})(); 

使用

// 写入
cookies.write('foo', 'bar');
// 删除
cookies.remove('foo');
// 读取
cookies.read('username=John Doe') // => John Doe 

功能汇总

日期转换 toGMTString 方法

toGMTString() 方法可根据格林尼治标准时间 (GMT) 把 Date 对象转换为字符串,并返回结果。

介绍这个方法,主要科普一个新旧交替的世界标准时间。

首先这个 GMT 和 UTC 的定义如下:

GMT(Greenwich Mean Time):格林尼治标准时间。格林尼治是英国伦敦南郊原皇家格林尼治天文台所在地,地球本初子午线的标界处,世界计算时间和经度的起点。格林尼治标准时间过去被当成世界标准的时间。

UTC(Coordinated Universal Time):协调世界时。又称世界统一时间、世界标准时间、国际协调时间。协调世界时是以原子时秒长为基础,在时刻上尽量接近于世界时的一种时间计量系统。协调世界时是现在使用的世界标准时间。

所以在W3C下面有一行提示:

不赞成使用此方法。请使用 toUTCString() 取而代之!!

toUTCString 方法

根据协调世界时 (UTC) 把 Date 对象转换为字符串,并返回结果。

正则 match 方法

match() 方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。

返回值

它的返回值是一个存放匹配结果的数组。该数组的内容依赖于 regexp 是否具有全局标志 g。

  • 如果 regexp 没有标志 g,那么 match() 方法就只能在 stringObject 中执行一次匹配。

    • 如果找到匹配结果,它将返回一个数组,存放了与它找到的匹配文本有关的信息。数组的第1个元素存放的是匹配的文本,其余的元素存放与正则表达式的子表达式匹配的文本和input、index、groups三个元素。* 如果没找到匹配结果,返回 null。
  • 如果 regexp 具有标志 g,则 match() 方法将执行全局检索,找到 stringObject 中的所有匹配子字符串。

    • 如果找到匹配结果,它将返回一个数组,数组的第1个元素存放的是匹配的文本,其余元素存放匹配的一个或多个文本。* 如果没找到匹配结果,返回 null。

小结

1.对 cookie 的读写删操作还是挺简单的,主要是参数要考虑全面。日常开发中使用频率不是很高,但是如果需求涉及到可以帮助快速完成开发。
2.在 standardBrowserEnv 函数中,不难在里面发现 utils 的身影,所以基础工具函数的建设必不可少。
3.把 Date 对象转换为字符串推荐使用 toUTCString 方法。
4.无法删除的属性,可以设置成无效的值,比如这里的通过过期时间提前于当前时间。再比如前一篇介绍过将属性值设置为 undefined。

2.formDataToJSON 抽丝剥茧 formData 与 Object 的转换

FormData 对象

FormData 对象用以将数据编译成键值对,以便用XMLHttpRequest来发送数据。

FormData 对象主要用于发送表单数据,但亦可用于发送带键数据 (keyed data),而独立于表单使用。一般文件流数据的发送,会用到 FormData 对象。

第一条丝线— parsePropPath

matchAll

matchAll 函数会根据入参的正则表达式和字符串,返回所有匹配项的数组。

/**
 * It takes a regular expression and a string, and returns an array of all the matches
 *
 * @param {string} regExp - The regular expression to match against.
 * @param {string} str - The string to search.
 *
 * @returns {Array<boolean>}
 */
const matchAll = (regExp, str) => {let matches;const arr = [];

	// matches 为每次匹配正确的值while ((matches = regExp.exec(str)) !== null) {arr.push(matches);}return arr;
} 
regExp.exec 方法

在一个指定字符串中执行一个搜索匹配。返回一个结果数组或 null。

匹配的标准用法就是如上功能中的那样,如果需要进一步了解可以阅读 MDN 文档

parsePropPath

parsePropPath ****函数会将字符串中的字母匹配出来放到数组中。

但看描述和函数还不太确定具体用法,但是经典的来了。

当我们设计的某个函数不好用文字做描述,可以用举例的方式辅助描述。比如下面这个函数,不但举例,还举了多个例子,所以这个函数的用途一目了然。

/**
 * It takes a string like `foo[x][y][z]` and returns an array like `['foo', 'x', 'y', 'z']
 *
 * @param {string} name - The name of the property to get.
 *
 * @returns An array of strings.
 */
function parsePropPath(name) {// foo[x][y][z]// foo.x.y.z// foo-x-y-z// foo x y zreturn utils.matchAll(/\w+|[(\w*)]/g, name).map(match => {	// 匹配值为数组,当匹配的第一个元素为空数组时返回空字符串,否则返回第二个元素或第一个元素存在值的那个。return match[0] === '[]' ? '' : match[1] || match[0];});
} 

第二条丝线— arrayToObject

arrayToObject 函数会将数组转换对象。

/**
 * Convert an array to an object.
 *
 * @param {Array<any>} arr - The array to convert to an object.
 *
 * @returns An object with the same keys and values as the array.
 */
function arrayToObject(arr) {const obj = {};
	// 返回数组索引值的数组const keys = Object.keys(arr);let i;const len = keys.length;let key;for (i = 0; i < len; i++) {key = keys[i];obj[key] = arr[key];}return obj;
} 

第三条丝线— forEachEntry

forEachEntry 函数会循环一个可迭代的对象,直到循环结束,把对象的 key 和 value 返回。

/**
 * For each entry in the object, call the function with the key and value.
 *
 * @param {Object<any, any>} obj - The object to iterate over.
 * @param {Function} fn - The function to call for each entry.
 *
 * @returns {void}
 */
const forEachEntry = (obj, fn) => {const generator = obj && obj[Symbol.iterator];const iterator = generator.call(obj);let result;// 循环直到迭代器已将序列迭代完毕while ((result = iterator.next()) && !result.done) {const pair = result.value;fn.call(obj, pair[0], pair[1]);}
}; 

我找了一个可迭代对象测试了一下输出

const data = new Map();

data.set('a', 1);
data.set('b', 2);
data.set('c', 3);

forEachEntry(data, (name, value) => {let res = name + '-' + value;console.log('name-value:', res);
});

// 输出结果
// > name-value: a-1
// > name-value: b-2
// > name-value: c-3 

formDataToJSON

formDataToJSON函数接受 一个 FormData 对象最终返回 JavaScript 对象。

/**
 * It takes a FormData object and returns a JavaScript object
 *
 * @param {string} formData The FormData object to convert to JSON.
 *
 * @returns {Object<string, any> | null} The converted object.
 */
function formDataToJSON(formData) {
	/** * 递归函数 将 FormData 的键值全部插入到给 target 对象 * @param {Array} path FormData 的属性名数组 * @param {string} value FormData 的属性值 * @param {Object} target 最终的目标对象 * @param {number} index FormData 的属性值数组的索引值 * @returns 递归是否中止的布尔值 */function buildPath(path, value, target, index) {let name = path[index++];
		// isFinite 函数会先将测试值转换为数字,然后再对其进行是否为有限数检测。const isNumericKey = Number.isFinite(+name);const isLast = index >= path.length;name = !name && utils.isArray(target) ? target.length : name;if (isLast) {
			// 如果检查对象具有该属性,将 value 添加到对应的数组中if (utils.hasOwnProp(target, name)) {target[name] = [target[name], value];} else {target[name] = value;}return !isNumericKey;}if (!target[name] || !utils.isObject(target[name])) {target[name] = [];}const result = buildPath(path, value, target[name], index);if (result && utils.isArray(target[name])) {target[name] = arrayToObject(target[name]);}return !isNumericKey;}

	// formData 参数是 FormData 类型且 formData.entries 是一个函数if (utils.isFormData(formData) && utils.isFunction(formData.entries)) {const obj = {};utils.forEachEntry(formData, (name, value) => {buildPath(parsePropPath(name), value, obj, 0);});return obj;}return null;
} 

我找 axios 自带的测试实例中的例子打印了一下结果

对象的值是数组

const formData = new FormData();
formData.append('foo', '1');
formData.append('foo', '2');

const res = formDataToJSON(formData);
console.log('res', res); // -> { foo: ['1', '2'] } 

对象

const formData = new FormData();

formData.append('foo', '1');
formData.append('bar', '2');

const res = formDataToJSON(formData);
console.log('res', res); // -> {foo: '1', bar: '2'} 

嵌套对象

const formData = new FormData();

formData.append('foo[bar][baz]', '123');

const res = formDataToJSON(formData);
console.log('res', res); // -> { foo : { bar : {baz: '123'} } } 

小结

1.formDataToJSON 函数接受 一个 FormData 对象最终返回 JavaScript 对象。
2.FormData() 构造函数用于创建一个新的FormData对象。该函数在 Web 中可用,一些环境会报错“FormData is not define”。
3.FormData 对象转换 JavaScript 对象的功能不是很常用。但是这里的主要收获是,当函数设计的相对复杂的时候,可以用抽丝剥茧的方式,现将支线整理出来,最终拼凑成一个完整的主线。

总结

功能函数会依据实际需求去实现功能。通过分析 axios 源码中的两个重点的功能函数,学习复杂功能如何设计,以及补充了些知识点,还得到目前代码中有些判断条件简化的收获。

很愉快的一次源码阅读体验。

axios 源码阅读历程

捎带分享一下我关于 axios 源码阅读中的历程。

数月前

因为工作中需要弄清楚 axios 的 request 中的入参而在源码中寻找答案,当时想抽时间进行一次源码阅读,但是没有坚持下去,就此作罢。

数日前

前几天做功能联想,重新开始阅读,这次收获颇多。

最后

最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值