小知识,大挑战!本文正在参与“ 程序员必备小知识 ”创作活动
本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金
前言
大家封装axios是否都是通过拦截服务呢?处理时,大多从以下问题出发吧,具体代码可点击前往。
1、统一处理请求头headers信息,如口令(token、Authorization)信息
2、重复接口防止提交,如采用loading、防抖处理等
3、请求入参统一,post、put、facth和get等方式
4、对响应数据状态处理,如登录拦截
5、对响应数据信息(错误)处理
6、其他:对文件请求处理...
- 实现方式
// 添加请求拦截器
axios.interceptors.request.use(function(config){
// ... 在发送请求前,统一做点什么,如请求头配置、token信息等处理
return config;
},function(error){
// ... 对请求错误统一做点什么
return Promise.reject(error)
})
// 添加响应拦截器
axios.interceptors.response(function(response){
// ... 对响应数据做点什么
return response;
},function(error){
// ... 对错误数据统一做点什么
return Promise.reject(error)
})
export default axios;
如何实现跨平台接口封装,支持后端多种微服务对对接
封装Api服务,解决以下问题
1、支持跨平台,可直接复用于多平台,如移动端、PC端、小程序;
2、支持多服务,后端基本不会只有一个服务,一般大点公司,都是 网关+N服务方式(核心);
3、集中配置、解耦、可移植,简化开发流程,开发体验也是杠杠的。
结构目录
services
apis ---接口配置列表
base ---单一工具
axiosAjax.js ---ajax封装
jsonpAjax.js ---jsonp封装
config.js ---基本配置
couplingAjax.js ---ajax数据统一处理
index.js ---导出出口
代码使用
- apis/BaseServer/index.js文件
export default function BaseServer(ajax, config) {
return {
// 根据城市名称模糊搜索
queryList: opt =>
ajax({
url: "/sale/list",
method: "get",
...opt
})
};
}
- 在业务组件中引入
import Api from "./services";
,得到的一定是正确的数据
// 前提 import Api from "./services";
const opt = {
data: {
// ...
pageNum: 1,
pageSize: 15
},
loading: true
// success: true
}
Api.BaseServer.queryList(opt).then(res => {
console.log("拿到一定是正确的数据", res);
});
*Ajax请求配置以及请求后数据统一处理配置,如上queryList(opt)
方法传入opt对象配置如下:
注:下边的配置项,适当的把部分配置放apis/BaseServer/index.js文件
配置,部分存放到业务组件中。
配置项 | 说明 | 是否必填 | 类型 | 默认值 |
---|---|---|---|---|
url | 请求Api | 是 | string | |
loading | 加载拦截,全屏 | 否 | boolean | false |
baseURL | 基础路径 | 否 | string | |
data | 请求发送数据 | 否 | object | |
params | 地址栏拼接数据,仅限于'put', 'post', 'patch' | 否 | object | |
timeout | 超时时间 | 否 | number | 30 * 1000 |
method | 请求方法:get、post、put、patch、jsonp | 否 | string | get |
headers | 请求头 | 否 | object | { "Content-Type": "application/json" } |
success | 请求成功时,是否提示 | 否 | boolean | false |
error | 请求失败时,是否提示 | 否 | boolean | true |
jsonp | 是否使用jsonp请求接口 | 否 | boolean | false |
jsonpOpt | jsonp库的options参数,配合jsonp使用 | 否 | object | false |
file | 是否为文件模式 | 否 | boolean | false |
mock | 是否为mock模式 | 否 | boolean | false |
responseType | 数据格式 | 否 | string | json |
isResponse | 是否简化数据 | 否 | boolean | false |
reLogin | 是否校验登录 | 否 | boolean | true |
代码开发
- 基本配置
// 接口和页面初始化配置中心
// 在前置配置之前,需要搞清楚后端微服务前缀路由是什么,然后再配置到该文件下面
const gateway = "";
let service = {
domainName: "", // 主域名
gateway, // 流量网关前缀,后面的才是微服务后端代码前缀
BaseServer: gateway + "/order" // 公共服务
};
console.log("当前环境", process.env.VUE_APP_NODE_ENV);
switch (process.env.VUE_APP_NODE_ENV) {
// 当走淘宝mock的情况
case "rapmock": {
service = {
...service
};
break;
}
// 开发, 本地开发走vue代理
case "development": {
service = {
...service,
domainName: ""
};
break;
}
// 测试环境
case "staging": {
service = {
...service,
domainName: ""
};
break;
}
// 生产
case "production": {
service = {
...service,
domainName: ""
};
break;
}
}
export default service;
- services/index.js
import { BaseApi } from "./couplingAjax";
import config from "./config";
import BaseServer from "./apis/BaseServer";
const baseServer = opt => BaseApi(opt, { prefix: config.BaseServer });
export default {
BaseServer: BaseServer(baseServer, config)
};
- couplingAjax.js文件
请求头信息、登录校验、响应数据都可在本文件中自行配置
import { ajax } from "./base/axiosAjax";
import config from "./config";
import Tips from "./base/tips";
// import { loginOut } from '@/services/tool/LoginSet'
// import vuex from "@/store/index";
import qs from "qs";
import { debounce } from "@/utils/antiShakingAndThrottling";
// 口令封装处理
const handlerToken = (header = {}) => {
const token = "token-test"; // vuex.getters.token;
if (!token) return header;
header["Authorization"] = token;
return header;
};
// 401退出登录
const signOut = debounce(() => {
// loginOut()
Tips.error({ msg: "用户登录失效,将重新登录", title: "错误" });
}, 1000);
// 处理opt传入参数
const handlerData = (opt, apiBase = {}) => {
const { prefix } = apiBase;
opt.baseURL = opt.baseURL ? opt.baseURL : config.domainName;
opt.url = prefix + opt.url;
opt.method = opt.method ?? "get";
opt.data = opt.data ?? {};
opt.headers = opt.headers ?? { "Content-Type": "application/json" }; // 设置默认headers
opt.headers = handlerToken(opt.headers);
opt.file = opt.file ?? false; // 是否为文件模式,文件下载模式为后端直接下载文件,不做处理判断
opt.mock = opt.mock ?? process.env.VUE_APP_NODE_ENV === "rapmock"; // 是否为mock模式
// opt.responseType = opt.responseType ?? (opt.mock ? 'json' : 'text') // 细节需要加括号,上环境情况下后端返回的数据是base64字符串
opt.responseType = opt.responseType ?? "json";
opt.isResponse = opt.isResponse ?? false; // 是否直接获取response数据,避免因为简化data数据获取导致无法获取完整数据情况
opt.reLogin = opt.reLogin ?? true; // 是否判断401状态跳转到登录页面
return opt;
};
// 错误信息
const handlerErrorMessage = (error, message, tipsCode) => {
error &&
Tips.error({
msg: error !== true ? error : message ?? "系统异常,请稍后重试!",
tipsCode
});
};
// 成功信息
const handlerSuccessMessage = (success, message, tipsCode = "") => {
success &&
Tips.success({
msg: success !== true ? success : message ?? "成功",
tipsCode
});
};
// 业务接口
async function BaseApi(
opt = {},
{
prefix = "",
codeField = "code",
// dataField = "data",
codeNum = 200,
msgField = "msg",
tipsCode = "code"
}
) {
opt = handlerData(opt, { prefix }); // 参数预处理
const error = opt.error ?? true; // 默认,提示错误信息
const success = opt.success ?? false; // 默认:不提示成功信息
// 特殊格式请求处理
const posts = ["put", "post", "patch"];
if (
posts.includes(opt.method) &&
opt.headers["Content-Type"] === "application/x-www-form-urlencoded"
) {
opt.data = qs.stringify(opt.data);
}
try {
const result = await ajax(opt); // 请求接口
if (result.headers["authorization"]) {
// vuex.commit("user/SET_TOKEN", result.headers["authorization"]);
}
// 是否已登录
if (opt.reLogin && result.status === 401) {
signOut();
return Promise.reject(result);
}
switch (opt.file) {
case false: {
// 解密后端返回信息
/*const response = opt.mock ? result.data
: result.data
? JSON.parse(base64Decode(result.data))
: result.data;*/
const response = result.data;
const code = response[codeField];
// const data = response[dataField];
const message = response[msgField];
const errCode = response[tipsCode];
// 提前统一处理接口提示信息
if (code === codeNum) {
handlerSuccessMessage(success, message); // success===false:不提示信息
return Promise.resolve(response);
} else {
handlerErrorMessage(error, message, errCode); // error===false:不提示信息
return Promise.reject(response);
}
}
// 走文件模式下
case true: {
return Promise.resolve(result);
}
}
} catch (e) {
const response = e.response;
if (opt.reLogin && response?.status === 401) signOut();
else {
const resData = response?.data ?? {};
const message = resData[msgField];
const errCode = resData[tipsCode];
handlerErrorMessage(error, message, errCode);
}
return Promise.reject(e);
}
}
export { BaseApi };
- axiosAjax.js文件
import axios from "axios";
import jsonpAjax from "./jsonpAjax";
import { Loading } from "element-ui";
// axios函数封装
const ajax = async ({
url = "",
loading = false, // 加载拦截
baseURL = "",
data = {},
params = {}, // 地址栏拼接数据,仅限于'put', 'post', 'patch'
headers = { "Content-Type": "application/json;charset=UTF-8" }, // 头部信息处理
method = "get",
timeout = 30 * 1000,
responseType = "json", // 表示服务器响应的数据类型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
jsonp = false, //是否使用jsonp请求接口
jsonpOpt = {} // jsonp库的options参数
}) => {
// 接口全局加载提示
let loadingInstance = "";
if (loading !== false) {
loadingInstance = Loading.service({
lock: true,
text: loading !== true ? loading : "加载中……",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.5)"
});
}
try {
const posts = ["put", "post", "patch"]; // 使用data作为发送数据主体
let response = null;
if (jsonp) {
response = await jsonpAjax({
url,
baseURL,
data,
timeout,
jsonpOpt
});
} else {
response = await axios({
url: url,
baseURL: baseURL,
headers: headers,
method: method,
params,
[posts.includes(method.toLowerCase()) ? "data" : "params"]: data,
timeout: timeout,
responseType
});
}
loadingInstance && loadingInstance.close();
return Promise.resolve(response);
} catch (e) {
loadingInstance && loadingInstance.close();
return Promise.reject(e);
}
};
export { ajax };
- jsonpAjax.js文件
// 原始文档https://github.com/webmodules/jsonp
import jsonp from "jsonp";
function connectUrl(data) {
let url = "";
for (let k in data) {
let value = data[k] !== undefined ? data[k] : "";
url += `&${k}=${encodeURIComponent(value)}`; //使用的es6的模板字符串的用法 ${}
}
return url ? url.substring(1) : ""; //这里主要判断data是否为空
}
const handlerOpt = ({
url = "",
baseURL = "", // 将会拼接到url前面
data = {}, // 传入的参数,注意是对象
timeout = 60 * 1000,
jsonpOpt = {}
}) => {
url = baseURL + url; // 拼接基础路径
//拼接字符串(根路径 + 参数),看根路径是否包含 ‘?’
url = url + (url.indexOf("?") < 0 ? "?" : "&") + connectUrl(data);
jsonpOpt = {
// param 用于指定回调的查询字符串参数的名称(默认为callback)
// prefix 处理 jsonp 响应的全局回调函数的前缀(默认为__jp)
// name 处理 jsonp 响应的全局回调函数的名称(默认为prefix+ 递增计数器)
timeout, //发出超时错误后多长时间。0禁用(默认为60000)
...jsonpOpt
};
return {
url,
baseURL,
data,
timeout,
jsonpOpt
};
};
//封装一个jsonp的函数
export default function jsonpAjax(opt = {}) {
let { url, jsonpOpt } = handlerOpt(opt);
return new Promise((resolve, reject) => {
jsonp(url, jsonpOpt, (err, data) => {
if (!err) {
resolve(data);
} else {
reject(err);
}
});
});
}
- tips.js
import { Message } from "element-ui";
// import { checkAnswer } from '@/utils/jumpToSolution'
// 统一message
const customMessage = async ({ msg = "", type = "success", tipsCode = "" }) => {
if (!msg) return null;
if (!tipsCode) {
Message({
message: msg,
type,
showClose: true
});
}
if (tipsCode) {
Message({
dangerouslyUseHTMLString: true,
message: `${msg} <a href="/nbs-pc/#/wiki-search?name=${tipsCode}" style="color: #409EFF;" target="_blank">更多帮助</a>`,
type,
showClose: true
})
}
};
// 消息提示
const Tips = {
success(opt = {}) {
customMessage({ type: "success", ...opt });
},
error(opt = {}) {
customMessage({ type: "error", ...opt });
}
};
export default Tips;
- upload.js文件
// 适用于按钮点击上传场景
import Tips from "./tips";
// 对uri地址进行数据拼接
const new_url = obj => {
if (obj) {
let fields = "";
for (let key in obj) {
fields = fields + `&${key}=${obj[key]}`;
}
return "?" + fields.substring(1, fields.length);
} else {
return "";
}
};
const paramsHandle = options => {
options.baseURL = ""; //个人处理,需要兼容之前的elementui等插件的上传
options.fdata = options.fdata || ""; //文件上传的url拼接地址
options.success = options.success || "文件上传成功";
options.url = options.url + new_url(options.fdata);
options.loading = options.loading || "文件上传中";
options.headers = options.headers || {};
options.headers["Content-Type"] = "multipart/form-data";
options.method = "post";
options.multiple = options.multiple || false; //是否多文件,默认false
//文件类型验证,注意传入数组,默认["image/jpeg", "image/png"]
options.type = options.type || ["image/jpeg", "image/png"];
options.size = options.size || 5; //文件大小限制,默认5M大小
options.max = options.max || 5; //最多上传几个文件
return options;
};
// 文件上传
const upload = (ajaxCallback, params) => {
const options = paramsHandle(params);
//文件验证处理
let input = document.createElement("input");
input.type = "file";
options.multiple ? (input.multiple = "multiple") : "";
input.click();
return new Promise((suc, err) => {
let type = options.type;
input.addEventListener("input", watchUpload, false);
function watchUpload(event) {
//console.log(event);
//移除监听
let remove = () => {
input.removeEventListener("input", watchUpload, false);
input = null;
};
const file = event.path[0].files;
const len = file.length;
// 文件数量限制
if (len > options.max) {
remove();
Tips.error({ msg: "文件个数超过" + options.max });
err(file);
return false;
}
let formData = new FormData();
for (let i = 0; i < len; i++) {
// 文件大小限制
if (options.size !== 0 && file[i].size / 1024 / 1024 > options.size) {
remove();
Tips.error({ msg: file[i].name + "文件超过" + options.size + "M" });
err(file[i]);
return false;
}
// 文件类型限制
if (type.length > 0 && !type.includes(file[i].type)) {
remove();
Tips.error({ msg: file[i].name + "文件类型为" + file[i].type });
err(file);
return false;
}
formData.append("dhtUpload", file[i], file[i].name);
}
options.data = formData;
// 最终进行文件上传
ajaxCallback(options)
.then(e => {
suc(e);
})
.catch(e => {
err(e);
});
// 没有问题下,清空监听。
remove();
}
});
};
export default upload;
文章参考
如有问题,请留言,以便修改或者删除,谢谢
其他
防抖节流antiShakingAndThrottling.js,请点击前往。
仓库地址:github.com/wwmingly/ax…