/**
|
* Fetcher
|
* @author Tevin
|
*/
|
|
import Taro from '@tarojs/taro';
|
import Qs from 'qs';
|
import { Tools } from '@components/common/Tools';
|
import { $hostBoot } from '@components/bases/HostBoot';
|
import project from '@project';
|
|
export class Fetcher {
|
|
/**
|
* @constructor
|
* @param {Object} options
|
*/
|
constructor(options = {}) {
|
this._data = {
|
urlPrefix: options.urlPrefix || ['/api/common/', '/api/common/'],
|
};
|
}
|
|
/**
|
* 请求配置
|
* @private
|
*/
|
_defaultConfig = (() => {
|
// 跨域模式,一般为 App 内嵌页面
|
if (project.appHybrid) {
|
return {
|
url: '',
|
header: {
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
},
|
mode: 'cors',
|
credentials: 'include',
|
dataType: 'json',
|
timeout: 30 * 1000,
|
};
|
}
|
// 正常模式,小程序、普通H5
|
else {
|
return {
|
url: '',
|
header: {
|
'X-Requested-With': 'XMLHttpRequest',
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
'Ax-Rq-Type': 'separation',
|
},
|
credentials: 'same-origin',
|
dataType: 'json',
|
timeout: 30 * 1000,
|
};
|
}
|
})();
|
|
/**
|
* 拼写 URL 地址
|
* @param {String} devSuffix
|
* @param {String} [serSuffix]
|
* @return {String}
|
*/
|
spellURL(devSuffix, serSuffix) {
|
let url = '';
|
// mock地址
|
if ($hostBoot.isOnMock()) {
|
url = this._data.urlPrefix[0].replace('api/', '') + devSuffix + '.json';
|
}
|
// 正常地址
|
else {
|
url = this._data.urlPrefix[1] + (serSuffix || devSuffix);
|
}
|
const fixReg = /[a-zA-Z0-9]+\/\.\.\//;
|
while (url.indexOf('../') >= 0) {
|
url = url.replace(fixReg, '');
|
}
|
url = url.replace(/[./]\//g, '');
|
return url;
|
}
|
|
/**
|
* get 请求
|
* @param {String} url
|
* @param {*} data
|
* @param {object} [options]
|
* @return {Promise<any>}
|
*/
|
get(url, data, options = {}) {
|
const params = Qs.stringify(data);
|
if (url.indexOf('?') >= 0) {
|
url += '&' + params;
|
} else {
|
url += '?' + params;
|
}
|
return this.query('get', url, null, options);
|
}
|
|
/**
|
* post 请求
|
* @param {String} url
|
* @param {*} data
|
* @param {object} [options]
|
* @return {Promise<any>}
|
*/
|
post(url, data, options = {}) {
|
// mock 模式转换为 get 请求
|
if ($hostBoot.isOnMock()) {
|
return this.get(url, data, options);
|
}
|
const params = Qs.stringify(data);
|
const data2 = {};
|
params.split('&').forEach(param => {
|
if (param) {
|
const item = param.split('=');
|
data2[decodeURIComponent(item[0])] = decodeURIComponent(item[1]);
|
}
|
});
|
return this.query('post', url, data2, options);
|
}
|
|
/**
|
* 基础 ajax 请求
|
* @param {String} type
|
* @param {String} url
|
* @param {*} [data]
|
* @param {object} [options]
|
* @return {Promise<any>}
|
*/
|
query(type, url, data = null, options = {}) {
|
return new Promise((resolve, reject) => {
|
const header = {
|
...this._defaultConfig.header,
|
};
|
// 小程序中追加 cookie
|
if (process.env.TARO_ENV === 'weapp') {
|
header['Cookie'] = this._getCookies();
|
}
|
Taro.request({
|
...this._defaultConfig,
|
header,
|
url: this._createUrlPrefix(options) + url,
|
method: type.toUpperCase(),
|
data,
|
success: response => {
|
// 小程序中保存 cookie
|
if (process.env.TARO_ENV === 'weapp') {
|
this._saveCookies(response.cookies);
|
}
|
// 捕获响应
|
options && options.onCapture && options.onCapture({
|
url: this._createUrlPrefix(options) + url,
|
request: data,
|
response: { ...response.data },
|
httpCode: response.statusCode,
|
});
|
/**
|
* @type {{state: {code, http, msg}, data: Object}}
|
* @example response.state.code
|
* 2000 通用请求成功
|
* 2001 请求成功,但是没有数据,弹窗提示 msg(仅特殊情况使用)
|
* 5000 通用请求失败,弹窗提示 msg
|
* 9001 登录已过期,弹窗提示过期且返回登录页
|
* 9002 已登录但没有操作权限,弹窗提示 msg
|
*/
|
const responseData = this._adaptiveResponseData(response.data);
|
responseData.state.http = response.statusCode;
|
resolve(this._transformResponseData(responseData, options));
|
},
|
fail: error => {
|
// 处理响应
|
this._resolveCaughtNetErr(error, options, msg => {
|
// 捕获响应
|
options && options.onCapture && options.onCapture({
|
url: this._createUrlPrefix(options) + url,
|
request: data,
|
httpCode: error && error.status,
|
httpMsg: msg + (error.message ? (' / ' + error.message) : ''),
|
});
|
});
|
reject(null);
|
},
|
});
|
});
|
}
|
|
_createUrlPrefix(options) {
|
// 如果指定了主机类,使用固定主机类型地址,否则使用默认主机类型地址
|
let urlPrefix = '';
|
if (options.hostType) {
|
urlPrefix = $hostBoot.getHost(options.hostType);
|
} else {
|
urlPrefix = $hostBoot.getHost();
|
}
|
// mock 模式转换地址
|
if ($hostBoot.isOnMock()) {
|
urlPrefix += (project.host.assetsPath.indexOf('..') === 0 ? '/' : '') +
|
project.host.assetsPath.replace('/assets', '/mocks');
|
}
|
return urlPrefix;
|
}
|
|
// 小程序中,保存 cookies
|
_saveCookies(cookies) {
|
const localCookies = JSON.parse(Taro.getStorageSync('cookies') || '{}');
|
cookies.forEach(cookie => {
|
const mc = cookie.match(/([a-zA-Z0-9_\-]+)=(.*?);/);
|
localCookies[mc[1]] = mc[2];
|
});
|
Taro.setStorageSync('cookies', JSON.stringify(localCookies));
|
}
|
|
// 小程序中,获取 cookies
|
_getCookies() {
|
const localCookies = JSON.parse(Taro.getStorageSync('cookies') || '{}');
|
const cookiesArr = [];
|
Object.keys(localCookies).forEach(key => {
|
cookiesArr.push(key + '=' + localCookies[key]);
|
});
|
return cookiesArr.join('; ');
|
}
|
|
/**
|
* 适配旧版响应体,转换为新版
|
* @private
|
*/
|
_adaptiveResponseData(responseData) {
|
// 标准请求,不转换
|
if (typeof responseData.state === 'object' && typeof responseData.data === 'object') {
|
return responseData;
|
}
|
// App版请求(存在ret直接视为App请求),响应体转换
|
if (typeof responseData.ret !== 'undefined') {
|
// 转换数据体
|
let data2 = { rows: [] };
|
// 数组类型
|
if (responseData.data instanceof Array) {
|
if (responseData.data.length > 0) {
|
data2.rows = responseData.data;
|
}
|
}
|
// 对象类型
|
else if (responseData.data instanceof Object) {
|
if (!Tools.isEmptyObject(responseData.data)) {
|
data2 = responseData.data;
|
}
|
}
|
// 不存在
|
else if (typeof responseData.data === 'undefined') {
|
data2 = {};
|
}
|
// 转换响应码
|
let code = 0;
|
// 正常
|
if (responseData.ret === 0) {
|
code = 2000;
|
}
|
// 特殊模式下的状态码,转换为正常模式,由页面处理业务
|
else if (responseData.ret === 10999) {
|
code = 2000;
|
}
|
// 未登录
|
else if (responseData.ret === 101110) {
|
code = 9001;
|
}
|
// 其他按报错
|
else {
|
code = 5000;
|
}
|
// 合并响应体
|
return {
|
state: {
|
code,
|
msg: responseData.msg,
|
},
|
data: data2,
|
};
|
}
|
// 旧请求,操作类通讯,响应体转换
|
if (typeof responseData.status !== 'undefined' && typeof responseData.dataMsg !== 'undefined') {
|
// 转换数据体
|
let data2 = { rows: [] };
|
// 数组类型
|
if (responseData.dataMsg instanceof Array) {
|
if (responseData.dataMsg.length > 0) {
|
data2.rows = responseData.dataMsg;
|
}
|
}
|
// 对象类型
|
else if (responseData.dataMsg instanceof Object) {
|
if (!Tools.isEmptyObject(responseData.dataMsg)) {
|
data2 = responseData.dataMsg;
|
}
|
}
|
// 合并响应体
|
return {
|
state: {
|
code: responseData.status === 200 ? 2000 : 5000,
|
msg: responseData.msg,
|
},
|
data: data2,
|
};
|
}
|
// 旧版请求,数据列表类通讯,响应体转换
|
if (typeof responseData.data !== 'undefined' && typeof responseData.count !== 'undefined') {
|
const data = (!!responseData.data && typeof responseData.data !== 'object') ?
|
{ data: responseData.data } : null;
|
return {
|
state: {
|
code: responseData.code === 0 ? 2000 : 5000,
|
msg: responseData.msg,
|
},
|
data: {
|
rows: responseData.data || [],
|
...data,
|
total: responseData.count,
|
ext: responseData.ext,
|
},
|
};
|
}
|
}
|
|
/**
|
* 解析捕获的网络错误
|
* @param err
|
* @param options
|
* @param callback
|
* @private
|
*/
|
_resolveCaughtNetErr(err, options, callback) {
|
let msg = '';
|
if (err && err.status) {
|
switch (err.status) {
|
case 400:
|
msg += '通讯请求有误!(400 Bad Request)';
|
break;
|
case 401:
|
msg += '您的登录已失效!请重新登录!(401 Unauthorized)';
|
break;
|
case 403:
|
msg += '通讯请求被拒绝!(403 Forbidden)';
|
break;
|
case 404:
|
msg += '通讯请求不存在!(404 Not Found)';
|
break;
|
case 405:
|
msg += '通讯请求不允许访问!(405 Method Not Allowed)';
|
break;
|
case 500:
|
msg += '通讯服务器处理异常!(500 Internal Server Error)';
|
break;
|
case 502:
|
msg += '通讯网关异常!(502 Bad Gateway)';
|
break;
|
case 503:
|
msg += '通讯服务器维护中/已过载!(503 Service Unavailable)';
|
break;
|
case 504:
|
msg += '通讯网关已超时!(504 Gateway Timeout)';
|
break;
|
default:
|
msg += '网络通讯异常!(' + err.message + ')';
|
break;
|
}
|
} else {
|
msg += '解析通讯数据异常!';
|
}
|
callback(msg);
|
setTimeout(() => {
|
if (typeof options.silence === 'undefined' || !options.silence) {
|
this._message('fail', msg);
|
}
|
}, 20);
|
}
|
|
/**
|
* 转换响应体
|
* @param response
|
* @param options
|
* @returns {Object|{}|null}
|
* @private
|
*/
|
_transformResponseData(response, options) {
|
if (!response) {
|
return null;
|
}
|
if (response.state.code === 2000) {
|
if (!response.data) {
|
return {};
|
} else {
|
// 允许通用列表为 null
|
if (typeof response.data.rows !== 'undefined' && !Tools.isArray(response.data.rows)) {
|
response.data.rows = [];
|
}
|
// 先转驼峰
|
response.data = this.transKeyName('camel', response.data);
|
// 转换常规数字字符串为数值
|
response.data = this._transNumStringToNumber(response.data);
|
return response.data;
|
}
|
} else if (response.state.code === 2001) {
|
setTimeout(() => {
|
if (typeof options.silence === 'undefined' || !options.silence) {
|
this._message('info', response.state.msg);
|
}
|
}, 20);
|
return null;
|
} else if (response.state.code === 9001) {
|
// 在微信公众号中,每次进入即登录,登录失效关闭重进即可(进入链接带公司绑定码,页面没有存这个码,也不需要)
|
// 在小程序中,使用自动登录机制,自动登录失败才去授权页绑定账号
|
if (process.env.TARO_ENV === 'weapp') {
|
Taro.navigateTo({ url: '/pages/home/index/index?mode=login' });
|
}
|
// 在App中,跳转到首页取消登录
|
if (project.appHybrid) {
|
Taro.navigateTo({ url: '/pages/home/index/index?mode=logout' });
|
}
|
return null;
|
} else {
|
setTimeout(() => {
|
if (typeof options.silence === 'undefined' || !options.silence) {
|
this._message('error', response.state.msg);
|
}
|
}, 20);
|
return null;
|
}
|
}
|
|
/**
|
* 下划线字符串转小驼峰
|
* @param {String} str
|
* @return {String}
|
* @private
|
*/
|
stringToCamel(str) {
|
let str2 = '';
|
if (str.indexOf('_') <= 0) {
|
str2 = str;
|
} else {
|
let words = str.split('_');
|
for (let i = 1; i < words.length; i++) {
|
words[i] = words[i].substr(0, 1).toUpperCase() + words[i].substr(1);
|
}
|
str2 = words.join('');
|
}
|
return str2;
|
}
|
|
/**
|
* 小驼峰字符串转下划线
|
* @param {String} str
|
* @return {String}
|
* @private
|
*/
|
stringToUnderline(str) {
|
let str2 = '';
|
if ((/[A-Z]/).test(str)) {
|
str2 = str.replace(/([A-Z])/g, ($1) => {
|
return '_' + $1.toLowerCase();
|
});
|
} else {
|
str2 = str;
|
}
|
return str2;
|
}
|
|
/**
|
* 驼峰与下划线命名模式转换
|
* @param {String} type - 'camel' or 'underline'
|
* @param {Object} json
|
* @return {Object}
|
*/
|
transKeyName(type, json) {
|
const transform = (json, json2) => {
|
for (let p in json) {
|
if (json.hasOwnProperty(p)) {
|
let key;
|
// 数值键名直接传递
|
if (/^\d+$/.test(p)) {
|
key = parseInt(p);
|
}
|
// 字符串键名进行转换
|
else {
|
if (type === 'camel') {
|
key = this.stringToCamel(p);
|
} else if (type === 'underline') {
|
key = this.stringToUnderline(p);
|
}
|
}
|
// 属性为对象时,递归转换
|
if (json[p] instanceof Object) {
|
json2[key] = transform(json[p], Tools.isArray(json[p]) ? [] : {});
|
}
|
// 属性非对象,为字符串但内容符合json格式,递归转换
|
else if (Tools.isString(json[p]) && /^[{[]+("([a-zA-Z][a-zA-Z0-9\-_]*?)":(.+?))+[}\]]+$/.test(json[p])) {
|
json2[key] = JSON.parse(json[p]);
|
json2[key] = transform(json2[key], Tools.isArray(json2[key]) ? [] : {});
|
json2[key] = JSON.stringify(json2[key]);
|
}
|
// 属性非对象,非json字符串,直接传递
|
else {
|
json2[key] = json[p];
|
}
|
}
|
}
|
return json2;
|
};
|
return transform(json, Tools.isArray(json) ? [] : {});
|
}
|
|
/**
|
* 转换图片路径(旧版运营平台)
|
* @param {String} type - fix or cut
|
* @param {String} path
|
* @example
|
* fix -> '/upload/4/5e6c91eeccedc.jpg'
|
* cut -> '4/5e56307c489c7.jpg'
|
*/
|
transImgPath(type, path) {
|
if (!path) {
|
return '';
|
}
|
// 多项时,先拆分转换,再合并
|
if (path.indexOf(',') >= 0) {
|
return path.split(',').map(p => this.transImgPath(type, p)).join(',');
|
}
|
// 单项时转换
|
else {
|
// 修复补齐
|
if (type === 'fix') {
|
// 完整 URL,地址不变
|
if (/^http/.test(path)) {
|
return path;
|
}
|
// 绝对路径
|
if (/^(\/upload|\/static|\/mini|\/assets)/.test(path)) {
|
return $hostBoot.getHost() + path;
|
}
|
// 部分路径
|
else {
|
return $hostBoot.getHost() + '/upload/' + path;
|
}
|
}
|
// 裁剪多余部分
|
else if (type === 'cut') {
|
const pathArr = path.split('upload/');
|
return pathArr[pathArr.length - 1];
|
}
|
}
|
}
|
|
/**
|
* 转换简单的数字字符串为数值
|
* @param {*} json
|
* @return {*}
|
* @private
|
*/
|
_transNumStringToNumber(json) {
|
// 匹配 9 位数字,范围:999999999 ~ 0.0000001,排除正数且0开头的字符串
|
const simpleNumReg = /(^[1-9][\d.]{0,8}$)|(^-\d[\d.]{0,8}$)|(^\d(\.\d{0,7})?$)/;
|
// 匹配金额
|
const moneyStrReg = /^-?\d+(,\d+)*\.\d{2}$/;
|
const transNumber = (json) => {
|
for (let p in json) {
|
if (json.hasOwnProperty(p)) {
|
// 属性为对象时,递归转换
|
if (json[p] instanceof Object) {
|
transNumber(json[p]);
|
}
|
// 属性非对象,判断是否为简单数字字符串且不是金额,是则转换
|
else {
|
if (Tools.isString(json[p]) && simpleNumReg.test(json[p]) && !moneyStrReg.test(json[p])) {
|
json[p] = Number(json[p]);
|
}
|
}
|
}
|
}
|
};
|
transNumber(json);
|
return json;
|
}
|
|
/**
|
* 显示提示信息
|
* @param type
|
* @param msg
|
*/
|
_message(type, msg) {
|
Taro.showToast({
|
title: msg,
|
icon: 'none',
|
mask: false,
|
duration: (type === 'fail' || type === 'error') ? 3000 : 2000,
|
});
|
}
|
|
}
|