一、前言
正在用vue写一个前端项目,想使用axios库来从后端获取数据。每一次都使用类似“const response = await axios.get(...);”、“const result = response.data;”的方式来请求虽然问题不大,但当接口多起来时,代码的结构耦合度会很高,并且万一不同版本库的调用语法不同,项目后续维护起来也很麻烦。所以参考了这篇博文 (https://juejin.cn/post/7042341684012646407/) 的做法,对axios库进行了二次封装,方便开发过程中api的调用以及后续代码维护。
虽然不太想做前端/(ㄒoㄒ)/~~,但对我来说也是一次学习(自我安慰),所以还是把过程记录下来,万一日后又用到了,也方便把知识重新拾起。
二、文件结构
先在src文件夹下建立一个service文件夹,在service文件夹下再分别新建request.js和index.js文件。主要封装过程在request.js下进行。
三、request.js
request.js的代码如下。对我自己而言,js的语法实在是有点晦涩难懂。一个也许不太准确的理解:
把attachAPI看作是一个class,class的一个重要成员变量是请求客户端对象client,它通过外部传参来进行初始化。hostApi是一个函数数组,记录attachAPI这个class的所有api成员函数。
createRequestClient是唯一暴露给外部的函数,createRequestClient函数会先创建一个请求客户端对象client,这个client会被用来作为attachAPI对象的初始化参数,最后把这个attachAPI对象返回给我们使用。
// request.js
import axios from 'axios';
const MATCH_METHOD = /^(GET|POST|PUT|DELETE|HEAD|OPTIONS|CONNECT|TRACE|PATCH)\s+/;
const MATCH_PATH_PARAMS = /:(\w+)/g;
const USE_DATA_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];
/**
* 注册API到Axios实例
* @param client 客户端对象,类型为AxiosInstance
* @param apis 该客户端所有待注册的api
*/
function attachAPI(client, apis) {
const hostApi = Object.create(null);
for (const apiName in apis) {
const apiConfig = apis[apiName];
let apiPath = apiConfig;
hostApi[apiName] = (params, options) => {
// e.g. api.createUser({ name: 'xx', avatar: "1" }) => _params={ name: 'xx', avatar: "1" }
const _params = { ...(params || {}) };
// e.g. 'POST /api/test' => prefix='POST ', method='POST'
const [prefix, method] = apiPath.match(MATCH_METHOD) || ['GET ', 'GET'];
// e.g. 'POST /api/test' => '/api/test'
let url = apiPath.replace(prefix, '');
// 匹配以冒号 : 开头、冒号后面跟着一个或多个单词字符(字母、数字或下划线)的字符串
// 使用全局标志 g,确保在整个字符串中查找所有匹配项,而不仅仅是第一个
const matchParams = apiPath.match(MATCH_PATH_PARAMS);
// 匹配请求链接中的动态参数
if (matchParams) {
matchParams.forEach((match) => {
const key = match.replace(':', '');
if (Reflect.has(_params, key)) {
url = url.replace(match, Reflect.get(_params, key));
Reflect.deleteProperty(_params, key);
}
});
}
// 对于某些methods,请求体需要携带数据
const requestParams = USE_DATA_METHODS.includes(method)
? { data: _params }
: { params: _params };
return client.request({
url,
method: method.toLowerCase(),
...requestParams,
...options,
});
};
}
return hostApi;
}
/**
* 创建请求客户端
* @param requestConfig 客户端初始化配置,包括baseURL、headers、headerHandlers、errorHandler、timeout、apis属性
*/
export function createRequestClient(requestConfig) {
const client = axios.create({
baseURL: requestConfig.baseURL,
headers: requestConfig.headers,
timeout: requestConfig.timeout,
});
// 设置请求拦截器,config 是拦截器接收到的请求配置对象
client.interceptors.request.use((config) => {
// 检查 requestConfig 对象中是否存在 headerHandlers 属性,若不存在则赋值为空数组
// .map() 方法遍历 headerHandlers 数组中的每一个函数,并对每个函数进行调用
const headerHandlers = (requestConfig.headerHandlers || []).map((handler) => {
// handler 是从 config.headerHandlers 数组中取出的每一个元素
return handler(config)
.then((mixHeaders) => {
Object.assign(config.headers, mixHeaders);
})
.catch();
});
// 确保所有请求头修改动作完成后,返回修改后的 config 对象
return Promise.all(headerHandlers).then(() => config);
});
// 设置响应拦截器
client.interceptors.response.use(
// 成功回调
(res) => res,
// 错误回调
(error) => {
const requestError = requestConfig.errorHandler
? requestConfig.errorHandler(error)
: error;
// 返回一个被拒绝的 Promise,确保调用链中的 .catch() 方法能够捕获到这个错误
return Promise.reject(requestError);
},
);
return attachAPI(client, requestConfig.apis);
}
四、index.js
// index.js
import { createRequestClient } from './request';
/**
* 请求客户端的初始化参数
* @var BASE_URL base链接
* @var HEADER 静态接口请求头
* @var HEADER_HANDLERS 动态接口请求头
* @var ERROR_HANDLERS 错误处理函数
* @var TIME_OUT 请求超时时间(ms)
*/
const BASE_URL = '/api_test';
const HEADER = {
'Content-Type': 'application/json',
};
const HEADER_HANDLERS = [
(config) => {
const token = localStorage.getItem('token');
if (token) {
return Promise.resolve({ 'Authorization': `Bearer ${token}` });
}
return Promise.resolve({});
},
];
const ERROR_HANDLERS = (error) => {
console.log(error.message);
};
const TIME_OUT = 1500;
export const api = createRequestClient({
baseURL: BASE_URL,
header: HEADER,
headerHandlers: HEADER_HANDLERS,
errorHandler: ERROR_HANDLERS,
timeout: TIME_OUT,
apis: {
getCameraState: 'GET /camera/state/:id',
startGrabbing: 'PUT /camera/grab/:id',
}
});
五、调用方法
import { api } from '@/service/index';
api.getCameraState({ id: 1 }).then(res => console.log(res.data));
api.startGrabbing({ id: 1, mode: "rgb8" }).then(res => console.log(res.data));
其实就是:
api.方法({ 参数 }).then(res => {});
传递参数时,参数通过键值对来表示。
当参数代表url中的动态参数时,键值对的键需要和url的动态参数名字相同,例如:api.getCameraState({ id: 1 }) 的键名 id 对应 /camera/state/:id 中的id;
当请求方法是['POST', 'PUT', 'PATCH', 'DELETE']中的其中一个时,请求体携带的数据就是所有传递参数。