【若川视野 x 源码共读】第19期 | axios 工具函数

521 阅读6分钟

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

学习目标

  1. 如何调试axios源码;
  2. 如何学习优秀开源项目的代码,应用到自己的项目;
  3. axios源码中实用的工具函数;

源码地址

github.com/axios/axios…

调试技巧

git clone https://github.com/axios/axios.git

cd axios

npm start

打开 http://localhost:3000/

打开浏览器的开发中工具->source->axios->axios.js即可打断点调试

源码解读

isBuffer 是否为Buffer

由于axios可用于浏览器环境和node环境,所以会涉及node相关模块的使用

JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。

但在处理像TCP流或文件流时,必须使用到二进制数据。因此在 Node.js中,定义了一个 Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。

/**
 * Determine if a value is a Buffer
 
 * 先判断不是 `undefined`和`null`
 * 再判断 `val`存在构造函数,因为`Buffer`本身是一个类
 * 最后通过自身的`isBuffer`方法判断
 */
function isBuffer(val) {
  return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor)
    && typeof val.constructor.isBuffer === 'function' && val.constructor.isBuffer(val);
}

// Buffer使用

// 创建一个长度为 256、且用 0 填充的 Buffer。
const buf = Buffer.alloc(256);
// 写入字符串
len = buf.write("hello friends");

console.log("写入字节数 : "+  len); // 输出:写入字节数 : 13

// 创建一个包含 '0x1, 0x2, 0x3' 的 Buffer。
const ss = Buffer.from('1, 2, 3');
console.log(typeof ss.constructor) // 输出:function
console.log(ss.constructor.isBuffer(ss)) // 输出:true
// 读取 Node 缓冲区数据
console.log( ss.toString('utf8')); //输出: 1, 2, 3

const buf1 = Buffer.from([1, 2, 3]);
const json = JSON.stringify(buf1); // 这里会隐式地调用该 toJSON()转为## JSON 对象
console.log(json);

isStream 是否为Stream

Stream是Node中的一个抽象接口,很多对象都实现了此接口,比如http 服务器发起请求的request 对象、stdout(标准输出)等。

Stream 有四种流类型:

  • Readable - 可读操作。
  • Writable - 可写操作。
  • Duplex - 可读可写操作.
  • Transform - 操作被写入数据,然后读出结果。

所有的 Stream 对象都是 EventEmitter 的实例。常用的事件有:

  • data - 当有数据可读时触发。
  • end - 没有更多的数据可读时触发。
  • error - 在接收和写入过程中发生错误时触发。
  • finish - 所有数据已被写入到底层系统时触发。
/**
 * Determine if a value is a Stream
 * 判断val是对象且包含pipe方法
 */
function isStream(val) {
  return isObject(val) && isFunction(val.pipe);
}

// Stream使用
var fs = require("fs");
var zlib = require('zlib');

var data = '';

// 创建可读流
var readerStream = fs.createReadStream('input.txt');
// 设置编码为 utf8。
readerStream.setEncoding('UTF8');
// 处理流事件 --> data, end, and error
readerStream.on('data', function(chunk) {
   data += chunk;
});
readerStream.on('end',function(){
   console.log(data);
});


// 压缩 input.txt 文件为 input.txt.gz
fs.createReadStream('input.txt')
  .pipe(zlib.createGzip())
  .pipe(fs.createWriteStream('input.txt.gz'));

isFormData

FormData 接口提供了一种表示表单数据的键值对 key/value 的构造方式,并且可以轻松的将数据通过XMLHttpRequest.send() 方法发送。

typeof FormData === 'function' node环境返回 false instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上 Object.prototype.toString返回 [object FormData]

// Determine if a value is a FormData

function isFormData(thing) {
  var pattern = '[object FormData]';
  return thing && (
    (typeof FormData === 'function' && thing instanceof FormData) ||
    toString.call(thing) === pattern ||
    (isFunction(thing.toString) && thing.toString() === pattern)
  );
}

isFile 判断文件类型

function isFile(val) {
  return Object.prototype.toString.call(val) === '[object File]';
}

var file = new File(["foo"], "foo.txt");
console.log(Object.prototype.toString.call(file)) // 输出: [object File]

isBlob 判断Blob

Blob对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,通过使用 FileReader 的其它方法可以把 Blob 读取为字符串或者数据URL。

function isBlob(val) {
  return Object.prototype.toString.call(val) === '[object Blob]';
}

// Blob使用
// 通过其它对象创建 Blob 对象
var debug = {hello: "world"};
var blob = new Blob([JSON.stringify(debug, null, 2)], {type : 'application/json'});

isURLSearchParams 判断URLSearchParams

