UNPKG

fastlion-amis

Version:

一种MIS页面生成工具

669 lines (581 loc) 16.9 kB
import omit from 'lodash/omit'; import { Api, ApiObject, EventTrack, fetcherResult, Payload } from '../types'; import { fetcherConfig } from '../factory'; import { tokenize, dataMapping, escapeHtml } from './tpl-builtin'; import { evalExpression } from './tpl'; import { isObject, isObjectShallowModified, hasFile, object2formData, qsstringify, cloneObject, createObject, qsparse, uuid } from './helper'; import isPlainObject from 'lodash/isPlainObject'; import { debug } from './debug'; import { message } from 'antd'; const rSchema = /(?:^|raw\:)(get|post|put|delete|patch|options|head|jsonp):/i; interface ApiCacheConfig extends ApiObject { cachedPromise: Promise<any>; requestTime: number; } const apiCaches: Array<ApiCacheConfig> = []; const isIE = !!(document as any).documentMode; export function normalizeApi( api: Api, defaultMethod: string = 'get' ): ApiObject { if (typeof api === 'string') { let method = rSchema.test(api) ? RegExp.$1 : ''; method && (api = api.replace(method + ':', '')); api = { method: (method || defaultMethod) as any, url: api }; } else { api = { ...api }; } return api; } export function buildApi( api: Api, data?: object, options: { autoAppend?: boolean; ignoreData?: boolean; [propName: string]: any; } = {} ): ApiObject { api = normalizeApi(api, options.method); const { autoAppend, ignoreData, ...rest } = options; api.config = { ...rest }; api.method = (api.method || (options as any).method || 'get').toLowerCase(); if (api.headers) { api.headers = dataMapping(api.headers, data, undefined, false); } if (api.requestAdaptor && typeof api.requestAdaptor === 'string') { api.requestAdaptor = str2function(api.requestAdaptor, 'api') as any; } if (api.adaptor && typeof api.adaptor === 'string') { api.adaptor = str2function( api.adaptor, 'payload', 'response', 'api' ) as any; } if (!data) { return api; } else if ( data instanceof FormData || data instanceof Blob || data instanceof ArrayBuffer ) { api.data = data; return api; } const raw = (api.url = api.url || ''); const idx = api.url.indexOf('?'); if (~idx) { const hashIdx = api.url.indexOf('#'); const params = qsparse( api.url.substring( idx + 1, ~hashIdx && hashIdx > idx ? hashIdx : undefined ) ); api.url = tokenize(api.url.substring(0, idx + 1), data, '| url_encode') + qsstringify( (api.query = dataMapping(params, data, undefined, api.convertKeyToPath)) ) + (~hashIdx && hashIdx > idx ? api.url.substring(hashIdx) : ''); } else { api.url = tokenize(api.url, data, '| url_encode'); } if (ignoreData) { return api; } if (api.data) { api.body = api.data = dataMapping( api.data, data, undefined, api.convertKeyToPath ); } else if (api.method === 'post' || api.method === 'put') { api.body = api.data = cloneObject(data); } // get 类请求,把 data 附带到 url 上。 if (api.method === 'get' || api.method === 'jsonp') { if (!~raw.indexOf('$') && !api.data && autoAppend) { api.query = api.data = data; } else if ( api.attachDataToQuery === false && api.data && !~raw.indexOf('$') && autoAppend ) { const idx = api.url.indexOf('?'); if (~idx) { let params = (api.query = { ...qsparse(api.url.substring(idx + 1)), ...data }); api.url = api.url.substring(0, idx) + '?' + qsstringify(params); } else { api.query = data; api.url += '?' + qsstringify(data); } } if (api.data && api.attachDataToQuery !== false) { const idx = api.url.indexOf('?'); if (~idx) { let params = (api.query = { ...qsparse(api.url.substring(idx + 1)), ...api.data }); api.url = api.url.substring(0, idx) + '?' + qsstringify(params); } else { api.query = api.data; api.url += '?' + qsstringify(api.data); } delete api.data; } } if (api.headers) { api.headers = dataMapping(api.headers, data); } if (api.requestAdaptor && typeof api.requestAdaptor === 'string') { api.requestAdaptor = str2function(api.requestAdaptor, 'api') as any; } if (api.adaptor && typeof api.adaptor === 'string') { api.adaptor = str2function( api.adaptor, 'payload', 'response', 'api' ) as any; } return api; } export function str2function( contents: string, ...args: Array<string> ): Function | null { try { let fn = new Function(...args, contents); return fn; } catch (e) { console.warn(e); return null; } } const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor; export function str2AsyncFunction( contents: string, ...args: Array<string> ): Function | null { try { let fn = new AsyncFunction(...args, contents); return fn; } catch (e) { console.warn(e); return null; } } export function responseAdaptor(ret: fetcherResult, api: ApiObject) { let data = ret.data; let hasStatusField = true; // 服务器返回的错误码捕获,504: 网关超时 if ([504].includes(ret.status)) { return { data: { data: [], status: 0, msg: '查询数据超时,未获得数据' }, headers: {}, msg: '查询数据超时,未获得数据', msgTimeout: 10000, ok: false, reqUrl: api.url, status: 1 } } if (!data) { throw new Error('Response is empty'); } // 返回内容是 string,说明 content-type 不是 json,这时可能是返回了纯文本或 html if (typeof data === 'string') { // 如果是文本类型就尝试解析一下 if ( ret.headers && ((ret.headers as any)['content-type'] || '').startsWith('text/') ) { try { data = JSON.parse(data); if (typeof data === 'undefined') { throw new Error('Response should be JSON'); } } catch (e) { const responseBrief = typeof data === 'string' ? escapeHtml((data as string).substring(0, 100)) : ''; throw new Error(`Response should be JSON\n ${responseBrief}`); } } else { if (api.responseType === 'blob') { throw new Error('Should have "Content-Disposition" in Header'); } else { throw new Error( `Content type is wrong "${(ret.headers as any)['content-type']}"` ); } } } // 兼容几种常见写法 if (data.hasOwnProperty('errorCode')) { // 阿里 Java 规范 data.status = data.errorCode; data.msg = data.errorMessage; } else if (data.hasOwnProperty('errno')) { data.status = data.errno; data.msg = data.errmsg || data.errstr || data.msg; } else if (data.hasOwnProperty('no')) { data.status = data.no; data.msg = data.error || data.msg; } else if (data.hasOwnProperty('error')) { // Google JSON guide // https://google.github.io/styleguide/jsoncstyleguide.xml#error if (typeof data.error === 'object' && data.error.hasOwnProperty('code')) { data.status = data.error.code; data.msg = data.error.message; } else { data.status = data.error; data.msg = data.errmsg || data.msg; } } if (!data.hasOwnProperty('status')) { hasStatusField = false; } const payload: Payload = { // Jay 20001,20002,...是菜鸟打印的接口的status // chencicsy 10003 是action二次确认的status ok: hasStatusField === false || data.status == 0 || [20001, 20002, 20003, 10003, 10005].includes(data.status), status: hasStatusField === false ? 0 : data.status, msg: data.msg || data.message, msgTimeout: data.msgTimeout, data: !data.data && !hasStatusField ? data : data.data, // 兼容直接返回数据的情况 /** * @author:chencicsy * @description: 获取文件流形式下载的头部信息 @param {{ headers: any }} */ headers: ret.headers, reqUrl: ret?.config?.baseURL }; // 兼容返回 schema 的情况,用于 app 模式 if (data && data.type) { payload.data = data; } if (payload.status == 422) { payload.errors = data.errors; } debug('api', 'response', payload); if (payload.ok && api.responseData) { debug('api', 'before dataMapping', payload.data); const responseData = dataMapping( api.responseData, createObject( { api }, (Array.isArray(payload.data) ? { items: payload.data } : payload.data) || {} ) ); debug('api', 'after dataMapping', responseData); payload.data = responseData; } if ([20001, 20002, 20003, 10003, 10005].includes(data.status)) { payload.data.status = data.status; payload.msg = '' } if ([10004].includes(data.status)) { if (!payload?.data) { payload.data = {} } payload.data.status = data.status; payload.msg = data?.data?.showText ? data?.data?.showText : data?.msg; } // [20001, 20002, 10003].includes(data.status) && (payload.data.status = data.status) // 20001,20002,...是菜鸟打印的接口的status return payload; } export function wrapFetcher( fn: (config: fetcherConfig) => Promise<fetcherResult>, tracker?: (eventTrack: EventTrack, data: any) => void ): (api: Api, data: object, options?: object) => Promise<Payload | void> { return function (api, data, options) { api = buildApi(api, data, options) as ApiObject; if (api.requestAdaptor) { debug('api', 'before requestAdaptor', api); api = api.requestAdaptor(api) || api; debug('api', 'after requestAdaptor', api); } if (api.data && (hasFile(api.data) || api.dataType === 'form-data')) { api.data = api.data instanceof FormData ? api.data : object2formData(api.data, api.qsOptions); } else if ( api.data && typeof api.data !== 'string' && api.dataType === 'form' ) { api.data = qsstringify(api.data, api.qsOptions) as any; api.headers = api.headers || (api.headers = {}); api.headers['Content-Type'] = 'application/x-www-form-urlencoded'; } else if ( api.data && typeof api.data !== 'string' && api.dataType === 'json' ) { api.data = JSON.stringify(api.data) as any; api.headers = api.headers || (api.headers = {}); api.headers['Content-Type'] = 'application/json'; } debug('api', 'request api', api); tracker?.( { eventType: 'api', eventData: omit(api, ['config', 'data', 'body']) }, api.data ); if (api.method?.toLocaleLowerCase() === 'jsonp') { return wrapAdaptor(jsonpFetcher(api), api); } if (typeof api.cache === 'number' && api.cache > 0) { const apiCache = getApiCache(api); return wrapAdaptor( apiCache ? (apiCache as ApiCacheConfig).cachedPromise : setApiCache(api, fn(api)), api ); } // IE 下 get 请求会被缓存,所以自动加个时间戳 if (isIE && api && api.method?.toLocaleLowerCase() === 'get') { const timeStamp = `_t=${Date.now()}`; if (api.url.indexOf('?') === -1) { api.url = api.url + `?${timeStamp}`; } else { api.url = api.url + `&${timeStamp}`; } } return wrapAdaptor(fn(api), api); }; } export function wrapAdaptor(promise: Promise<fetcherResult>, api: ApiObject) { const adaptor = api.adaptor; return adaptor ? promise .then(async response => { debug('api', 'before adaptor data', (response as any).data); let result = adaptor((response as any).data, response, api); if (result?.then) { result = await result; } debug('api', 'after adaptor data', result); return { ...response, data: result }; }) .then(ret => responseAdaptor(ret, api)) : promise.then(ret => responseAdaptor(ret, api)); } export function jsonpFetcher(api: ApiObject): Promise<fetcherResult> { return new Promise((resolve, reject) => { let script: HTMLScriptElement | null = document.createElement('script'); let src = api.url; script.async = true; function remove() { if (script) { // @ts-ignore script.onload = script.onreadystatechange = script.onerror = null; if (script.parentNode) { script.parentNode.removeChild(script); } script = null; } } const jsonp = api.query?.callback || 'axiosJsonpCallback' + uuid(); const old = (window as any)[jsonp]; (window as any)[jsonp] = function (responseData: any) { (window as any)[jsonp] = old; const response = { data: responseData, status: 200, headers: {} }; resolve(response); }; const additionalParams: any = { _: new Date().getTime(), _callback: jsonp }; src += (src.indexOf('?') >= 0 ? '&' : '?') + qsstringify(additionalParams); // @ts-ignore IE 为script.onreadystatechange script.onload = script.onreadystatechange = function () { // @ts-ignore if (!script.readyState || /loaded|complete/.test(script.readyState)) { remove(); } }; script.onerror = function () { remove(); const errResponse = { status: 0, headers: {} }; reject(errResponse); }; script.src = src; document.head.appendChild(script); }); } export function isApiOutdated( prevApi: Api | undefined, nextApi: Api | undefined, prevData: any, nextData: any ): nextApi is Api { if (!nextApi) { return false; } else if (!prevApi) { return true; } nextApi = normalizeApi(nextApi); if (nextApi.autoRefresh === false) { return false; } const trackExpression = nextApi.trackExpression ?? nextApi.url; if (typeof trackExpression !== 'string' || !~trackExpression.indexOf('$')) { return false; } prevApi = normalizeApi(prevApi); let isModified = false; if (nextApi.trackExpression || prevApi.trackExpression) { isModified = tokenize(prevApi.trackExpression || '', prevData) !== tokenize(nextApi.trackExpression || '', nextData); } else { prevApi = buildApi(prevApi as Api, prevData as object, { ignoreData: true }); nextApi = buildApi(nextApi as Api, nextData as object, { ignoreData: true }); isModified = prevApi.url !== nextApi.url; } return !!( isModified && isValidApi(nextApi.url) && (!nextApi.sendOn || evalExpression(nextApi.sendOn, nextData)) ); } export function isValidApi(api: string) { return ( api && /^(?:(https?|wss?|taf):\/\/[^\/]+)?(\/?[^\s\/\?]*){1,}(\?.*)?$/.test(api) ); } export function isEffectiveApi( api?: Api, data?: any, initFetch?: boolean, initFetchOn?: string ): api is Api { if (!api) { return false; } if (initFetch === false) { return false; } if (initFetchOn && data && !evalExpression(initFetchOn, data)) { return false; } if (typeof api === 'string' && api.length) { return true; } else if (isObject(api) && (api as ApiObject).url) { if ( (api as ApiObject).sendOn && data && !evalExpression((api as ApiObject).sendOn as string, data) ) { return false; } return true; } return false; } export function isSameApi( apiA: ApiObject | ApiCacheConfig, apiB: ApiObject | ApiCacheConfig ): boolean { return ( apiA.method === apiB.method && apiA.url === apiB.url && !isObjectShallowModified(apiA.data, apiB.data, false) ); } export function getApiCache(api: ApiObject): ApiCacheConfig | undefined { // 清理过期cache const now = Date.now(); let result: ApiCacheConfig | undefined; for (let idx = 0, len = apiCaches.length; idx < len; idx++) { const apiCache = apiCaches[idx]; if (now - apiCache.requestTime > (apiCache.cache as number)) { apiCaches.splice(idx, 1); len--; idx--; continue; } if (isSameApi(api, apiCache)) { result = apiCache; break; } } return result; } export function setApiCache( api: ApiObject, promise: Promise<any> ): Promise<any> { apiCaches.push({ ...api, cachedPromise: promise, requestTime: Date.now() }); return promise; } export function clearApiCache() { apiCaches.splice(0, apiCaches.length); } export function normalizeApiResponseData(data: any) { if (typeof data === 'undefined') { data = {}; } else if (!isPlainObject(data)) { data = { [Array.isArray(data) ? 'items' : 'result']: data }; } return data; } // window.apiCaches = apiCaches;