手写一个简单的axios库

1,120 阅读7分钟

前言

AJAX 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术,通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。

但是如果多个ajax请求有依赖关系的话,就会形成回调地狱。

axios是通过promise实现对ajax技术的一种封装,就像jQuery实现ajax封装一样。

简单来说: ajax技术实现了网页的局部数据刷新,axios实现了对ajax的封装。

axios的应用

语法:

axios([config])
axios(url,[config])
axios.get/delete/head/options(url,[config])
axios.post/put/patch(url,[data],[config]

后面三种都是快捷写法,指定了 请求地址/请求方式/请求主体内容 这些东西,config无需再次配置了,最后处理的方案还是第一种。

axios({
  url: '/getUsers',
  method: 'get',
  responseType: 'json', // 默认的
  data: {
      //'a': 1,
      //'b': 2,
  }
}).then(function (response) {
  console.log(response);
  console.log(response.data);
}).catch(function (error) {
  console.log(error);
})


axios.get('/user/login', {
    baseURL: 'http://127.0.0.1:8888'
}).then(response => {
    console.log(response.request);
    return response.data;
}).then(data => {
    console.log('响应主体信息:', data);
});


axios.post('/user/login', {
    // 默认会把对象变为JSON字符串传递给服务器 {"account":"zhufengpeixun","password":"xxxxxx"}
    account: 'zhufengpeixun',
    password: 'xxxxxx'
}, {
    baseURL: 'http://127.0.0.1:8888',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
    },
    transformRequest: [function (data) {
        return Qs.stringify(data);
    }]
});

返回的结果都是一个promise实例。

  1. 成功:从服务器获取到结果,并且HTTP状态码是以2开始的;
  2. 失败:
    • 从服务获取到数据了,但是HTTP状态码不是以2开始的;
    • 没有从服务器获取到数据。

返回结果,response 对象:

status:状态码
statusText:状态文本
request:基于new XMLHttpRequest创建的xhr对象
headers:响应头信息
data:响应主体信息
config:发送请求的时候传递的配置信息
request:基于new XMLHttpRequest创建的xhr对象
isAxiosError:true/false 能从服务器获取到结果,只不过状态码不是以2开头的
response:等同于成功返回的response对象,如果没有从服务器获取任何的结果,response是不存在的     

配置信息 config

url:''
method:'get' 
baseURL:''  向服务器发送请求的地址是由baseURL+url完成的
transformRequest:[function(data,headers){  
   ...
   return data; 
}] 针对于POST系列请求,请求主体传递的信息进行格式化处理,发生在发送ajax请求之前
transformResponse:[function (data) {
   ...
   return data;
}] 针对于服务器响应主体中的信息,进行的格式化处理,发生在自己.then/catch之前
headers:{}  设置自定义请求头信息
params:{}  GET系列请求问号传递参数的信息(键值对方式存储,也可以是URLSearchParams对象),axios内部默认你是基于paramsSerializer方法中的Qs.stringify方法把params对象变为xxx=xxx&xxx=xxx格式的
data:{} 请求主体传递信息的对象
timeout:0 设置请求超时时间  在这么久的时间内还没有完成数据请求,则触发xhr.ontimeout事件
withCredentials:false 设置定在跨域请求中是否允许携带资源凭证(例如:cookie)
responseType:'json'  axios内部会把服务器返回的信息转换为指定格式的数据,支持:'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
onUploadProgress/onDownloadProgress:function(progressEvent){ ... } 监听上传或者下载的进度,用的是xhr.upload.onprogress事件
validateStatus:function (status) {
   return status >= 200 && status < 300;
}

真实项目中,大部分post请求,基于请求主体传递给服务器的格式,不期望是默认的json格式字符串,而是需要改为服务器要求的格式,例如:x-www-form-urlencoded,则需要我们统一基于transformRequest处理。

axios.defaults.validateStatus = status => {
    // status是才服务器获取的HTTP状态码
    return status >= 200 && status < 300;
}; 

axios.defaults.transformRequest = [data => {
    // data是基于请求主体传递给服务器的信息,transformRequest只对post系列请求有用
    // 返回的是啥,最后基于请求主体传递给服务器的就是啥
    return Qs.stringify(data);
}];

axios.defaults.transformResponse = [data => {
    // data是从服务器获取的响应主体信息,并且根据responseType的值,格式化处理过了
    // 返回的是啥,以后基于.then获取的response.data就是啥
    try {
        data = JSON.parse(data);
    } catch (err) {}
    return data;
}]

//还有其他默认配置
axios.defaults.baseURL = 'http://127.0.0.1:8888';
axios.defaults.withCredentials = true;
axios.defaults.headers['Content-Type'] = 'application/x-www-form-urlencoded';
// axios.defaults.headers.common/post/get/delete/...
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';

前后端数据通信的数据格式

