细读Axios源码系列五 - 默认参数合并、构建响应结果、transformResponse与transformRequest

写在开头

经过前两篇文章的学习,axios 的核心功能就讲完了,主要就是拦截器取消请求这两个功能,剩下的就是一些细节性的东西,杂七杂八的了,今天我们就慢慢来把它给完善起来。

默认参数合并

第二篇文章中,我们创建了一个 lib/defaults.js 文件,它是整个 axios 默认的配置信息文件。但我们还没具体使用到它,它记录的默认配置信息,最终肯定是要和我们使用 axios(config) 对象时传递的请求配置信息合并起来的,下面,我们且来瞧瞧在源码中,它是如何被使用到的。

先来到 lib/axios.js 文件:

var Axios = require('./core/Axios');
var bind = require('./helpers/bind');
var utils = require('./utils');
var defaults = require('./defaults'); // 引入默认配置文件

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig); // 实例化时接收默认配置
  var instance = bind(Axios.prototype.request, context);
  utils.extend(instance, Axios.prototype, context);
  utils.extend(instance, context);
  return instance;
}

var axios = createInstance(defaults); // 把默认配置传递到 Axios 实例中

axios.CancelToken = require('./cancel/CancelToken');
axios.Cancel = require('./cancel/Cancel');
axios.isCancel = require('./cancel/isCancel');
module.exports = axios;
复制代码

lib/core/Axios.js 文件:


var dispatchRequest = require('./dispatchRequest');
var InterceptorManager = require('./InterceptorManager');
var mergeConfig = require('./mergeConfig'); // 引入合并函数

function Axios(instanceConfig) {
  this.defaults = instanceConfig; // 保存默认配置
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

Axios.prototype.request = function request(config) { // config 为 请求配置, 这里说过很多遍啦。
  // 允许 config 为字符串类型, 能直接传递一个 URL
  // 例如: axios.get(url, config) ; 更多查看: https://www.axios-http.cn/docs/instance
  if (typeof config === 'string') {
    config = arguments[1] || {}; // 取第二个参数作为请求配置
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  // 默认配置 与 请求配置 进行合并
  config = mergeConfig(this.defaults, config);

  // 设置请求方式, 默认为 get, 都统一转成小写, 但在 xhr.js 中会统一转成大写
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }
  
  ...
}
module.exports = Axios;
复制代码

新建 lib/core/mergeConfig.js 文件:

var utils = require('../utils');

/**
 * 把默认配置和请求配置合并后返回, 以请求配置为准
 * @param {*} config1: 默认配置
 * @param {*} config2: 请求配置
 */
module.exports = function mergeConfig(config1, config2 = {}) {
  var config = {};
  // 规定一些 "特定属性", 按 它们值的类型 或者 它们的功能 分类好
  var valueFromConfig2Keys = ['url', 'method', 'data'];
  var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params'];
  var defaultToConfig2Keys = [
    'baseURL', 'transformRequest', 'transformResponse', 'paramsSerializer',
    'timeout', 'timeoutMessage', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName',
    'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', 'decompress',
    'maxContentLength', 'maxBodyLength', 'maxRedirects', 'transport', 'httpAgent',
    'httpsAgent', 'cancelToken', 'socketPath', 'responseEncoding'
  ];
  var directMergeKeys = ['validateStatus'];
  
  // 合并两个值
  function getMergedValue(target, source) {
    if (utils.isPlainObject(target) && utils.isPlainObject(source)) {
      // 如果两个值都是普通对象, 则直接合并
      return utils.merge(target, source);
    } else if (utils.isPlainObject(source)) {
      // 如果第一个值不是一个普通对象, 第二个值是一个普通对象, 则直接返回第二个值
      return utils.merge({}, source);
    } else if (utils.isArray(source)) {
      // 如果两个值都不是普通对象, 且第二个值是一个数组, 则返回一个新数组对象
      return source.slice();
    }
    // 如果都不是就返回第二个值
    return source;
  }
  // 针对对象形式
  function mergeDeepProperties(prop) {
    if (!utils.isUndefined(config2[prop])) {
      config[prop] = getMergedValue(config1[prop], config2[prop]);
    } else if (!utils.isUndefined(config1[prop])) {
      config[prop] = getMergedValue(undefined, config1[prop]);
    }
  }

  // 这四个 forEach 是那些 "特定属性" 的合并具体过程
  utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) {
    if (!utils.isUndefined(config2[prop])) { // 请求配置中存在这个属性
      config[prop] = getMergedValue(undefined, config2[prop]); 
    }
  });
  utils.forEach(mergeDeepPropertiesKeys, mergeDeepProperties);
  utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) {
    if (!utils.isUndefined(config2[prop])) {
      config[prop] = getMergedValue(undefined, config2[prop]);
    } else if (!utils.isUndefined(config1[prop])) {
      config[prop] = getMergedValue(undefined, config1[prop]);
    }
  });
  utils.forEach(directMergeKeys, function merge(prop) {
    if (prop in config2) {
      config[prop] = getMergedValue(config1[prop], config2[prop]);
    } else if (prop in config1) {
      config[prop] = getMergedValue(undefined, config1[prop]);
    }
  });
  
  // 把所有 "特定属性" 拼接成一个数组
  var axiosKeys = valueFromConfig2Keys
        .concat(mergeDeepPropertiesKeys).concat(defaultToConfig2Keys).concat(directMergeKeys);
  // 找到 请求配置 中不属于 "特定属性" 的那些key, 像 axios({url: '', name: '橙某人'}) 中的 name
  var otherKeys = Object.keys(config1).concat(Object.keys(config2)).filter(function filterAxiosKeys(key) {
      return axiosKeys.indexOf(key) === -1;
  });
  // 直接把不属于 "特定属性" 的其他属性做一个拷贝
  utils.forEach(otherKeys, mergeDeepProperties);

  return config;
}
复制代码

