跨平台Axios服务封装,支持后端多种微服务对接

1,652 阅读7分钟

小知识,大挑战!本文正在参与“  程序员必备小知识  ”创作活动

本文同时参与 「掘力星计划」  ,赢取创作大礼包,挑战创作激励金

前言

大家封装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                    ---导出出口

image.png

代码使用

  • apis/BaseServer/index.js文件
export default function BaseServer(ajax, config) {
  return {
    // 根据城市名称模糊搜索
    queryListopt =>
      ajax({
        url"/sale/list",
        method"get",
        ...opt
      })
  };
}
  • 在业务组件中引入import Api from "./services";,得到的一定是正确的数据
// 前提 import Api from "./services";
const opt = {
      data: {
        // ...
        pageNum1,
        pageSize15
      },
      loadingtrue
      // success: true
}

Api.BaseServer.queryList(opt).then(res => {
      console.log("拿到一定是正确的数据", res);
    });

*Ajax请求配置以及请求后数据统一处理配置,如上queryList(opt)方法传入opt对象配置如下:
注:下边的配置项,适当的把部分配置放apis/BaseServer/index.js文件配置,部分存放到业务组件中。

配置项说明是否必填类型默认值
url请求Apistring
loading加载拦截,全屏booleanfalse
baseURL基础路径string
data请求发送数据object
params地址栏拼接数据,仅限于'put', 'post', 'patch'object
timeout超时时间number30 * 1000
method请求方法:get、post、put、patch、jsonpstringget
headers请求头object{ "Content-Type": "application/json" }
success请求成功时,是否提示booleanfalse
error请求失败时,是否提示booleantrue
jsonp是否使用jsonp请求接口booleanfalse
jsonpOptjsonp库的options参数,配合jsonp使用objectfalse
file是否为文件模式booleanfalse
mock是否为mock模式booleanfalse
responseType数据格式stringjson
isResponse是否简化数据booleanfalse
reLogin是否校验登录booleantrue

代码开发

  • 基本配置
// 接口和页面初始化配置中心
// 在前置配置之前,需要搞清楚后端微服务前缀路由是什么,然后再配置到该文件下面
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 {
  BaseServerBaseServer(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;

文章参考

链接:juejin.cn/post/701055…

如有问题,请留言,以便修改或者删除,谢谢

其他

防抖节流antiShakingAndThrottling.js,请点击前往。
仓库地址:github.com/wwmingly/ax…