URLSearchParams 接口定义了一些实用的方法来处理 URL 的查询字符串。

function isURLSearchParams(val) {
  return Object.prototype.toString.call(val) === '[object URLSearchParams]';
}

// 例子
const url = new URL('https://juejin.cn/editor/drafts/7094229471175114789?q=URLUtils.searchParams&topic=api'); 
const searchParams = new URLSearchParams(url.search);
searchParams.has("topic") === true; // true
searchParams.get("topic") === "api"; // true
searchParams.getAll("topic"); // ["api"]
searchParams.append("topic", "webdev");
searchParams.toString(); // "q=URLUtils.searchParams&topic=api&topic=webdev"
searchParams.set("topic", "More webdev"); 
searchParams.toString(); // "q=URLUtils.searchParams&topic=More+webdev"
searchParams.delete("topic"); 
searchParams.toString(); // "q=URLUtils.searchParams"

trim 去除首尾空格

// `trim`方法不存在的话,用正则
function trim(str) {
  return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
}

isStandardBrowserEnv 判断标准浏览器环境

function isStandardBrowserEnv() {
  if (typeof navigator !== 'undefined' && (navigator.product === 'ReactNative' ||
                                           navigator.product === 'NativeScript' ||
                                           navigator.product === 'NS')) {
    return false;
  }
  return (
    typeof window !== 'undefined' &&
    typeof document !== 'undefined'
  );
}
// 目前product属性返回浏览器的引擎(产品)名称,所有浏览器都返回“Gecko”

forEach 遍历对象或数组

function forEach(obj, fn) {
  // Don't bother if no value provided
  if (obj === null || typeof obj === 'undefined') {
    return;
  }

  // Force an array if not already something iterable
  // 非对象类型则强制转换为可迭代对象 数组本身具有可迭代属性
  if (typeof obj !== 'object') {
    /*eslint no-param-reassign:0*/
    obj = [obj];
  }

  if (isArray(obj)) {
    // Iterate over array values
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    // Iterate over object keys
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}
  • 数组的 forEach() 方法按升序为数组中含有效值的每一项执行一次 callback 函数,那些已删除或者未初始化的项将被跳过。例如[1,2,,4] callback会跳过第三个仅执行三次。 备注:  除了抛出异常以外,没有办法中止或跳出 forEach() 循环
  • for循环 使用更灵活,可使用break随时退出循环
  • for...of语句可迭代对象(包括 ArrayMapSetStringTypedArrayarguments 对象等等)上创建一个迭代循环。数组建议使用
  • for...in语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性对象建议使用 通过obj.hasOwnProperty(prop)判断自身属性

merge递归合并多个对象

function merge(/* obj1, obj2, obj3, ... */) {
  var result = {};
  function assignValue(val, key) {
    if (isPlainObject(result[key]) && isPlainObject(val)) { // 重复key则覆盖前面的值 值为对象则循环调用merge merge基础对象为前一次的值
      result[key] = merge(result[key], val);
    } else if (isPlainObject(val)) { // 新key的对象 循环调用merge merge基础对象为{}
      result[key] = merge({}, val);
    } else if (isArray(val)) { // 新key数组 则直接赋值 slice()方法返回一个新的数组对象
      result[key] = val.slice();
    } else { // 新key的其他类型值则直接赋值
      result[key] = val;
    }
  }
   // 循环入参 依次执行赋值函数
  for (var i = 0, l = arguments.length; i < l; i++) {
    forEach(arguments[i], assignValue);
  }
  return result;
}

extend扩展对象属性

function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  });
  return a;
}
// extend对相同的属性仅覆盖 不合并
extend({foo: {a: 1, c: 3}}, {foo: {a: 2, b: 3}}) // foo: {a2, b: 3}
merge({foo: {a: 1, c: 3}}, {foo: {a: 2, b: 3}}) // foo: {a2, c: 3, b: 3}

stripBOM删除UTF-8编码中BOM

function stripBOM(content) {
  if (content.charCodeAt(0) === 0xFEFF) {
    content = content.slice(1);
  }
  return content;
}

 BOM全称是Byte Order Mark,它是一个Unicode字符,通常出现在文本的开头,用来标识字节序。UTF-8主要的优点是可以兼容ASCII,前 128 个 UTF-8 字符与前 128 个 ASCII 字符(编号为 0-127) 精确匹配,这意味着现有的 ASCII 文本已经是有效的 UTF-8。但如果使用BOM的话,这个好处就荡然无存了。

总结

浏览器环境开发是我们熟悉的区域,相对来说,node环境的开发薄弱一些,需要去做更多的学习和实践。

其他资料