默认配置请求配置 的合并其实就是简单的两个对象合并过程,不过,由上面的 mergeConfig.js 文件可以看出,它也不是一股脑直接就用 Object.assign() 合并完就算了,它是有条件的筛选规定的属性进行合并,并且是以 请求配置 的对象为准的。

那么,写到这里 默认配置请求配置 的合并就完成,在 lib/core/Axios.js 文件中,我们即可获取到一个完整的参数信息了。

接下来,我们继续哈。(✪ω✪)

构建响应结果

正常情况下,我们使用 axios 发送一个请求,axios 会把我们的响应结果做一层封装,把后端真正返回的数据塞在 data 属性下,还会多返回 configheadersstatus 等属性其他信息。

image.png

下面,我们一起来看看源码中是如何做的。

来到 lib/adapters/xhr.js 文件:

var utils = require('./../utils');
var settle = require('./../core/settle');
var parseHeaders = require('./../helpers/parseHeaders');

module.exports = function xhrAdapter(config) {
    return new Promise(function dispatchXhrRequest(resolve, reject) {
        // 这里的config已经是将默认配置和请求配置合并后的结果了
        var requestData = config.data; // 获取到config.data中的请求数据, config.params 我们后续再讲
        var requestHeaders = config.headers;
        var responseType = config.responseType;

        var request = new XMLHttpRequest();

        request.open(config.method.toUpperCase(), config.url, true);
        
        // 设置超时时长: 毫秒, 超时请求会自动终止
	request.timeout = config.timeout;

        // 定义一个请求结束后的处理函数
        function onloadend() {
          if (!request) return;

          // XMLHttpRequest.getAllResponseHeaders() 会返回所有响应头, 以字符串的形式, 详情可以看这里: https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/getAllResponseHeaders
          var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
          // 根据响应类型获取返回结果
          var responseData = !responseType || responseType === 'text' ||  responseType === 'json' ? request.responseText : request.response;

          // 构建响应结果
          var response = {
            data: responseData,
            status: request.status,
            statusText: request.statusText,
            headers: responseHeaders,
            config: config,
            request: request
          };

          // 验证响应结果状态码情况
          settle(resolve, reject, response);

          // 请求结束, 清空当前的 XMLHttpRequest 对象
          request = null;
        }

        if ('onloadend' in request) {
          request.onloadend = onloadend;
        } else {
          request.onreadystatechange = function handleLoad() {
            if (!request || request.readyState !== 4) return;
            if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) return;
            // 这里制造出下一个宏任务的原因是: readystate处理程序是在ontimeout或onerror处理程序之前调用, 我们应该要在它们调用结束之后才来统一调用onloadend处理最终结果
            setTimeout(onloadend);
          };
        }
        
        // 监听超时
        request.ontimeout = function handleTimeout() {
          reject('请求超时');
          request = null;
        };

        // 监听网络错误
        request.onerror = function handleError() {
          reject('网络错误');
          request = null;
        };

        if (config.cancelToken) {
          config.cancelToken.promise.then(function onCanceled(cancel) {
            if (!request) return;
            request.abort();
            reject(cancel);
            request = null;
          });
        }
        request.send(requestData); // XMLHttpRequest请求 请求中要发送的数据体:https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/send
    });
}
复制代码

