UNPKG

@ovine/core

Version:

Build flexible admin system with json.

462 lines (461 loc) 17.9 kB
/** * 封装 fetch 请求 * TODO: 请求模块需要 编写测试用例 */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; /* eslint-disable consistent-return */ import { getApiCache, setApiCache } from 'amis/lib/utils/api'; import { object2formData, qsstringify, hasFile } from 'amis/lib/utils/helper'; import { filter } from 'amis/lib/utils/tpl'; import { get, map, isPlainObject, isFunction, toUpper, pick, assign } from 'lodash'; import { parse } from 'qs'; import { fetch } from 'whatwg-fetch'; import logger from "../logger"; import { getSessionStore, setSessionStore } from "../store"; import { isExpired, promisedTimeout, rmUrlRepeatSlant } from "../tool"; import { saveFile } from "../file"; const log = logger.getLogger('lib:utils:request'); // 请求错误集中处理, 必须 throw 错误 function requestErrorCtrl(error, option, response) { // log.info('requestErrorCtrl', { error, option, response }) const errorSource = { option, response, error }; let withInsErrorHook = true; // 如果返回 false,则不调用 全局的错误处理 if (option.onError) { withInsErrorHook = option.onError(response, option, error); } if (withInsErrorHook !== false && this.onError) { this.onError(response, option, error); } if (this.onFinish) { this.onFinish(response, option, error); } throw errorSource; } // 请求成功集中处理 function requestSuccessCtrl(response, option) { return __awaiter(this, void 0, void 0, function* () { if (this.onSuccess) { const res = wrapResponse(response); response.data = this.onSuccess(res.data, option, res); } if (option.onSuccess) { const res = wrapResponse(response); response.data = yield option.onSuccess(res.data, option, res); } if (this.onFinish) { this.onFinish(wrapResponse(response), option); } }); } // 模拟数据 function mockSourceCtrl(option) { return __awaiter(this, void 0, void 0, function* () { const { mockSource, api, method, url, mock = true, mockDelay = 200 } = option; if (!this.isMock || !mock || !mockSource) { return 'none'; } const apiStr = api || `${method} ${url || ''}`; // mock数据生成方式 const mockSourceGen = get(mockSource, apiStr) ? mockSource[apiStr] : mockSource; // mock 原始数据 const fakeRes = {}; fakeRes.data = isFunction(mockSourceGen) ? mockSourceGen(option) : mockSourceGen; yield requestSuccessCtrl.call(this, fakeRes, option); if (mockDelay) { yield promisedTimeout(mockDelay); } log.log('mockSource', option.url, fakeRes.data, option); return fakeRes; }); } // 缓存请求 只缓存 GET 请求 function cacheSourceCtrl(type, option, resource) { const { url = '', expired = 0, method = 'GET' } = option || {}; if (!expired || method !== 'GET') { return; } const timestampKey = `${url}:timestamp`; if (type === 'set') { // 不存在 resource 直接返回 if (!resource) { return; } // 所有数据按照 字符串缓存 setSessionStore(url, resource); setSessionStore(timestampKey, (Date.now() + expired).toString()); return; } if (type === 'get') { const cached = getSessionStore(url); const whenExpired = getSessionStore(timestampKey); if (cached && whenExpired) { if (!isExpired(whenExpired)) { log.log('expiredSource', option.url, cached, option); return cached; } } } } // 读取json结果,非JSON结果 在 request 模块处理 function readJsonResponse(response) { return __awaiter(this, void 0, void 0, function* () { try { const text = yield response.text(); response.responseText = text; const data = JSON.parse(text); delete response.responseText; // 如果解析成功 将responseText 参数删除 return data; } catch (e) { return {}; } }); } function saveFileFromRes(options) { const { blob, disposition } = options; const fileName = get(/filename="(.*)"$/.exec(disposition), '1'); saveFile(blob, fileName ? decodeURIComponent(fileName) : undefined); } // 发出 fetch 请求 function fetchSourceCtrl(option) { return __awaiter(this, void 0, void 0, function* () { const { url, body, config, responseType } = option; if (config.onUploadProgress && body && typeof XMLHttpRequest !== 'undefined') { const result = yield uploadWithProgress.call(this, option); return result; } const result = yield fetch(url, option) .catch((error) => { requestErrorCtrl.call(this, error, option, wrapResponse()); }) .then((response) => __awaiter(this, void 0, void 0, function* () { // 当 fetch 发生错误时 不做任何处理 if (!response) { return; } const status = Number(response.status); if (status <= 100 || status >= 400) { try { response.data = yield readJsonResponse(response); } catch (e) { // } requestErrorCtrl.call(this, new Error('status <= 100 || status >= 400'), option, wrapResponse(response)); return; } try { if (responseType === 'blob' || config.responseType === 'blob') { const blob = yield response.blob(); response.data = { blob }; saveFileFromRes({ blob, disposition: response.headers.get('Content-Disposition') || '' }); } else { response.data = yield readJsonResponse(response); } yield requestSuccessCtrl.call(this, response, option); return response; } catch (error) { requestErrorCtrl.call(this, error, option, wrapResponse(response)); } })); return result; }); } function fakeSourceCtrl(option) { return __awaiter(this, void 0, void 0, function* () { const fakeReq = option.onFakeRequest; const fakeRes = yield fakeReq(option); const fakeResponse = option.withoutWrapRes ? fakeRes : wrapResponse(fakeRes); return { data: fakeResponse, }; }); } // fetch 添加 onUploadProgress 支持 function uploadWithProgress(option) { const errorCtrl = requestErrorCtrl.bind(this); const successCtrl = requestSuccessCtrl.bind(this); return new Promise((resolve) => { var _a; const { config, method = '', url = '', headers = {}, body } = option; let xhr = new XMLHttpRequest(); xhr.open(method.toLowerCase(), url, true); // 兼容 withCredentials 与 credentials 参数 const credentials = (_a = option.fetchOptions) === null || _a === void 0 ? void 0 : _a.credentials; if (config.withCredentials || (credentials && credentials !== 'omit')) { xhr.withCredentials = true; } map(headers, (header, key) => { if (xhr) { xhr.setRequestHeader(key, header); } }); function onXhrError(status, text) { errorCtrl(new Error(text), option, wrapResponse({ status, data: (xhr === null || xhr === void 0 ? void 0 : xhr.response) || (xhr === null || xhr === void 0 ? void 0 : xhr.responseText), statusText: xhr === null || xhr === void 0 ? void 0 : xhr.statusText, }, true)); xhr = null; } xhr.onreadystatechange = function () { if (!xhr || xhr.readyState !== 4) { return; } if (xhr.status === 0 && !(xhr.responseURL && xhr.responseURL.indexOf('file:') === 0)) { return; } const responseHeaders = (xhr === null || xhr === void 0 ? void 0 : xhr.getAllResponseHeaders()) || {}; const response = { data: xhr.response || xhr.responseText, status: xhr.status, statusText: xhr.statusText, headers: responseHeaders, }; if (this.status <= 100 || this.status >= 400) { errorCtrl(new Error('status <= 100 || status >= 400'), option, wrapResponse(response, true)); } else { successCtrl(wrapResponse(response, true), option).then(() => { resolve(response); }); } }; xhr.onerror = function () { onXhrError(this.status, 'Network Error'); }; xhr.ontimeout = function () { onXhrError(this.status, 'TimeOut Error'); }; if (xhr.upload && config.uploadProgress) { xhr.upload.onprogress = config.uploadProgress; } xhr.send(body); }); } // 获取 fetch 参数 function getFetchOption(option) { const { headers, data = {}, fetchOptions, body, dataType = 'json', qsOptions } = option; const { url, method } = getUrlByOption.call(this, option); // 自行实现取消请求的回调 const { cancelExecutor, withCredentials } = option.config || {}; let signal = null; if (cancelExecutor && typeof AbortController !== 'undefined') { const controller = new AbortController(); signal = controller.signal; cancelExecutor(() => { controller.abort(); }); } /** * amis dataType 逻辑 */ // fetch 请求参数封装 let fetchBody; const fetchHeaders = headers; if (!/GET|HEAD|OPTIONS/i.test(method)) { if (data instanceof FormData || data instanceof Blob || data instanceof ArrayBuffer) { fetchBody = data; } else if (hasFile(data) || dataType === 'form-data') { fetchBody = object2formData(data, qsOptions); } else if (dataType === 'form') { fetchBody = qsstringify(data, qsOptions); fetchHeaders['Content-Type'] = 'application/x-www-form-urlencoded'; } else if (dataType === 'json') { fetchBody = JSON.stringify(assign({}, body, data)); fetchHeaders['Content-Type'] = 'application/json'; } } const fetchOption = Object.assign(Object.assign({}, fetchOptions), { signal, url, method, headers: fetchHeaders, body: fetchBody }); // 兼容 withCredentials 参数 if (withCredentials && !fetchOption.credentials) { fetchOption.credentials = 'include'; } return fetchOption; } // 确保 data 一定是对象 function wrapResponse(response, transJson) { if (!response) { const fakeRes = { data: {} }; return fakeRes; } if (typeof response.data === 'undefined') { response.data = {}; } else if (!isPlainObject(response.data)) { if (!transJson) { response.data = { value: response.data }; return response; } try { response.data = JSON.parse(response.data); return response; } catch (_) { // } } return response; } // 获取请求参数 function getReqOption(option) { return __awaiter(this, void 0, void 0, function* () { // 对象参数 先填充默认值 let opt = Object.assign(Object.assign({ fetchOptions: {}, headers: {}, config: {} }, option), normalizeUrl(option.url || option.api || '', option.method)); const { actionAddr, api, onPreRequest, onRequest } = opt; opt.api = api || option.url; opt.actionAddr = actionAddr || opt.api; if (!opt.url) { log.error('请求一定要传 url 参数', option); requestErrorCtrl.call(this, new Error('请求一定要传 url 参数'), wrapResponse()); } if (this.onPreRequest) { opt = this.onPreRequest(opt); } if (onPreRequest) { opt = yield onPreRequest(opt); } let reqOption = Object.assign(Object.assign({}, opt), getFetchOption.call(this, opt)); if (this.onRequest) { reqOption = this.onRequest(reqOption); } if (onRequest) { reqOption = onRequest(reqOption); } return reqOption; }); } // 处理格式化 URL 字符串 export function normalizeUrl(urlStr, defMethod) { let method = toUpper(defMethod); let url = urlStr; if (/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS) /i.test(url)) { const [apiMethod, apiStr] = urlStr.split(' '); method = apiMethod; url = apiStr; } return { url, method: (toUpper(method) || 'GET'), }; } // 获取 url 参数 export function getUrlByOption(option) { const { qsOptions, data, domain = 'api', domains, url: optUrl = '', method } = option; const urlOption = { url: optUrl, method }; const apiDomains = domains || this.domains || {}; let url = optUrl; // url中不存在 'http' 匹配 if (!/^https?:\/\//.test(url)) { const urlPrefix = apiDomains[domain]; if (!urlPrefix) { log.error('request.getUrlByOption 解析出错', option); } url = `${urlPrefix}/${url}`; } // 删除多于的斜杠 url = rmUrlRepeatSlant(url); const idx = url.indexOf('?'); const hashIdx = url.indexOf('#'); const hasString = hashIdx !== -1 ? url.substring(hashIdx) : ''; if (/\$/.test(url)) { url = filter(url, data); } // 添加 get 请求参数 if (urlOption.method === 'GET' && data) { if (idx !== -1) { const urlParams = parse(url.substring(idx + 1, hashIdx !== -1 ? hashIdx : undefined)); const params = Object.assign(Object.assign({}, urlParams), data); url = `${url.substring(0, idx)}?${qsstringify(params, qsOptions)}${hasString}`; } else { url += `?${qsstringify(data, qsOptions)}${hasString}`; } } urlOption.url = url; return urlOption; } // 使用 class 能够更容易重写 request 的一些回调值 export class Request { constructor(config) { // 配置的域名 this.domains = {}; this.setConfig(config); } // 设置配置 setConfig(config) { const { domains = {}, isMock } = config || {}; this.domains = domains; this.isMock = isMock; } // 解析请求参数 getUrlByOption(option) { return getUrlByOption.call(this, option); } // eslint-disable-next-line no-dupe-class-members request(option, params) { return __awaiter(this, void 0, void 0, function* () { const that = this; if (params) { option.data = Object.assign(Object.assign({}, option.data), params); } // 获取请求参数 const reqOption = yield getReqOption.call(that, option); const { onFakeRequest } = option; // 命中缓存 直接返回 const cachedResponse = cacheSourceCtrl('get', reqOption); if (cachedResponse) { return { data: cachedResponse, }; } // mock 数据拦截 const mockSource = yield mockSourceCtrl.call(that, reqOption); if (mockSource !== 'none') { cacheSourceCtrl('set', reqOption, mockSource.data); return mockSource; } // 兼容 cache 参数, 用于多请求并发情况 if (reqOption.method === 'GET' && reqOption.cache && reqOption.cache > 0) { const apiObj = pick(reqOption, ['url', 'cache', 'method', 'data']); const apiCache = getApiCache(apiObj); log.debounce(() => log.log(onFakeRequest ? 'fakeCacheSource' : 'cacheSource', reqOption.url, reqOption)); if (apiCache) { return apiCache.cachedPromise; } // 伪装 请求 也支持缓存 const cachedPromise = onFakeRequest ? fakeSourceCtrl.call(that, reqOption) : fetchSourceCtrl.call(that, reqOption); setApiCache(apiObj, cachedPromise); return cachedPromise; } // 伪装 请求 直接返回数据 if (onFakeRequest) { const fakeResponse = yield fakeSourceCtrl.call(that, reqOption); log.log('[fakeSource]', option.url, fakeResponse.data, reqOption); return fakeResponse; } const response = yield fetchSourceCtrl.call(that, reqOption); cacheSourceCtrl('set', reqOption, response.data); log.log('[apiSource]', reqOption.url, response.data, reqOption); return response; }); } }