@ovine/core
Version:
Build flexible admin system with json.
462 lines (461 loc) • 17.9 kB
JavaScript
/**
* 封装 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;
});
}
}