我们新建 lib/core/settle.js 文件:

// 验证响应结果的状态值, 再去调用 resolve 或者 reject
module.exports = function settle(resolve, reject, response) {
  // 可以通过 config.validateStatus 自定义响应状态码的验证规则, 默认值: status >= 200 && status < 300
  var validateStatus = response.config.validateStatus;
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    reject('出现错误了');
  }
};

复制代码

settle.js 文件的作用就是对外提供一个自定义验证 HTTP 状态码的功能,像文档上说的这样子:

image.png

再新建 lib/helpers/parseHeaders.js 文件:

var utils = require('./../utils');

// Headers whose duplicates are ignored by node
// c.f. https://nodejs.org/api/http.html#http_message_headers
var ignoreDuplicateOf = [
  'age', 'authorization', 'content-length', 'content-type', 'etag',
  'expires', 'from', 'host', 'if-modified-since', 'if-unmodified-since',
  'last-modified', 'location', 'max-forwards', 'proxy-authorization',
  'referer', 'retry-after', 'user-agent'
];

module.exports = function parseHeaders(headers) {
  var parsed = {};
  var key;
  var val;
  var i;
  if (!headers) { return parsed; }
  utils.forEach(headers.split('\n'), function parser(line) {
    i = line.indexOf(':');
    key = utils.trim(line.substr(0, i)).toLowerCase();
    val = utils.trim(line.substr(i + 1));
    if (key) {
      if (parsed[key] && ignoreDuplicateOf.indexOf(key) >= 0) {
        return;
      }
      if (key === 'set-cookie') {
        parsed[key] = (parsed[key] ? parsed[key] : []).concat([val]);
      } else {
        parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
      }
    }
  });
  return parsed;
};
复制代码

这个文件的作用是把返回的响应头字符串转变成对象的形式。因为请求结束后,返回的响应头是以字符串的形式存在的,为了方便我们查看,需要进行转换,而我们可以通过 XMLHttpRequest.getAllResponseHeaders() 方法获取到所有的响应头字符串。

image.png

做完以上这些,我们执行 grunt build 命令把项目打包,然后写个测试用例看看:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <script src="../dist/axios.js"></script>
</head>
<body>
    <script>
        axios({
            url: 'http://localhost:3000/posts',
            method: 'get',
        }).then(res => {
            console.log(res)
        })
    </script>
</body>
</html>
复制代码

image.png

可以看到 axios 的响应结果我们大致构建出来了,但是图中,我们的 data 属性中的数据还只是一个 json 字符串,这还需要一些后续的工作,我们继续来看囖。(✪ω✪)

