HTTP系列:AJAX基础梳理、axios基本使用梳理 (juejin.cn) 可以看一下前一篇关于前后端交互的基础知识。本篇文章重点放在前后端交互的封装处理上。
这篇文章的所有代码-github,可以对着完整的代码查看这篇文章更便于理解
前后端数据交互的几种方式汇总
前后端数据交互现在有两种常用的方式, XMLHttpRequest
和 Fetch
,另外一些跨域办法也可以进行交互
XMLHttpRequest
(ajax请求)
$.ajax
:jQuery中的请求库,其基于回调函数方式封装的ajax库- axios :基于Promise管理封装的ajax库
三种方式的使用:
let xhr = new XMLHttpRequest;
xhr.open('GET', 'http://127.0.0.1:9999/user/list');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
};
xhr.send();
$.ajax({
url: 'http://127.0.0.1:9999/user/list',
method: 'GET',
dataType: 'json',
success(data) {
// data就是从服务器获取的结果
console.log(data);
}
});
// 数据请求成功,会让返回的promise实例变为成功
axios.get('http://127.0.0.1:9999/user/list').then(response => {
console.log(response.data);
});
Fetch
Fetch是基于新的通信方案完成客户端和服务器端的数据交互(不是XMLHttpRequest)
- ES6内置类
- 默认是基于Promise管理异步编程
fetch('http://127.0.0.1:9999/user/list', {
method: 'GET'
}).then(response => {
// Response类的实例:text/json/blob/arrayBuffer方法 -> 返回的是新的promise实例「目的是把服务器返回的响应主体信息变为我们想要的格式数据」
return response.json();//把返回的数据变成json格式,并且是Promise
}).then(data => {
console.log(data);
});
then()
中的 response
是 Response
类的实例。其包含一些 text/json/blob/arrayBuffer
等方法。例如 response.json()
时把返回的数据变成 json
格式的对象,并且是Promise实例,然后我们可以继续使用 then
链进行进一步的处理
兼容性没有 XMLHttpRequest
好
某些跨域方案
某些跨域方案也可以实现前后端通信
- proxy/cors(基于
XMLHttpRequest
/Fetch
发送请求,但是可以实现跨域) - jsonp
- postMesage + iframe
axios二次封装
axios基本使用: HTTP系列:AJAX基础梳理、axios基本使用梳理 (juejin。cn)
axios有三种方式去设置配置项
- axios内部配置项,例如默认
method:'GET'
- 通过
axios.defaults
设置的公共配置项,例如axios.defaults.baseURL
- 业务层发送请求时候传递的配置项
axios.get( '',{ params:{ ... })
这三个优先级是递减的,即后面写的会覆盖前面的
对Axios的二次封装目的:把当前项目中,所有请求的公共部分进行统一处理。主要是两方面:
axios.defaults
设置公共的配置项axios.interceptors
基于拦截器做统一处理
首先配置请求接口的统一前缀。在webpack环境里,我们可以根据环境变量的值,设置不同的前缀,来区分不同的环境,例如:
// + 开发 development
// + 测试 test
// + 灰度 grayscale
// + 生产 production
//npm run build/start/test/gray... 设置不同的环境变量
const env = process.env.NODE_ENV || 'development';
switch (env) {
case 'development':
axios.defaults.baseURL = 'http://127.0.0.1:9999';
break;
case 'test':
axios.defaults.baseURL = 'http://168.1.123.1:9999';
break;
case 'production':
axios.defaults.baseURL = 'http://api.zhufengpeixun.cn';
break;
}
使用模块化的方式进行封装。用一个文件来写封装axios的所有代码,然后将其导出。
相关的配置和注意事项都写在注释中
//http.js
axios.defaults.baseURL = 'http://localhost:9999';
// 设置超时时间{10S} & 设置跨域请求中是否携带资源凭证
axios.defaults.timeout = 10000;
axios.defaults.withCredentials = true;//默认允许携带资源凭证(后台有的时不允许携带资源凭证的,就要自己手动设置)
// 配置公共的自定义请求头信息 headers['common']/headers['post/get...']/headers/...
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
// POST系列请求对于请求主体信息的统一格式化
axios.defaults.transformRequest = function (data, headers) {
//如果不是对象,直接返回,例如字符串
if (data === null || typeof data !== "object") return data;
let contentType = headers['Content-Type'] || headers.post['Content-Type'];
//对urlencoded和json格式的body信息进行格式化
if (contentType.includes('urlencoded')) return Qs.stringify(data);
if (contentType.includes('json')) return JSON.stringify(data);
return data;
};
// 设置响应状态码的校验处理{规定服务器返回的状态码哪些是算请求成功,哪些算失败}
axios.defaults.validateStatus = function (status) {
return status >= 200 && status < 400;
};
// 请求拦截器,当所有配置处理完,在向服务器发送请求之前,我们拦截到现有的配置,再去做一些统一修改
axios.interceptors.request.use(function (config) {
// 例如:传递Token
/* const token = sessionStorage.getItem('token');
if (token) {
config.headers['Authorization'] = token;
} */
return config;
});
// 响应拦截器,当前请求有结果之后,我们在业务层自己调用then/catch方法之间拦截一下,这样可以做一些成功或者失败的统一提示处理等...
axios.interceptors.response.use(function onfulfilled(response) {
// 成功:服务器正常返回结果 & validateStatus状态码校验成功
//这里可以做一些与本公司业务有关的判断,例如错误的统一提示等
//。。。
return response.data;
}, function onrejected(reason) {
// 失败:@1服务器返回了结果但是状态码没有经过validateStatus校验 || @2服务器压根没有返回任何的结果 || @3请求中断或者超时...
let response = reason.response;
if (response) {
// @1
switch (response.status) {
case 401:
break;
// ...
}
} else {
if (reason && reason.code === 'ECONNABORTED') {
// @3
}
if (!navigator.onLine) {
// @2
}
}
return Promise.reject(reason);//保证promise到业务层还是失败的
});
export default axios;
测试使用:
<!--index。html-->
<!--这里不是webpack环境,得加上完整的路径-->
<script src="node_modules/axios/dist/axios.min.js"></script>
<script src="node_modules/qs/dist/qs.js"></script>
<!-- type="module" 可以在JS中使用ES6Module模块管理规范{import & export} -->
<script type="module" src="index.js"></script>
这里index.js type
为 'module'
是为了可以使用import语法导入基于 export
/ export default
导出的模块
//index.js
import axios from './http.js';
axios.get('/user/list').then(data => {
console.log('axios1',data);
});
axios.get('/home',{//自己进行一些配置
baseURL:'http://127.0.0.1:8888',
withCredentials:false//有的后台不允许携带资源凭证
}).then(data => {
console.log('axios2',data);
});
axios.post('/user/login', {
account: '137000000',
password: '11111111'
}).then(data => {
console.log(data);
});
拦截器的原理:
相当于 axios.get('/user/list').then(data => {});
在这个 then
前面 then
执行 .then(onfufilled,onrejected)
,里面的两个方法就是拦截器传入的方法,先走拦截器里的两个方法,再走自己写的 then
方法
对业务层的封装处理
上面那样封装还不够,如果业务层有一些共同的逻辑需要处理,那么还时需要再对业务层进行一层封装,对业务层的成功和失败或者特殊要求进行统一的提示和处理
//request.js
/* 对业务层的一些处理 */
import axios from './http.js';
const handle = function handle(data) {
let code = +data.code;//公司内部本身自己进行的规定
if (code === 0) return data;
// 业务层失败:也可以做一些统一提示或者处理
// ...
return Promise.reject(data.codeText);
};
const requestGET = function requestGET(url, options) {
return axios.get(url, options).then(handle);
};
const requestPOST = function requestPOST(url, data, options) {
return axios.post(url, data, options).then(handle);
};
export default {
requestGET,
requestPOST
};
多个请求配置处理封装成不同的实例
如果一个项目中100个请求90多个都是统一的,只有极个别几个是不一样的,那么我们只需要发送请求时候单独改一些配置即可。
如果一个项目中100个请求60多个都是一个配置,另外40多个是其他配置,那么这种情况应该怎么做呢?
我们可以创建axios的多个实例,让每个实例都拥有不同的配置,例如:
//http_file.js
import axios from "./http.js";
// 创建一个和axios类似的相同实例instance
const instance = axios.create();
instance.defaults.baseURL = '';
// ...
export default instance;
这样用到其他配置的axios就可以单独写了,不会和公共配置项冲突。
fetch二次封装
具体的逻辑写在了注释中
//对Fetch的封装:让其支持params/请求主体的格式化/请求地址的公共前缀
/* const env = process.env.NODE_ENV || 'development',
baseURL = '';
switch (env) {
case 'development':
baseURL = 'http://127.0.0.1:9999';
break;
case 'test':
baseURL = 'http://168.1.123.1:9999';
break;
case 'production':
baseURL = 'http://api.zhufengpeixun.cn';
break;
} */
// 公用前缀 & 默认配置
let baseURL = 'http://127.0.0.1:9999',
inital = {
method: 'GET',
params: null,
body: null,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
credentials: true,
responseType: 'JSON',
cache: 'no-cache'
};
// 校验是否为纯粹的对象
const isPlainObject = function isPlainObject(obj) {
var proto, Ctor;
if (!obj || typeof obj !== "object") return false;
proto = Object.getPrototypeOf(obj);
if (!proto) return true;
Ctor = proto.hasOwnProperty('constructor') && proto.constructor;
return typeof Ctor === "function" && Ctor === Object;//构造函数是Object
};
// 发送数据请求
const request = function request(url, config) {
// 合并配置项{不要去更改inital中的内容}
(config == null || typeof config !== "object") ? config = {}: null;//确保config肯定是对象
if (config.headers && isPlainObject(config.headers)) {
// 单独的给HEADERS先进行深度合并
config.headers = Object.assign({}, inital.headers, config.headers);
}
let {
method,
params,
body,
headers,
credentials,
responseType,
cache
} = Object.assign({}, inital, config);//和饼config
// 处理URL{格式校验 & 公共前缀 & 拼接params中的信息到URL的末尾}
if (typeof url !== "string") throw new TypeError( ` ${url} is not an string! ` );
if (!/^http(s?):///i.test(url)) url = baseURL + url;//判断是不是以http或者https开头,如果不是,就用baseurl拼起来
if (params != null) {//不是null和undefined,存在params
if (isPlainObject(params)) {
params = Qs.stringify(params);
}
url += ` ${url.includes('?')?'&':'?'}${params} ` ;//拼接
}
// 处理请求主体的数据格式{根据headers中的Content-Type处理成为指定的格式}
if (body != null) {
if (isPlainObject(body)) {
let contentType = headers['Content-Type'] || 'application/json';//默认application/json
if (contentType.includes('urlencoded')) body = Qs.stringify(body);
if (contentType.includes('json')) body = JSON.stringify(body);
}
}
// 处理credentials{如果传递的是true,我们让其为include,否则是same-origin}
//include,允许跨域请求当中携带资源凭证,same-origin,允许同源性请求当中携带资源凭证
credentials = credentials ? 'include' : 'same-origin';
// 基于fetch请求数据
method = method.toUpperCase();
responseType = responseType.toUpperCase();
config = {
method,
credentials,
cache,
headers
};
/^(POST|PUT|PATCH)$/i.test(method) ? config.body = body : null;
return fetch(url, config).then(function onfulfilled(response) {
// 走到这边不一定是成功的:
// Fetch的特点的是,只要服务器有返回结果,不论状态码是多少,它都认为是成功
let {
status,
statusText
} = response;
if (status >= 200 && status < 400) {
// 真正成功获取数据
let result;
switch (responseType) {
case 'TEXT':
result = response.text();
break;
case 'JSON':
result = response.json();
break;
case 'BLOB':
result = response.blob();
break;
case 'ARRAYBUFFER':
result = response.arrayBuffer();
break;
}
return result;
}
// 应该是失败的处理
return Promise.reject({
code: 'STATUS ERROR',
status,
statusText
});
}).catch(function onrejected(reason) {
// @1:状态码失败
if (reason && reason.code === "STATUS ERROR") {
switch (reason.status) {
case 401:
break;
// ...
}
}
// @2:断网
if (!navigator.onLine) {
// ...
}
// @3:处理返回数据格式失败
// ...
return Promise.reject(reason);
});
};
export default request;
测试
//fetch
//get
request('/user/list').then(data => {
console.log(data);
});
//post
request('/user/login', {
method: 'POST',
body: {
account: '137000000',
password: '1234567890'
}
}).then(data => {
console.log(data);
});
这篇文章的所有代码-github