前后端数据通信的数据格式(POST->请求主体  GET->URL问号传递参数):
   form-data  MIME:multipart/form-data
     表单提交
     文件上传:文件流信息放置在formData中
   x-www-form-urlencoded  MIME:application/x-www-form-urlencoded
     普通数据的传输一般都用这种方式(这样GETPOST传递给服务器的数据格式统一了)
     'xxx=xxx&xxx=xxx'
     GET系列请求,URL传递的参数信息其实就是这种格式
   raw 原始格式
     json字符串  MIME:application/json  服务器返回给客户端的数据一般也都是json格式字符串
     text普通字符串  MIME:text/plain
     xml字符串 MIME:application/xml
     ...
   binary 文件流
     根据上传的文件不同,传递的MIME也是不一样的  例如:image/jpeg 或者 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
     ...

拦截器:axios.interceptors

请求拦截器:axios.interceptors.request

发生在axios内部帮我们整理好配置项,在发送给服务器之前,请求拦截器一般是对配置项的处理

axios.interceptors.request.use(config => {
    // config整理好的配置项,返回全新的config
    // 实战场景,例如:每一次向服务器发送请求的时候,可能需要传递一个Token来校验身份(放在请求头中)
    let token = localStorage.getItem('token');
    if (token) {
        // X-Token 服务器要求的名字 ,还可能会叫做Authorization...
        config.headers['X-Token'] = token;
    }
    return config;
});

响应拦截器:axios.interceptors.response

从服务器获取到信息后(或者知道结果,哪怕是失败),到我们自己执行.then/.catch之间触发的

axios.interceptors.response.use(response => {
    // 成功:根据validateStatus处理的
    // 实际开发中,response包含的信息太多了,但是到业务逻辑层,往往只需要响应主体的信息
    return response.data;
}, reason => {
    // 失败:获取数据了,但是状态码不对,或者是没有获取任何的数据...
    // 实际开发中,不论哪一个请求失败,基本上的提示信息或者处理方案是一致的,此时我们在响应拦截器中对错误进行统一的处理
    let response = reason.response;
    if (response) {
        // 从服务器获取到数据了,只是状态码不对,根据不同的状态码,做不同的提示即可
        switch (response.status) {
            case 400:
                break;
            case 401:
                break;
            case 404:
                break;
        }
    } else {
        // 数据都没有获取到
        if (!navigator.onLine) {
            // 断网了
        }
    }
    return Promise.reject(reason);
});

真实项目配置

真实项目中,根据业务场景上的一写统一情况,还可以封装一个get/post公共方法,方法中往往夹杂着业务的一些统一逻辑,以后基于自己封装的方法发送请求。

function api_get(url, params) {
    // ... 自己根据业务逻辑的统一处理
    return axios.get(url, {
        params
    }).then(data => {
        if (data.code == 0) {
            // 业务成功
            return data;
        }
        // 业务失败
        return Promise.reject(data);
    });
}

function api_post(url, data) {
    // ...
    return axios.post(url, data);
}

使用:

axios.get('/user/list').then(data => {
    // 此处拿到的直接是基于拦截器处理后的“响应主体”信息
    console.log(data);
}).catch(reason => {

});

手写一个简单的axios库

手写的axios虽然没有实现原版axios的全部功能,但是基本符合axios用promise管理ajax的思想。