transformResponse

上面我们一直在说默认配置 lib/defaults.js 文件,但我们之前在 第二篇 文章中也只是给它写了一点内容而已:

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // adapter = require('./adapters/http');
  }
  return adapter;
}
var defaults = {
  timeout: 0,
  adapter: getDefaultAdapter(),
}
module.exports = defaults;
复制代码

现在,我们来把它完善完善 lib/defaults.js 文件:

var utils = require('./utils');

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // adapter = require('./adapters/http');
  }
  return adapter;
}
var defaults = {
    timeout: 0, // 超时时长
    adapter: getDefaultAdapter(), // 适配器
    // 在 lib/core/settle.js 中用于默认验证 HTTP 状态码
    validateStatus: function validateStatus(status) {
        return status >= 200 && status < 300;
    },
    transitional: {
        silentJSONParsing: true, // 版本兼容配置-返回值转换为 Json 出错时是否置为 null 返回
        forcedJSONParsing: true, // 版本兼容配置-responseType 设置非 json 类型时是否强制转换成 json 格式
        clarifyTimeoutError: false, // 版本兼容配置-请求超时时是否默认返回 ETIMEDOUT 类型错
    },
    // 注意 transformResponse 属性设计成一个数组, 是因为能更加方便的被使用, 我们可以通过传递数组,并通过一个一个函数来改造响应结果, 这样无疑是多样化的
    transformResponse: [function transformResponse(data) {
        var transitional = this.transitional;
        var silentJSONParsing = transitional && transitional.silentJSONParsing;
        var forcedJSONParsing = transitional && transitional.forcedJSONParsing;
        var strictJSONParsing = !silentJSONParsing && this.responseType === 'json';
        if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) {
          try {
            // 把数据转换成对象, 因为 xhr.js 文件中的取的是 responseText
            return JSON.parse(data);
          } catch (e) {
            if (strictJSONParsing) {
              if (e.name === 'SyntaxError') {
                // throw enhanceError(e, this, 'E_JSON_PARSE');
              }
              throw e;
            }
          }
        }
        return data;
   }],
}

module.exports = defaults;
复制代码

上面代码中,我们添加了一些默认配置,但我们主要是来看 transformResponse 属性,或许有一些小伙伴对它还比较陌生,下面是文档对它的介绍,可以先看看。其实它应该还有一个"孪生兄弟" - transformRequest 属性,但是我们暂时用不上,现在只是想解决 data 是字符串的问题,后面我们再来说它。

image.png

添加完默认配置后,我们来到 lib/core/dispatchRequest.js 文件:

var defaults = require('../defaults');
var isCancel = require('../cancel/isCancel');
var transformData = require('./transformData'); // 引入新文件

function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

// 这个 config 是默认配置和请求配置合并后的结果
module.exports = function dispatchRequest(config) {
    throwIfCancellationRequested(config);
    var adapter = config.adapter || defaults.adapter;
    return adapter(config).then(function onAdapterResolution(response) {
        throwIfCancellationRequested(config);
        // 返回响应结果前, 去执行 transformData() 方法, 其实也就是去执行 transformResponse 数组,
        // transformData() 方法内部就是遍历 transformResponse 数组依次执行每个函数
        response.data = transformData.call(
          config, // transformResponse数组中每个函数的 this
          response.data,
          response.headers,
          config.transformResponse
        );

        return response;
    }, function onAdapterRejection(reason) {
        if (!isCancel(reason)) {
            throwIfCancellationRequested(config);
        }
        return Promise.reject(reason);
    });
}
复制代码

新建 lib/core/transformData.js 文件:

var utils = require('./../utils');
var defaults = require('./../defaults');
module.exports = function transformData(data, headers, fns) {
  // 使用这个方法时, this 可以通过 call() 等方法去设置, 否则就取 默认配置 对象为this
  var context = this || defaults;
  utils.forEach(fns, function transform(fn) {
    // 执行每个函数, 把data和headers作为函数参数, 并改变函数内部this
    data = fn.call(context, data, headers);
  });
  return data;
};
复制代码

