🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# 跨平台Axios服务封装,支持后端多种微服务对接 ## 前言 大家封装axios是否都是通过拦截服务呢?处理时,大多从以下问题出发吧,具体代码可[点击](https://www.kancloud.cn/vvmily_king/vvmily/2326097)前往。 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 ---导出出口 ![](https://img.kancloud.cn/b2/70/b2706bac803d081e1c9b7b74b0cea645_435x323.png) ## 代码使用 * 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 }); // let showTip = await checkAnswer(tipsCode) // if (showTip) { // Message({ // dangerouslyUseHTMLString: true, // message: `${msg} <a href="/nbs-pc/#/wiki-search?name=${tipsCode}" style="color: #409EFF;" target="_blank">更多帮助</a>`, // type, // showClose: true, // }) // } else { // Message({ // message: msg, // 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,请[点击](https://www.kancloud.cn/vvmily_king/vvmily/2331774)前往。 仓库地址:https://github.com/wwmingly/axios-services