(function () {
	/* 校验是否为浏览器环境 */
	if (typeof window === "undefined" || window.window !== window) {
		throw new Error('The current environment is not a browser environment, please ensure the correctness of the environment!');
	}

	/* 创建AJAX请求类 */
	class MyAJAX {
		constructor(options = {}) {
			this.options = options;
			this.isGET = /^(GET|HEAD|DELETE|OPTIONS)$/i.test(options.method);
			return this.init();
		}
		init() {
			// 请求拦截器是重构CONFIG配置项
			let interceptorsRQ = _ajax.interceptors.request[0],
				interceptorsRS = _ajax.interceptors.response;
			if (typeof interceptorsRQ === "function") {
				this.options = interceptorsRQ(this.options);
			}

			let {
				method,
				headers,
				timeout,
				withCredentials,
				responseType,
				validateStatus
			} = this.options;

			return new Promise((resolve, reject) => {
				let xhr = new XMLHttpRequest;
				xhr.open(method, this.handleURL());

				// 设置自定义请求头信息
				Object.keys(headers).forEach(key => {
					xhr.setRequestHeader(key, headers[key]);
				});

				// 其它的配置信息
				xhr.withCredentials = withCredentials;
				timeout > 0 ? xhr.timeout = timeout : null;

				xhr.onreadystatechange = () => {
					// 校验网络状态码
					let status = xhr.status,
						success = status === 200 ? true : false;
					typeof validateStatus === "function" ? success = validateStatus(status) : null;

					// 网络状态码层面上是成功的
					if (success) {
						if (xhr.readyState === 4) {
							resolve(this.queryInfo(0, xhr));
						}
						return;
					}

					// 失败的
					reject(this.queryInfo(1, xhr));
				};

				// 请求超时或者请求中断都算失败的
				xhr.ontimeout = () => {
					reject(this.queryInfo(1, xhr));
				};
				xhr.onabort = () => {
					reject(this.queryInfo(1, xhr));
				};

				xhr.send(this.handleBODY());
			}).then(...interceptorsRS);
		}
		
		// 解析URL
		handleURL() {
			let {
				url,
				baseURL,
				params
			} = this.options;
			url = baseURL + url;
			// GET请求需要把PARAMS拼接到URL的末尾(和AXIOS不一样的地方,POST请求下我们不处理PARAMS【相对比较标准的】)
			if (this.isGET && params) {
				if (typeof params === "object") {
					// 如果是一个对象,变为URLENCODED格式
					let str = ``;
					Object.keys(params).forEach(key => {
						// 我们不对某一项是对象做处理
						str += `&${key}=${params[key]}`;
					});
					params = str.substring(1);
				}
				url += `${url.includes('?')?'&':'?'}${params}`;
			}
			return url;
		}
		
		// 解析请求主体
		handleBODY() {
			let {
				data,
				transformRequest
			} = this.options;
			// POST请求下才对DATA做处理
			if (!this.isGET && data) {
				if (typeof transformRequest === "function") {
					data = transformRequest(data);
				}
				typeof data === "object" ? data = JSON.stringify(data) : null;
				return data;
			}
			return null;
		}
		
		// 获取失败/成功信息
		queryInfo(lx, xhr) {
			let {
				responseType
			} = this.options;

			let response = {
				status: xhr.status,
				statusText: xhr.statusText,
				data: {},
				headers: {},
				config: this.options,
				request: xhr
			};

			// 获取响应头信息
			let allHeaders = xhr.getAllResponseHeaders();
			allHeaders = allHeaders.split(/(?:\n)/);
			allHeaders.forEach(item => {
				if (!item) return;
				let [key, value] = item.split(': ');
				response.headers[key] = value;
			});

			if (lx === 0) {
				// 把从服务器获取的结果变为responseType指定的数据格式
				switch (responseType.toLowerCase()) {
					case 'json':
						response.data = JSON.parse(xhr.responseText);
						break;
					case 'text':
						response.data = xhr.responseText;
						break;
					case 'document':
						response.data = xhr.responseXML;
						break;
					case 'arraybuffer':
					case 'blob':
					case 'stream':
						response.data = xhr.response;
						break;
					default:
						response.data = xhr.responseText;
				}
				return response;
			}

			// 失败
			return {
				config: this.options,
				request: xhr,
				message: xhr.responseText,
				response
			};
		}
	}

	/* 提供供外面调用的_ajax方法 */
	// 把用户传递的配置项和默认配置项进行合并处理
	function _initDefaults(options) {
		// HEADERS的特殊处理
		let headers = options.headers;
		if (headers && typeof headers === "object") {
			_ajax.defaults.headers = Object.assign(
				_ajax.defaults.headers,
				headers
			);
			delete options.headers;
		}
		return Object.assign(_ajax.defaults, options);
	}

	function _ajax(options = {}) {
		options = _initDefaults(options);
		return new MyAJAX(options);
	}

	// 默认的配置项
	_ajax.defaults = {
		url: '',
		baseURL: '',
		method: 'get',
		// GET系列请求传递参数
		params: {},
		// POST系列请求:配置默认传递给服务器的数据格式是JSON格式
		data: {},
		transformRequest: data => {
			if (data !== null && typeof data === "object") {
				return JSON.stringify(data);
			}
			return data;
		},
		// 自定义请求头信息
		headers: {
			"Content-Type": "application/json;charset=UTF-8"
		},
		timeout: 0,
		withCredentials: false,
		responseType: 'json',
		validateStatus: status => (status >= 200 && status < 300)
	};

	// 还需要提供一些快捷方法 _ajax.get/post/all/interceptors...
	["get", "delete", "head", "options"].forEach(name => {
		_ajax[name] = function (url, options = {}) {
			options.method = name;
			options.url = url;
			options = _initDefaults(options);
			return new MyAJAX(options);
		};
	});

	["post", "put"].forEach(name => {
		_ajax[name] = function (url, data = {}, options = {}) {
			options.method = name;
			options.url = url;
			options.data = data;
			options = _initDefaults(options);
			return new MyAJAX(options);
		};
	});

	// all方法
	_ajax.all = function all(arr = []) {
		return Promise.all(arr);
	};

	// 拦截器
	function _use(...funcs) {
		// this : request/response
		// 如果没有传递函数(成功/失败处理),我们给一个默认值
		function interceptorsA(response) {
			// response:对于请求拦截器来讲就是config
			return response;
		}

		function interceptorsB(reason) {
			return Promise.reject(reason);
		}

		funcs.length === 0 ? funcs = [interceptorsA, interceptorsB] : null;
		funcs.length === 1 ? funcs = [funcs[0], interceptorsB] : null;

		this.push(...funcs);
	}
	
	_ajax.interceptors = {
		request: [],
		response: []
	};
	
	_ajax.interceptors.request.use = _use;
	_ajax.interceptors.response.use = _use;


	// 暴露到全局供其使用
	window._ajax = _ajax;

	// 想让其支持ES6Module模块导入(基于CommonJS导出,也可以基于ES6Module导入)
	if (typeof module === "object" && typeof module.exports === "object") {
		module.exports = _ajax;
	}
})();