做完上面的工作后,我们就能进行测试了,执行 grunt build 命令,还是上面那个例子:

image.png

可以看到 data 属性中的结果变成正常的对象形式了。

当然,还没完,我们顺便测试一下 transformResponse 的功能:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <script src="../dist/axios.js"></script>
</head>
<body>
    <script>
        axios({
            url: 'http://localhost:3000/posts',
            method: 'get',
            transformResponse: [(data, headers) => {
                headers.name = '橙某人';
                return data;
            }, (data, headers) => {
                console.log(data, headers)
                return data;
            }],
        }).then(res => {
            console.log(res)
        })
    </script>
</body>
</html>
复制代码

image.png

上面例子中,我改动了一下 headers 参数,在其中添加了一个 name 属性,而 data 参数我没动,所以最终它的值还是一个字符串数组。

这说明了一个问题,当我们请求配置中提供了 transformResponse 属性时,就只会执行请求配置中的 transformResponse 属性了,而当我们没有提供时, lib/defaults.js 文件中默认配置的 transformResponse 属性就会被执行。

一切如我们所料,真是太棒了呢,是吧o(^▽^)o。

image.png

transformRequest

完成了 transformResponse 属性后,我们来看看它的"孪生兄弟" - transformRequest 属性。

同样在 lib/defaults.js 文件中先添加默认配置:

var utils = require('./utils');
var normalizeHeaderName = require('./helpers/normalizeHeaderName'); // 引入新文件
function getDefaultAdapter() {
  ...
}
// 设置请求头
function setContentTypeIfUnset(headers, value) {
  if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
    headers['Content-Type'] = value;
  }
}
var defaults = {
    ...,
    // 关于请求头信息: https://developer.mozilla.org/zh-CN/docs/Glossary/Request_header
    headers: {
        // 默认必传请求头
        common: {
            'Accept': 'application/json, text/plain, */*'
        }
    },
    transformRequest: [function transformRequest(data, headers) {
        // 验证名称大小写
        normalizeHeaderName(headers, 'Accept');
        normalizeHeaderName(headers, 'Content-Type');

        if (utils.isFormData(data) || utils.isArrayBuffer(data) || utils.isBuffer(data)
            || utils.isStream(data) || utils.isFile(data) || utils.isBlob(data)) {
          return data;
        }
        if (utils.isArrayBufferView(data)) return data.buffer;
        if (utils.isURLSearchParams(data)) {
          // 如果请求的参数data是一个 URLSearchParams 对象, 则会自动设置特殊的请求头
          setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
          return data.toString();
        }
        if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {
          setContentTypeIfUnset(headers, 'application/json');
          return JSON.stringify(data);
        }
        return data;
    }],
}
// 每一种不同类型的请求,可能会有一些不一样的请求头, 所以都给他们一个对象,方便我们为它们各自设置一些不一样的默认请求头。
// 如下图
var DEFAULT_CONTENT_TYPE = {
  'Content-Type': 'application/x-www-form-urlencoded'
};
utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
  defaults.headers[method] = {};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});

module.exports = defaults;
复制代码

image.png

老样子,我们回到 lib/core/dispatchRequest.js 文件:

var defaults = require('../defaults');
var isCancel = require('../cancel/isCancel');
var transformData = require('./transformData'); 
var utils = require('./../utils'); // 引入新文件

function throwIfCancellationRequested(config) {
  ...
}

