夫君子之行,静以修身,俭以养德。 ——诸葛亮
foreword
前端需要反复温故,以及敢于尝试新的技术。jQuery,Vue,React 用了好几年,我们习惯性使用ajax库进行一层简易的封装,然后向后台请求数据,而事实接触原生js请求并不是很多,所以手写了ajax jsonp fetch的封装,用来温故一下以前的知识.
一. 使用XMLHttpRequest 实现ajax请求
- 注明:暂时不考虑IE5和IE6 ,所以没有做小的兼容性处理
code
const isFunction = fn => typeof fn === 'function';
const ajaxHttp = (url, option = {}) => {
if (!XMLHttpRequest) {
throw Error('您的浏览器版本过低,请选择新版本的浏览器');
}
let {
data = {},
method = 'GET',
// 是否异步 @async
async = true,
timeout = 0,
header = {},
ontimeout = () => {},
onerror = () => {},
onprogressFn
} = option;
method = method.toUpperCase();
const isGet = method === 'GET';
if (isGet) {
// format str 拼接
const str = Object.entries(data)
.join('&')
.split(',')
.join('=');
url = url.includes('?') ? `${url}&${str}` : `${url}${str ? '?' + str : ''}`;
}
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, async);
xhr.timeout = timeout;
// 设置请求头 open方法之后 send方法之前调用
xhr.setRequestHeader('Content-Type', 'application/json');
Object.entries(header).forEach(item => {
xhr.setRequestHeader(item[0], item[1]);
});
// 错误回调
isFunction(onerror) ? (xhr.onerror = onerror) : '';
// 超时回调
isFunction(ontimeout) ? (xhr.ontimeout = ontimeout) : '';
// 判断onprogress是否支持 如果支持写入回调 这里一般用来获取上传进度的
if ('onprogress' in xhr.upload && isFunction(onprogressFn)) {
xhr.upload.onprogress = onprogressFn;
}
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
try {
resolve(JSON.parse(this.responseText));
} catch (error) {
resolve(this.responseText);
}
} else {
const resJson = {
code: this.status,
response: this.response
};
reject(resJson);
}
}
};
xhr.send(method === 'GET' ? null : JSON.stringify(data));
});
};
const creaetAjax = type => {
ajaxHttp[type] = (url, options) =>
ajaxHttp(url, {
...options,
method: type
});
};
const methods = ['get', 'GET', 'POST', 'post', 'put', 'PUT', 'DELETE', 'delete'];
// 写入各个方法,可以使用ajaxHttp.get 等调用
methods.forEach(type => creaetAjax(type));
- sync 注释
默认值为true,即为异步请求,若async=false,则为同步请求
当xhr为同步请求时,有如下限制:- xhr必须为 0
- xhr.withCredentials必须为false
- xhr.responseType 必须为"" (注意置为"text"也不允许)
- 如果上面任何一个条件不满足,都会报错,所以我们一般要避免sync请求.
参考文档:你真的会使用XMLHttpRequest吗?
二.对原生fetch进行简易的封装
- code
const errStatusReg = /[4,5]\d{2}/;
const fetchHttp = (url,options={})=>{
let {
method="GET",
headers={},
data={},
mode="cors",
credentials = "omit",
redirect = "manual",
cache = "default"
} = options;
method = method.toUpperCase();
const isGet = method === 'GET';
if(isGet){
// format str 拼接
const str = Object.entries(data).join('&').split(',').join('=');
url = url.includes("?")?`${url}&${str}`:`${url}${str?'?'+ str:''}`;
};
return fetch(url,{
method,
// 请求的 body 信息:可能是一个 Blob、BufferSource、FormData、URLSearchParams
// 或者 USVString 对象。注意 GET 或 HEAD 方法的请求不能包含 body 信息。
body:isGet?null:JSON.stringify(data),
// http 请求头
headers,
// 请求模式 @注释1
mode,
// 表达的含义请求是否携带cookie @注释2
credentials,
// 重定向 模式 @注释3
redirect,
// 请求的缓存模式 @注释4
cache
}).then(data=>{
if (errStatusReg.test(data.status))throw Error(`${data.status}:${data.statusText}`);
return data.json();
})
};
const creaetFetch = (type)=>{
fetchHttp[type] = (url,options)=>fetchHttp(url,{...options,method:type})
};
const methods = ["get","GET",'POST',"post","put","PUT","DELETE","delete"];
methods.forEach(type => creaetFetch(type));
注释解析
@1 请求模式 mode
常用的 mode 属性值:
1. same-origin: 表示只请求同域.如果你在该 mode 下进行的是跨域的请求的话, 那么就会报错.
2. no-cors: 正常的网络请求, 主要应对于没有后台没有设置 Access-Control-Allow-Origin.话句话说, 就是用来处理 script, image 等的请求的.他是 mode 的默认值.
3. cors: 用来发送跨域的请求.在发送请求时, 需要带上.
4. cors-with-forced-preflight: 这是专门针对 xhr2 支持出来的 preflight(多发一个options请求), 会事先多发一次请求给 server, 检查该次请求的合法性。-
@2 credentials(凭证)
- omit: 默认值, 忽略cookie的发送
- same-origin: 表示cookie只能同域发送, 不能跨域发送
- include: cookie既可以同域发送, 也可以跨域发送
@3 redirect
可用的 redirect 模式: follow(自动重定向), error(如果产生重定向将自动终止并且抛出一个错误), 或者 manual(手动处理重定向).在Chrome中, Chrome 47 之前的默认值是 follow, 从 Chrome 47 开始是 manual。-
@4 cache
- default :表示fetch请求之前将检查下http的缓存
- no-store: 表示fetch请求将完全忽略http缓存的存在, 这意味着请求之前将不再检查下http的缓存, 拿到响应后, 它也不会更新http缓存
- no-cache: 如果存在缓存, 那么fetch将发送一个条件查询request和一个正常的request, 拿到响应后, 它会更新http缓存
- reload:表示fetch请求之前将忽略http缓存的存在, 但是请求拿到响应后, 它将主动更新http缓存)
- force-cache: 表示fetch请求不顾一切的依赖缓存, 即使缓存过期了, 它依然从缓存中读取.除非没有任何缓存, 那么它将发送一个正常的request
- only-if-cached: 表示fetch请求不顾一切的依赖缓存, 即使缓存过期了, 它依然从缓存中读取.如果没有缓存, 它将抛出网络错误(该设置只在mode为”same-origin” 时有效).
注明:fetch这块原生就存在了很友好的API,这块可以参考MDN官方文档 fetch
三.jsonp封装
- code
const formatData = data => {
// 这里要先处理特殊情况的字符 & = 等等
return Object.entries(data)
.map(item => [encodeURIComponent(item[0]), item[1]])
.join('&')
.split(',')
.join('=');
};
/**
*
*
* @param {*} url 传入的请求url
* @param {*} options 包含两个参数 1. get请求的参数对象data,以及和后台协商好的回调函数名称
* @returns
*/
const jsonpHttp = (url, options) => {
options = options || {};
options.data = options.data || {};
const headDOM = document.querySelector('head');
const scriptDOM = document.createElement('script');
// 这里传入的是url所以存在一些特殊的操作
const str = formatData(options.data);
return new Promise((resolve, reject) => {
const CB = jsonoResData => {
window[options[`jsonpCB`]] = null;
headDOM.removeChild(scriptDOM);
clearTimeout(scriptDOM.timer);
resolve(jsonoResData);
};
window[options[`jsonpCB`]] = CB;
// 超时处理
if (options.timeout) {
scriptDOM.timer = setTimeout(() => {
headDOM.removeChild(scriptDOM);
window[options[`jsonpCB`]] = null;
reject({
timeout: true,
message: '请求超时'
});
}, options.timeout * 1000);
}
const urlFormat = url.includes('?') ? `${url}&${str}` : `${url}${str ? '?' + str : ''}`;
// 多加一个jsonp的回调函数名称
scriptDOM.src = `${urlFormat}&jsonpCB=${options[`jsonpCB`]}`;
headDOM.appendChild(scriptDOM);
});
};