/ javaScript

axios - 4 - 包装 XMLHttpRequest

console.info

该系类文章旨在研究 axios 的实现 。在研究源码的基础上,去理解 axios 是如何实现 ajax 请求并更好的去使用这个库。

简述

对应文件为 lib/adapters/xhr.js

对应 axios - 1 - 默认配置 中的 default.adapter 一项,主要是包装了浏览器的 XMLHttpRequest 对象,方便调用。

代码分析

代码结构

由于 axios 库内部将原生的 promise 做了兼容处理,所以在代码中,直接使用 promise 对象。

因此,函数调用后直接返回 promise 对应代码如下:

function xhrAdapter(config){
    return new Promise(function (resolve, reject){
        // 处理 XMLHttpRequest 代码
        // ...
    })
}

函数调用时,需要传入的 cofing 对象,在 axios - 2 - 参数字段 中有具体的描述。

声明发送请求对象

由于在 IE8/9 中并没有实现标准的 XMLHttpRequest 对象,实现请求的对象为:XDomainRequest,虽然该对象已经过时,但为了兼容对大多数的浏览器,必须处理下。

对应的代码为:

var request = new XMLHttpRequest();
var loadEvent = 'onreadystatechange'; // 请求变化事件名
var xDomain = false;

if (window.XDomainRequest && !('withCredentials' in request) && !isURLSameOrigin(config.url)) {
    request = new window.XDomainRequest();
    loadEvent = 'onload'; // IE 下的请求发送变化的事件名
    xDomain = true;
    request.onprogress = function handleProgress() {
    };
    request.ontimeout = function handleTimeout() {
    };
}

注: XDomainRequest 对象对应请求发送变化的事件名为 onload,而 XMLHttpRequestonreadystatechange

开启请求

调用 requestopen 方法,开启一个 ajax 请求,这里只是开启请求,并没有发送请求。

request.open(
    config.method.toUpperCase(), 
    buildURL(
        config.url, 
        config.params, 
        config.paramsSerializer
    ), 
    true
);

buildURL 根据当前 config 中的 url & params & paramsSerializer 建立请求链接。

axios 中,实现 buildURL 代码对应的文件为 lib/helper/buildURL.js

绑定事件函数

设置超时时间以及事件函数,包括:onerrorontimeoutonreadystatechange/onload,具体的绑定函数的内容,在最后。

request.timeout = config.timeout;
request[loadEvent] = function handleLoad(){};
request.onerror = function handleError(){};
request.ontimeout = function handleTimeout(){};

确定 xsrf

添加与服务器约定好的 xsrf 头信息,防止跨站脚本攻击。

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

var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ?
    cookies.read(config.xsrfCookieName) :
    undefined;

if (xsrfValue) {
    requestHeaders[config.xsrfHeaderName] = xsrfValue;
}

添加头信息

  1. 由于 XDomainRequest 并没有 setRequestHeader 方法,所以这里需要先判断,避免报错。
  2. XDomainRequest 对象并不需要设置请求头。
if ('setRequestHeader' in request) {
    utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
            // 若没有请求体,则不需要设置 content-type
            delete requestHeaders[key];
        } else {
            // 添加头信息
            request.setRequestHeader(key, val);
        }
    });
}

添加认证以及授权信息

// 确定请求是否需要带凭证发送,和 cookies 的获取有关
if (config.withCredentials) {
    request.withCredentials = true;
}

// 添加用户认证信息
if (config.auth) {
    var username = config.auth.username || '';
    var password = config.auth.password || '';
    requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}

注: btoabase64 加密函数,可用 window.btoa 直接获取,当 window 下没有过该对象时,需要自己实现。

axios 中,实现 btoa 代码对应的文件为 lib/helper/btoa.js

确定响应类型

if (config.responseType) {
    try {
        request.responseType = config.responseType;
    } catch (e) {
        if (request.responseType !== 'json') {
            throw e;
        }
    }
}

添加上传以及下载进度函数

通过该函数可以实现对于文件上传的进度条。

if (typeof config.onDownloadProgress === 'function') {
    request.addEventListener('progress', config.onDownloadProgress);
}

if (typeof config.onUploadProgress === 'function' && request.upload) {
    request.upload.addEventListener('progress', config.onUploadProgress);
}

处理请求取消

if (config.cancelToken) {
    config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
            return;
        }

        // 中断请求
        request.abort();
        reject(cancel);
        // 清空请求,释放内存
        request = null;
    });
}

发送请求

调用一次 send 方法即可。

request.send(requestData);

关于绑定的3个函数的内容

具体的处理过程查看代码中的注释。

  1. onreadystatechange/onload 请求变化时
function handleLoad() {

    // 请求未成功
    if (!request || (request.readyState !== 4 && !xDomain)) {
        return;
    }

    // file 协议的请求,大多数浏览器返回的状态码为 0 ,但是请求是成功的。
    if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        return;
    }

    // 处理 request 获取 response
    var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
    var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
    var response = {
        data: responseData,
        // 正常浏览器的 204 ,但 IE 是 1223
        status: request.status === 1223 ? 204 : request.status,
        statusText: request.status === 1223 ? 'No Content' : request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
    };

    // 处理 response
    settle(resolve, reject, response);

    // 取消 request 释放内存
    request = null;
}

注1: settle 函数调用 config 下的 validateStatus 函数(该函数需要传入 response.status )的返回值来 resolve 或是 reject 当前的 promise

对应于 axios 源码中 lib/core/settle.js 文件,

settle 函数的具体处理过程

function settle(resolve, reject, response) {
    var validateStatus = response.config.validateStatus;
    // 在 XDomainRequest 对象中没有 status
    if (!response.status || !validateStatus || validateStatus(response.status)) {
        resolve(response);
    } else {
        reject(createError(
            'Request failed with status code ' + response.status,
            response.config,
            null,
            response
        ));
    }
}

错误处理在 axios - 5 - 错误处理 中有具体的描述。

  1. onerror/ontimeout 请求出错时
function handleError() {
    // 处理错误信息,reject 当前 promise
    reject(createError('Network Error', config));

    // 取消 request 释放内存
    request = null;
}

function handleTimeout() {
    // 处理错误信息,reject 当前 promise
    reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED'));

    // 取消 request 释放内存
    request = null;
}

一整个 xhr.js 中的内容,就这样了。

总结

该文件规定了一个请求发送器,返回一个 Promise