module.exports = function dispatchRequest(config) {
    throwIfCancellationRequested(config);
    
    // 确保 headers 属性一定是一个对象, 因为如果 请求配置 中把 axios({headers: undefined}), 后续会带来不必要的麻烦
    config.headers = config.headers || {};

    // 请求发送前, 对象请求参数data做处理, 同transformResponse原理过程一样
    config.data = transformData.call(
        config,
        config.data,
        config.headers,
        config.transformRequest
    );

    // 合并 headers, 以 config.headers 为准
    config.headers = utils.merge(
        config.headers.common || {}, // 默认必传请求头
        config.headers[config.method] || {}, // 当前请求类型 的其他一些 默认请求头
        config.headers // 请求配置 中的请求头
    );

    // 删除默认请求了, 保持 header 的纯净
    utils.forEach(['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
        function cleanHeaderConfig(method) {
          delete config.headers[method];
        }
    );

    var adapter = config.adapter || defaults.adapter;
    return adapter(config).then(function onAdapterResolution(response) {
        throwIfCancellationRequested(config);
        response.data = transformData.call(
          config, // transformResponse数组中每个函数的 this
          response.data,
          response.headers,
          config.transformResponse
        );

        return response;
    }, function onAdapterRejection(reason) {
        if (!isCancel(reason)) {
            throwIfCancellationRequested(config);
        }
        return Promise.reject(reason);
    });
}
复制代码

修改完后,我们先把项目打包 grunt build,然后继续写个例子来测试一下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <script src="../dist/axios.js"></script>
</head>
<body>
    <script>
        axios({
            url: 'http://localhost:3000/posts',
            method: 'get',
        }).then(res => {
            console.log(res)
        })
    </script>
</body>
</html>
复制代码

image.png

image.png

观察图中的结果后,可以发现我们的默认请求头并没有生效。

这是因为还差那么一小步唷,继续来看,修改 lib/adapters/xhr.js 文件:

var utils = require('./../utils');
var settle = require('./../core/settle');
var parseHeaders = require('./../helpers/parseHeaders');
var utils = require('./../utils'); // 引入新文件

module.exports = function xhrAdapter(config) {
    return new Promise(function dispatchXhrRequest(resolve, reject) {
        var requestData = config.data;
        var requestHeaders = config.headers;
        var responseType = config.responseType;
        
        // 如果传递的 data 为 FormData 对象类型, 则删除头部的设置, 浏览器会自动为 FormData 参数类型的请求设置请求头
        if (utils.isFormData(requestData)) {
          delete requestHeaders['Content-Type'];
        }
    
        var request = new XMLHttpRequest();
        request.open(config.method.toUpperCase(), config.url, true);
	request.timeout = config.timeout;
        function onloadend() {
          if (!request) return;
          var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
          var responseData = !responseType || responseType === 'text' ||  responseType === 'json' ? request.responseText : request.response;
          var response = {
            data: responseData,
            status: request.status,
            statusText: request.statusText,
            headers: responseHeaders,
            config: config,
            request: request
          };
          settle(resolve, reject, response);
          request = null;
        }

        if ('onloadend' in request) {
          request.onloadend = onloadend;
        } else {
          request.onreadystatechange = function handleLoad() {
            if (!request || request.readyState !== 4) return;
            if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) return;
            setTimeout(onloadend);
          };
        }
        
        // 设置请求头, 
        if ('setRequestHeader' in request) {
          utils.forEach(requestHeaders, function setRequestHeader(val, key) {
            if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
              // 如果没有设置 config.data 则不需要 content-type 请求头
              delete requestHeaders[key];
            } else {
              request.setRequestHeader(key, val);
            }
          });
        }
        
        request.ontimeout = function handleTimeout() {};
        request.onerror = function handleError() {};
        if (config.cancelToken) {}
        request.send(requestData);
    });
}
复制代码

上面代码中,我们添加了两处代码,其实应该很简单啦,不就是把 headers 对象转化到 XMLHttpRequest 对象身上囖,其中主要是利用了 XMLHttpRequest.setRequestHeader() 方法来完成。

我们再次打包(grunt build)测试:

image.png

可以发现,这次就真的成功了。

当然,还没完,我们还得继续测试一下 transformRequest 属性与 headers 属性的具体使用功能呢。


