axios-api-query
Version:
一个javascript通过api对象模型描述自动生成axios请求实例的插件
755 lines (740 loc) • 25.6 kB
JavaScript
import qs from 'querystring';
import _assign from 'lodash/assign';
import _pick from 'lodash/pick';
import _omit from 'lodash/omit';
import _toLower from 'lodash/toLower';
import _has from 'lodash/has';
import _hasIn from 'lodash/hasIn';
import _replace from 'lodash/replace';
import _isString from 'lodash/isString';
import _get from 'lodash/get';
import _set from 'lodash/set';
import _keys from 'lodash/keys';
import _isObject from 'lodash/isObject';
import _cloneDeep from 'lodash/cloneDeep';
import _includes from 'lodash/includes';
import _concat from 'lodash/concat';
import _isEmpty from 'lodash/isEmpty';
import _now from 'lodash/now';
import _isFunction from 'lodash/isFunction';
// import _toUpper from 'lodash/toUpper'
import _isArray from 'lodash/isArray';
import _isNil from 'lodash/isNil';
import _forEach from 'lodash/forEach';
import { apiDefaultConfig, axiosDefaultConfig } from '@config/options.js';
import axios from './axios';
// window.Promise = Promise 如果是在 html 页面中直接测试(ie不支持Promise,Chrome则不用解释),请解释这句话
/**
* @class Loader
* @classdesc axios请求实例构建器
* @desc 构造函数接收3个参数,第一个参数为必填,第二和三可选
* @see 插件功能详细介绍请查看
* {@link https://github.com/zhangh-design/js-libs/tree/master/api-loader GitHub}
* @author zhangh
* @version 1.0.0
* @param { {} } userApiConfigModuleList={} - api接口配置描述模型
* @param { {} } [userApiConfig={}] - api接口模型配置参数
* @prop {string} userApiConfig.mockBasePath - mock-url请求地址(可以是相对 URL), 应该外部传入
* @prop {boolean} userApiConfig.mock=false - mock全局控制开关
* @prop {object} userApiConfig.gParams - URL全局自定义参数
* @prop {boolean} userApiConfig.cache=false - 缓存控制开关在URL路径后面添加一个时间戳参数 _=1571825001570
* @prop {string} userApiConfig.seq=/ - api接口命名空间分隔符
* @prop {string} userApiConfig.invalidChar - 进行特殊字符过滤的字符 例如:`~!@#$^&*()=|{}\
* @prop {object} userApiConfig.statusMessage - 前端response返回状态码提示短语 例如:400: '错误请求'
* @prop {boolean} userApiConfig.console_request_enable=false - 开启请求参数打印
* @prop {boolean} userApiConfig.console_response_enable=false - 开启响应参数打印
* @prop {function} userApiConfig.request_error_callback=null - 请求错误回调函数
* @prop {function} userApiConfig.vdjs_fail_callback=null - 请求错误回调函数
* @param { {} } [userAxiosConfig={}] - axios实例配置参数
* @prop {number} userAxiosConfig.timeout=15000 - 超时时间(毫秒)
* @prop {string} userAxiosConfig.baseURL - 访问url目录(可以是相对 URL), 应该外部传入
* @prop {number} userAxiosConfig.maxContentLength=2000 - 定义允许的响应内容的最大尺寸(字节数)
* @prop {number} userAxiosConfig.status=200 - 来自服务器响应的 HTTP 访问处理成功状态码
* @prop {string} userAxiosConfig.status=OK - 来自服务器响应的 HTTP 状态信息短语
* @prop {array} userAxiosConfig.transformResponse - 全局预处理过滤函数
* @prop {object} userAxiosConfig.headers={'Content-Type': 'application/json;charset=UTF-8'} - 请求响应头
* @prop {object} userAxiosConfig.defaults - 配置的默认值
* @prop {string} userAxiosConfig.responseType='json' - 服务器响应的数据类型
* @prop {object} userAxiosConfig.proxy - 定义代理服务器的主机名称和端口
* @param { {} } [ApiFilterExpand={}] - 自定义拦截器类的实例对象
* @example
* userApiConfigModuleList:{'goods': [{'read':{'name':'',desc: ''}, 'get': {}}]}
* userApiConfig:{'mockBasePath': 'mock/test/goods/read', 'mock': true}
* userAxiosConfig:{'timeout': 15000, 'baseURL': 'test/goods/read'}
*
*/
const Loader = class Api {
constructor(
userApiConfigModuleList = {},
userApiConfig = {},
userAxiosConfig = {},
ApiFilterExpand = null
) {
this.ApiFilterExpand = ApiFilterExpand; // 自定义拦截器类
/**
* @desc 请求头参数
* @access public
* @type {object}
* @default
* @example
* this.headerOptions = {token: 'test_123'};
*/
this.headerOptions = {};
/**
* @description
* 如果你要在自己的业务中使用Loader构造器构造出的axios对象,可以通过实例属性api来获取,
* 其实实例属性api属性也是Loader加载器唯一对外提供请求实例的对象
* @access public
* @type {object}
* @readonly
* @default
* @example
* 获取:Loader.api['goods/fruit/apple']().then((response)=>{}).catch((error)=>{})
* 数据:{'goods/fruit/apple': [{'read': {name: 'read',desc: '获取apple列表'}}]}
* */
this.api = {};
/* if (!_isPlainObject(userApiConfig) || !_isPlainObject(userAxiosConfig) || !_isPlainObject(userApiConfigModuleList)) {
console.error('error:01')
return
} */
// 默认配置和传入的覆盖配置
/**
* @access private
* @readonly
* @desc api接口模型配置参数
*
* */
this.apiParamsConfig = _pick(
_assign(apiDefaultConfig, userApiConfig),
_keys(apiDefaultConfig)
);
/**
* @access private
* @readonly
* @desc axios实例配置模型
*
* */
this.axiosParamsConfig = _pick(
_assign(axiosDefaultConfig, userAxiosConfig),
_keys(axiosDefaultConfig)
);
this.deconstructApiConfigModule(userApiConfigModuleList);
}
/**
* @description 解析api模型
* @access private
* @param { {} } userApiConfigModuleList - api接口模型
* @example
* {'goods': [{'read': {}},{'get': {}}]}
*/
deconstructApiConfigModule(userApiConfigModuleList = {}) {
for (const moduleFileHierarchyNameKey in userApiConfigModuleList) {
const apiItem = userApiConfigModuleList[moduleFileHierarchyNameKey];
if (_isNil(apiItem)) {
continue;
}
for (let i = 0; i < apiItem.length; i++) {
this.buildInstance(moduleFileHierarchyNameKey, apiItem[i]);
}
}
}
/**
* @description 注入外部接口-解析api模型
* @access protect
* @param { {} } moduleList - api接口模型
* @example
* {'goods': [{'read': {}},{'get': {}}]}
*/
injectApiModules(moduleName = '', moduleList = {}) {
const modules = {};
// 是否对象
const isObj = o => {
return Object.prototype.toString.call(o).slice(8, -1) === 'Object';
};
// 是否数组
const isArray = o => {
return Object.prototype.toString.call(o).slice(8, -1) === 'Array';
};
_forEach(moduleList, (value, key) => {
// 一层
if (isArray(value)) {
_set(modules, `${moduleName}${this.apiParamsConfig.seq}${key}`, value);
}
// 多层
if (isObj(value)) {
const sOnePath = `${moduleName}${this.apiParamsConfig.seq}${key}`;
_forEach(value, (apiModule, childKey) => {
if (isArray(apiModule)) {
// 两层
_set(
modules,
`${sOnePath}${this.apiParamsConfig.seq}${childKey}`,
apiModule
);
}
if (isObj(value)) {
// 三层
const sTwoPath = `${sOnePath}${this.apiParamsConfig.seq}${childKey}`;
_forEach(apiModule, (thirdApiModule, thirdChildKey) => {
_set(
modules,
`${sTwoPath}${this.apiParamsConfig.seq}${thirdChildKey}`,
thirdApiModule
);
});
}
});
}
});
this.deconstructApiConfigModule(modules);
}
/**
* @description 构建api实例
* @access private
* @param {string} namespace - api请求模型命名空间名称
* @param {Object} args - api请求模型参数
* @prop {string} args.name - api接口名称
* @prop {string} [args.method='GET'] - 请求类型
* @prop {string} [args.desc] - 描述
* @prop {string} [args.baseURL] - 访问url目录(可以是相对 URL)
* @prop {string} args.path='root/user/getUserInfo' - 请求接口路径
* @prop {string} args.mockPath='mock/root/user/getUserInfo' - mock请求接口路径
* @prop {boolean} [args.mock=false] - 是否打开mock操作
* @prop {boolean} [args.cache=false] - 是否打开cache
* @prop {object} [args.restful] - restful参数
* @prop {object} [args.headers] - 请求首部字段参数
* @prop {string} [args.removeInvalidChar=true] - 是否执行参数特殊字符过滤
* @prop {object} [args.params] - 待发送 Key/value 参数 GET
* @prop {object} [args.data] - POST请求,待发送 Key/value 参数
* @prop {object} [args.validator] - params和data参数验证对象
* @prop {object} [args.restfulValidator] - restful参数验证对象
* @prop {string} [args.responseType='json'] - 服务器响应的数据类型
* @prop {object} [args.proxy=null] - 定义代理服务器的主机名称和端口
* @prop {boolean} [args.isWhite=false] - 白名单接口
* @prop {boolean} [args.isLogin=false] - 是否登录接口(用在了token过期的调整判断)
* @prop {boolean} [args.isAbandonCheckedParams=false] - 是否放弃校验请求参数,默认 false 会校验,具体请求中设置 true 会放弃校验匹配
* @prop {object} [args.extData] - 外部自定义参数
* @example
* namespace:'goods/fruit'
* apiConfigModule: {name: 'read', desc: '', method:'GET', path: 'root/user/getUserInfo',mockPath: 'mock/root/user/getUserInfo',mock: false, cache: false, restful: {}, headers: {}, removeInvalidChar: true, params: {}, data: {},validator: {}, restfulValidator: {}, responseType: 'json', proxy: null}
*/
buildInstance(
namespace = '',
{
name,
method = 'GET',
desc = '',
baseURL,
path,
mockPath,
mock,
cache = false,
restful = {},
headers = {},
removeInvalidChar = true,
params = {},
data = {},
validator = {},
restfulValidator = {},
responseType,
proxy,
isWhite = false,
isLogin = false,
isAbandonCheckedParams = false,
timeout,
backServerPref,
extData = {}
}
) {
// eslint-disable-next-line
if (!name || (!path && !mockPath)) {
console.error('error:02');
return;
}
const apiName = `${namespace}${_get(
this,
'apiParamsConfig.seq',
'/'
)}${name}`;
Object.defineProperty(this.api, apiName, {
value: (
outParams = {
params: {},
data: {},
headers: {},
restful: {}
},
outOptions = {
request_error_callback: null,
transformResponse: null,
validator: null,
restfulValidator: null,
vdjs_fail_callback: null
}
) => {
// outParams -> {'restful': {}, 'headers': {}, 'params': {}, 'data': {}}
_set(headers, 'api-module-path', `${apiName}`);
if (!_has(headers, 'Content-Type')) {
_set(
headers,
'Content-Type',
_get(
this,
'axiosParamsConfig.headers.Content-Type',
'application/json;charset=UTF-8'
)
);
}
if (_has(outParams, 'headers.Content-Type')) {
_set(
headers,
'Content-Type',
_get(outParams, 'headers.Content-Type')
);
}
let [url, pickParams, pickData, pickHeaders, pickOptions] = [
path,
{},
{},
{},
{}
];
// 全局 mock
if (
(_get(this, 'apiParamsConfig.mock') === true && _isNil(mock)) ||
(!_isNil(mock) && mock === true)
) {
url = !mockPath ? '' : mockPath;
baseURL = _get(this, 'apiParamsConfig.mockBasePath');
}
if (!_isEmpty(_get(outParams, 'params', {})) || !_isEmpty(params)) {
let getParams = _pick(
_assign(
{},
params,
_get(this, 'apiParamsConfig.gParams', {}),
_get(outParams, 'params', {})
),
_concat(
_keys(params),
_keys(_get(this, 'apiParamsConfig.gParams', {}))
)
);
if (isAbandonCheckedParams) {
// 放弃校验请求参数
getParams = _assign(
{},
params,
_get(this, 'apiParamsConfig.gParams', {}),
_get(outParams, 'params', {})
);
}
pickParams = getParams;
}
if ((!_get(this, 'apiParamsConfig.cache', false) && !cache) || !cache) {
_set(pickParams, '_', _now());
}
if (!_isEmpty(_get(outParams, 'data', {})) || !_isEmpty(data)) {
let dataParams = _pick(
_assign({}, data, _get(outParams, 'data', {})),
_keys(data)
);
if (isAbandonCheckedParams) {
// 放弃校验请求参数
if (_isString(_get(outParams, 'data', {}))) {
// this.$api['system/authority/add']({ data: JSON.stringify([1, 2, 3, 4, 5]) })
// headers: { 'content-type': 'aplication/json;charset=UTF-8' }
dataParams = _get(outParams, 'data', {});
} else {
dataParams = _assign({}, data, _get(outParams, 'data', {}));
}
}
pickData = Loader.transformStringPostData(
Loader.removeInvalidChar(dataParams, removeInvalidChar),
headers,
method
);
}
if (!_isEmpty(_get(outParams, 'restful', {})) || !_isEmpty(restful)) {
let restfulParams = _pick(
_assign({}, restful, _get(outParams, 'restful', {})),
_keys(restful)
);
if (isAbandonCheckedParams) {
// 放弃校验请求参数
restfulParams = _assign(
{},
restful,
_get(outParams, 'restful', {})
);
}
url = Loader.transformRestfulUrl(url, restfulParams);
}
// 全局自定义参数
if (!_isEmpty(_get(this, 'apiParamsConfig.gParams', {}))) {
pickParams = _assign(
{},
pickParams,
_get(this, 'apiParamsConfig.gParams', {})
);
}
if (!_isEmpty(_get(outParams, 'headers', {})) || !_isEmpty(headers)) {
// headers 里的参数不进行特殊字符检查,防止比如 Content-Type 的这种原生参数设置被误检查出特殊字符
// pickHeaders = Loader.removeInvalidChar(_pick(_assign(headers, _get(outParams, 'headers', {})), _keys(headers)), removeInvalidChar)
let headersParams = _pick(
_assign({}, headers, _get(outParams, 'headers', {})),
_keys(headers)
);
if (isAbandonCheckedParams) {
// 放弃校验请求参数
headersParams = _assign(
{},
headers,
_get(outParams, 'headers', {})
);
}
pickHeaders = headersParams;
}
if (!_isEmpty(this.headerOptions)) {
// 外部设置的通用请求头参数
pickHeaders = _assign(
{},
_omit(this.headerOptions, _keys(pickHeaders)),
pickHeaders
);
}
if (!_isEmpty(outOptions)) {
pickOptions = Loader.encapsulationOutOptions(outOptions);
}
if (_has(outOptions, 'validator')) {
_assign(validator, _get(outOptions, 'validator'));
}
if (_has(outOptions, 'restfulValidator')) {
_assign(restfulValidator, _get(outOptions, 'restfulValidator'));
}
const requestOptions = this.encapsulationRequestOptions({
baseURL,
proxy,
responseType,
validator,
restfulValidator
});
// 是否登录接口
if (isLogin) {
pickHeaders.isLogin = true;
}
_forEach(extData, (value, key) => {
if (!_has(pickHeaders, key)) {
pickHeaders[key] = value;
}
});
// 拦截过滤函数处理
if (_hasIn(this.ApiFilterExpand, 'threeOuterFilter')) {
this.ApiFilterExpand.threeOuterFilter(
this.headerOptions,
baseURL || _get(this, 'axiosParamsConfig.baseURL'),
pickHeaders
);
}
// 自定义扩展过虑
if (isWhite) {
this.whiteFilter(pickHeaders);
}
// 后端服务名
if (
(_get(this, 'apiParamsConfig.mock') === false && _isNil(mock)) ||
(!_isNil(mock) && mock === false)
) {
let apiParamsBackServerPref = _get(
this,
'apiParamsConfig.backServerPref'
);
if (!_isNil(backServerPref)) {
apiParamsBackServerPref = backServerPref;
}
if (
!_isNil(apiParamsBackServerPref) &&
apiParamsBackServerPref.length > 0
) {
url = apiParamsBackServerPref + url;
}
}
/* const axiosParams = _assign({}, requestOptions, pickOptions, {
method: method.toUpperCase(),
url,
headers: pickHeaders,
params: pickParams,
data: pickData,
restful
});
return axios(axiosParams); */
const AxiosProxyFunc = function() {
this.params = outParams;
this.outOptions = outOptions;
this.init();
return this;
};
AxiosProxyFunc.prototype = {
params: {},
outOptions: {},
axiosInstance: null,
thenHandle: null,
catchHandle: null,
finallyHandle: null,
then: function(thenFunc) {
if (thenFunc) {
this.thenHandle = thenFunc;
}
return this;
},
catch: function(catchFunc) {
if (catchFunc) {
this.catchHandle = catchFunc;
}
return this;
},
finally: function(finallyFunc) {
if (finallyFunc) {
this.finallyHandle = finallyFunc;
}
return this;
},
init: function() {
const axiosParams = _assign({}, requestOptions, pickOptions, {
method: method.toUpperCase(),
url,
headers: pickHeaders,
params: pickParams,
data: pickData,
restful,
// timeout,
apiInstance: function() {
return {
thenHandle: this.thenHandle,
catchHandle: this.catchHandle,
finallyHandle: this.finallyHandle
};
}.bind(this)
});
if (!_isNil(timeout)) {
_set(axiosParams, 'timeout', timeout); // 单独设置超时时间
}
this.axiosInstance = axios(axiosParams);
this.axiosInstance
.then(resData => {
// eslint-disable-next-line promise/always-return
this.thenHandle !== null && this.thenHandle(resData);
})
.catch(error => {
this.catchHandle !== null && this.catchHandle(error);
})
.finally(() => {
this.finallyHandle !== null && this.finallyHandle();
});
}
};
const instance = new AxiosProxyFunc();
// return axios(axiosParams);
return instance;
}
});
}
/**
* @desc 白名单过滤器
* @param { Object } pickHeaders={} - Request Headers 请求头参数
*/
whiteFilter(pickHeaders = {}) {
if (_hasIn(this.ApiFilterExpand, 'isWhite')) {
this.ApiFilterExpand.isWhite(pickHeaders);
}
}
/**
* @desc 处理并发请求
* @param { array } apiArray - api请求实例数组
* @access public
* @returns {Promise}
* @example
* Loader.allApi[Loader.api['user/get'](), Loader.api['user/list']()]().then(()=>{}).catch(()=>{})
*/
allApi(apiArray) {
if (!_isArray(apiArray)) {
apiArray = [];
}
return Promise.all(apiArray);
}
/**
* @static
* @desc 转换url地址 fruit/hz/xh/{shop}/read -> fruit/hz/xh/1001/read
* @access private
* @param {string} url
* @param {{}} restfulData
* @returns {string}
* @example
* url:fruit/hz/xh/{shop}/read
* restfulData:{'shop': 10,'id': 1}
*/
static transformRestfulUrl(url, restfulData) {
let restfulUrl = url;
for (const key in restfulData) {
if (Loader.strHaveStr(restfulUrl, `{${key}}`)) {
restfulUrl = _replace(restfulUrl, `{${key}}`, restfulData[key]);
}
}
return restfulUrl;
}
// 正则匹配 判断字符串中是否包含某个字符串 strHaveStr('abc','bc')
static strHaveStr(str, regStr) {
var reg = new RegExp('^.*' + regStr + '.*$');
if (str.match(reg)) {
return true;
}
return false;
}
/**
* @desc 特殊字符过滤
* @access private
* @static
* @param {{}} requestData - 请求的数据对象 get post put delete
* @param {boolean} removeInvalidChar=true - 是否需要过滤特殊字符
*/
static removeInvalidChar(requestData = {}, removeInvalidChar = true) {
if (!removeInvalidChar || _isString(requestData)) return requestData;
// 全局替换正则
const reg = new RegExp(_get(this, 'apiParamsConfig.invalidChar'), 'g');
for (const key in requestData) {
if (_isString(requestData[key]) && reg.test(requestData[key])) {
requestData[key] = _replace(requestData[key], reg, '');
}
}
return requestData;
}
/**
* @desc
* axios post请求 headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}
* 参数需要通过qs.stringify()进行设置
* @static
* @access private
* @param {{}} requestData
* @param {{}} headersData
* @param {string} method=GET
*/
static transformStringPostData(requestData, headersData, method = 'GET') {
if (
_includes(['post', 'put'], _toLower(method)) &&
_includes(
_get(headersData, 'Content-Type'),
'application/x-www-form-urlencoded'
)
) {
requestData = qs.stringify(requestData);
}
return requestData;
}
/**
* @desc 封装外部配置参数
* @static
* @access private
* @param {{}} outOptions - 外部参数
* @returns {{}}
*/
static encapsulationOutOptions(outOptions = {}) {
const options = {};
const requestErrorCallback = _get(
outOptions,
'request_error_callback',
null
);
const vdjsFailCallback = _get(outOptions, 'vdjs_fail_callback', null);
const transformResponse = _get(outOptions, 'transformResponse', null);
if (_isFunction(requestErrorCallback)) {
_set(options, 'request_error_callback', requestErrorCallback);
}
if (_isFunction(vdjsFailCallback)) {
_set(options, 'vdjs_fail_callback', vdjsFailCallback);
}
if (_isArray(transformResponse) || _isFunction(transformResponse)) {
_set(
options,
'transformResponse',
_isArray(transformResponse)
? transformResponse
: [_isFunction(transformResponse)]
);
}
return options;
}
/**
* @desc 封装request请求参数
* @access private
* @returns {{}}
*/
encapsulationRequestOptions({
baseURL,
proxy,
responseType,
validator,
restfulValidator
}) {
const options = _cloneDeep(_get(this, 'axiosParamsConfig'));
if (_isObject(proxy)) {
_set(options, 'proxy', proxy);
}
if (_isString(responseType)) {
_set(options, 'responseType', responseType);
}
if (baseURL) {
_set(options, 'baseURL', baseURL);
}
if (
_isFunction(_get(this, 'apiParamsConfig.request_error_callback', null))
) {
_set(
options,
'request_error_callback',
_get(this, 'apiParamsConfig.request_error_callback')
);
}
if (_isFunction(_get(this, 'apiParamsConfig.vdjs_fail_callback', null))) {
_set(
options,
'vdjs_fail_callback',
_get(this, 'apiParamsConfig.vdjs_fail_callback')
);
}
_set(
options,
'statusMessage',
_get(this, 'apiParamsConfig.statusMessage', {})
);
_set(
options,
'console_response_enable',
_get(this, 'apiParamsConfig.console_response_enable', false)
);
_set(
options,
'console_request_enable',
_get(this, 'apiParamsConfig.console_request_enable', false)
);
if (
!_isArray(_get(options, 'transformResponse', null)) &&
_isFunction(_get(options, 'transformResponse', null))
) {
_set(options, 'transformResponse', [_get(options, 'transformResponse')]);
}
return _assign(options, {
validator,
restfulValidator
});
}
/**
* @desc 设置请求头 headers
* @param {Object} options={} - 外部参数
*/
setHeaderOptions(options = {}) {
if (!_isEmpty(options)) {
this.headerOptions = _assign({}, this.headerOptions, options);
}
}
};
export default Loader;