/** * Fetcher * @author Tevin */ import Taro from '@tarojs/taro'; import Qs from 'qs'; import { Tools } from '@components/common/Tools'; import project from '@project'; export class Fetcher { /** * @constructor * @param {Object} options */ constructor(options = {}) { this._data = { urlPrefix: options.urlPrefix || ['/api/common/', '/api/common/'], }; this._defaultConfig.url = Fetcher.host; } /** * 请求配置 * @private */ _defaultConfig = { 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 = ''; // 开发环境地址 if (Fetcher.inDevMod) { url = this._data.urlPrefix[0] + devSuffix; } // 生产环境地址 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 {(String[])[]} [remap] * @param {object} [options] * @return {Promise} */ get(url, data, remap = [], options = null) { const params = Qs.stringify(data); if (url.indexOf('?') >= 0) { url += '&' + params; } else { url += '?' + params; } return this.query('get', url, null, remap, options); } /** * post 请求 * @param {String} url * @param {*} data * @param {(String[])[]} [remap] * @param {object} [options] * @return {Promise} */ post(url, data, remap = [], options = null) { 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, remap, options); } /** * 基础 ajax 请求 * @param {String} type * @param {String} url * @param {*} [data] * @param {*} [remap] * @param {object} [options] * @return {Promise|} */ query(type, url, data = null, remap, options = {}) { return new Promise((resolve, reject) => { Taro.request({ ...this._defaultConfig, url: this._defaultConfig.url + url, method: type.toUpperCase(), data, success: response => { /** * @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, remap)); }, fail: error => { this._resolveCaughtNetErr(error); reject(null); }, }); }); } /** * 适配旧版响应体,转换为新版 * @private */ _adaptiveResponseData(responseData) { // 标准请求,不转换 if (typeof responseData.state === 'object' && typeof responseData.data === 'object') { return responseData; } // 旧请求,操作类通讯,响应体转换 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 * @private */ _resolveCaughtNetErr(err) { 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 += '解析通讯数据异常!'; } this.message('error', msg); } /** * 转换响应体 * @param response * @param {Array[]} remap * @returns {Object|{}|null} * @private */ _transformResponseData(response, remap) { 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); // 再重映射 if (remap && remap.length > 0) { response.data = this._remapData(response.data, remap); } // 转换常规数字字符串为数值 response.data = this._transNumStringToNumber(response.data); return response.data; } } else if (response.state.code === 2001) { this.message('info', response.state.msg); return null; } else if (response.state.code === 9001) { // this._showLoginExpired(); } else { this.message('error', response.state.msg); return null; } } /** * 转换响应体数据结构 * @param {Object} data * @param {Array[]} maps * @example maps: [ * ['rows.[]', 'recvName', 'userName'] // 默认语法:键名转换(路径,旧名,新名) * ] * @private */ _remapData(data, maps) { // 渡值 const ferryValue = (source, paths, map) => { // 最后一环,传值 if (paths.length === 0) { // 目标已有值,跳过传值不覆盖 if (typeof source[map[2]] !== 'undefined') { return; } // 来源没有值,赋值空字符串 if (typeof source[map[1]] === 'undefined') { source[map[2]] = ''; } // 来源有值,直接赋值 else { source[map[2]] = source[map[1]]; } delete source[map[1]]; return; } // 提取当前环节 const curPath = paths.shift(); if (curPath === '[]') { source.forEach(item => { ferryValue(item, [...paths], map); }); } else { ferryValue(source[curPath], [...paths], map); } }; for (let map of maps) { // 键名转换 if (map[0].indexOf('.') >= 0) { const paths = map[0].split('.'); ferryValue(data, paths, map); } else { if (map[0].length > 0) { ferryValue(data, [map[0]], map); } else { ferryValue(data, [], map); } } } return data; } /** * 下划线字符串转小驼峰 * @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)/.test(path)) { return Fetcher.host + path; } // 部分路径 else { return Fetcher.host + '/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: true, duration: type === 'error' ? 5000 : 3000, }); } /** * 记录是否为本地开发模式 * @type {Boolean} */ static inDevMod = (() => { // 网页 if (process.env.TARO_ENV === 'h5') { // 当没有 url 指定时,只有内网 ip 和 33**/35** 的端口号,视为本地开发模式 return /^(192|127|localhost).*?:3[35]\d{2}$/i.test(window.location.host); } // 小程序 else if (process.env.TARO_ENV === 'weapp') { // 开发编译 if (process.env.NODE_ENV === 'development') { return true; } // 生产编译 else if (process.env.NODE_ENV === 'production') { return false; } } })(); /** * 当前服务器主机地址 * @type {String} */ static host = (() => { // 网页 if (process.env.TARO_ENV === 'h5') { // 开发 if (Fetcher.inDevMod) { return window.location.protocol + '//' + window.location.host; } // 生产 else { // 如果网址参数有指定服务器类型,匹配指定的服务器地址 const sever = Tools.getUrlParam('sever'); if (sever && typeof project.host.hosts[sever] !== 'undefined') { return project.host.hosts[sever]; } // 网页域名提取服务器地址 else if (window.location.protocol.indexOf('http') >= 0) { return window.location.protocol + '//' + window.location.host; } // 非 http 协议打开时,使用设置的服务器地址 else if (project.host.server) { return project.host.server; } } } // 小程序 else if (process.env.TARO_ENV === 'weapp') { // 开发 if (Fetcher.inDevMod) { return project.host.hosts[project.host.devType]; } // 生产 else { return project.host.server; } } })(); }