我们先来补充一下关于 json-server 模块中 post 方法的使用:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
    <script>
        axios({
            url: 'http://localhost:3000/posts',
            method: 'post',
            data: {
                title: '测试',
                author: "测试人员"
            }
        }).then(res => {
            console.log(res)
        })
    </script>
</body>
</html>
复制代码

我们引入了正常的 axios 库,然后发送一个 post 请求,这个时候,你来查看你启动的 xxx.json 文件(第一篇文章讲过 json-server 模块的使用),会发送 post 请求的数据被插入进来了,这是关于 json-server 模块中 post 方式的使用。

image.png


知道了 post 请求方式后,我们就能来测试,本来我们按道理来说,估计会怎么写的吧:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <script src="../dist/axios.js"></script>
</head>
<body>
    <script>
        axios({
            url: 'http://localhost:3000/posts',
            method: 'post',
            data: {
                title: '测试',
                author: "测试人员"
            },
            transformRequest: [(data) => {
                data.author = '橙某人';
                return data;
            }],
            headers: {
                anth: '11' // 传递其他请求头, 注意值不能是中文, 不要问我为什么知道...
            }
        }).then(res => {
            console.log(res)
        })
    </script>
</body>
</html>
复制代码

对于 headers 属性,它能传递其他请求头说明它的功能应该是正常的了。

image.png

而对于 transformRequest 属性,当我看到这两个结果后,我懵逼了!!!

image.png

image.png

经过测试,我发现原本 axios 也是有这个问题,这就应该不是在重写源码的时候写错导致的了。

仔细看了一下请求头,发现 Content-Type 竟然是以表单的形式在提交的。

image.png

这其实是在源码中 lib/defaults.js 文件写的默认配置:

image.png

好,事情真相大白了,不关我的事,果断甩锅。

而之所以 axios 会这么设计,这里小编的个人想法是,当我们使用 transformRequest 属性的时候,很多时候是想改变整个请求参数的类型,比如序列化参数这种。

在小编的另一篇 完整的Axios封装-单独API管理层、参数序列化、取消重复请求、Loading、状态码... 文章里面写到的这种序列化:

image.png

utils.js

你可以看小编写的 utils.js 文件,标有一些中午注释,或者直接看 官方utils.js 文件。

var bind = require('./helpers/bind');

var toString = Object.prototype.toString;

/**
 * 判断是否是一个字符串
 * @param {Object} val
 * @returns {boolean}
 */
function isString(val) {
  return typeof val === 'string';
}

/**
 * 判断是否是 undefined
 * @param {Object} val
 * @returns {boolean}
 */
function isUndefined(val) {
  return typeof val === 'undefined';
}

/**
 * 判断是否是对象
 * @param {Object} val
 * @returns {boolean}
 */
function isObject(val) {
  return val !== null && typeof val === 'object';
}

/**
 * 判断是否是一个函数
 * @param {Object} val
 * @returns {boolean}
 */
function isFunction(val) {
  return toString.call(val) === '[object Function]';
}

/**
 * 判断对象是否是普通对象
 * @param {Object} val
 * @return {boolean}
 */
function isPlainObject(val) {
  if (toString.call(val) !== '[object Object]') {
    return false;
  }

  var prototype = Object.getPrototypeOf(val);
  return prototype === null || prototype === Object.prototype;
}

/**
 * 判断是否是数组
 * @param {Object} val
 * @returns {boolean}
 */
function isArray(val) {
  return toString.call(val) === '[object Array]';
}

/**
 * 判断是否可以使用 URLSearchParams, 浏览器和Node环境均可使用, 但低版本浏览器要使用需要安装 npm install --save url-search-params
 * URLSearchParams 能帮助你快速解析URL中的参数请求, 也能帮助你快速拼接URL中需要的参数形式
 * @param {Object} val
 * @returns {boolean}
 */
function isURLSearchParams(val) {
  return typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams;
}

/**
 * 判断是否是日期类型
 * @param {Object} val
 * @returns {boolean}
 */
function isDate(val) {
  return toString.call(val) === '[object Date]';
}

/**
 * 判断是否是 FormData 对象
 * @param {Object} val
 * @returns {boolean}
 */
function isFormData(val) {
  return (typeof FormData !== 'undefined') && (val instanceof FormData);
}

/**
 * 判断是否是一个 ArrayBuffer 类型
 * @param {Object} val
 * @returns {boolean}
 */
function isArrayBuffer(val) {
  return toString.call(val) === '[object ArrayBuffer]';
}

/**
 * 判断是否是一个 Buffer 类型
 * @param {Object} val
 * @returns {boolean}
 */
function isBuffer(val) {
  return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor)
    && typeof val.constructor.isBuffer === 'function' && val.constructor.isBuffer(val);
}

/**
 * 判断是否是一个 Stream 类型
 * @param {Object} val
 * @returns {boolean}
 */
function isStream(val) {
  return isObject(val) && isFunction(val.pipe);
}

/**
 * 判断是否是一个 File 类型
 * @param {Object} val
 * @returns {boolean}
 */
function isFile(val) {
  return toString.call(val) === '[object File]';
}

/**
 * 判断是否是一个 Blob 类型
 * @param {Object} val
 * @returns {boolean}
 */
function isBlob(val) {
  return toString.call(val) === '[object Blob]';
}

/**
 * 判断是否是一个 isArrayBufferView
 * @param {Object} val
 * @returns {boolean}
 */
function isArrayBufferView(val) {
  var result;
  if ((typeof ArrayBuffer !== 'undefined') && (ArrayBuffer.isView)) {
    result = ArrayBuffer.isView(val);
  } else {
    result = (val) && (val.buffer) && (val.buffer instanceof ArrayBuffer);
  }
  return result;
}

/**
 * 去除头尾空格
 * @param {String} str
 * @returns {String}
 */
function trim(str) {
  return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
}

/**
 * 重写 forEach
 * @param {Object|Array} obj
 * @param {Function} fn
 * @returns
 */
function forEach(obj, fn) {
  if (obj === null || typeof obj === 'undefined') return;
	// 不是对象类型, 则变成一个数组, 如函数
  if (typeof obj !== 'object') obj = [obj];

  if (isArray(obj)) {
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

/**
 * 扩展对象 a 身上的属性, 会将 b 身上的属性拷贝到 a 身上
 * @param {Object} a
 * @param {Object} b
 * @param {Object} thisArg: this 指向
 */
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;
}

/**
 * 接收多个对象进行合并
 * @param {Object} obj1
 * @returns {Object}
 */
function merge(/* obj1, obj2, obj3, ... */) {
  var result = {};
  function assignValue(val, key) {
    if (isPlainObject(result[key]) && isPlainObject(val)) {
      result[key] = merge(result[key], val);
    } else if (isPlainObject(val)) {
      result[key] = merge({}, val);
    } else if (isArray(val)) {
      result[key] = val.slice();
    } else {
      result[key] = val;
    }
  }

  for (var i = 0, l = arguments.length; i < l; i++) {
    forEach(arguments[i], assignValue);
  }
  return result;
}

module.exports = {
    isString: isString,
    isUndefined: isUndefined,
    isArray: isArray,
    isObject: isObject,
    isPlainObject: isPlainObject,
    isDate: isDate,
    isFunction: isFunction,
    isFormData: isFormData,
    isArrayBuffer: isArrayBuffer,
    isBuffer: isBuffer,
    isStream: isStream,
    isFile: isFile,
    isBlob: isBlob,
    isArrayBufferView: isArrayBufferView,
    isURLSearchParams: isURLSearchParams,
    trim: trim,
    forEach: forEach,
    extend: extend,
    merge: merge
}
复制代码



至此,本篇文章就写完啦,撒花撒花。

image.png

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。

猜你喜欢

转载自juejin.im/post/7068600259764551710