UNPKG

amis

Version:

一种MIS页面生成工具

435 lines (379 loc) 10.7 kB
import {Api, ApiObject, fetcherResult, Payload} from '../types'; import {fetcherConfig} from '../factory'; import {tokenize, dataMapping} from './tpl-builtin'; import qs from 'qs'; import {evalExpression} from './tpl'; import { isObject, isObjectShallowModified, hasFile, object2formData, qsstringify, cloneObject, createObject } from './helper'; const rSchema = /(?:^|raw\:)(get|post|put|delete|patch|options|head):/i; interface ApiCacheConfig extends ApiObject { cachedPromise: Promise<any>; requestTime: number; } const apiCaches: Array<ApiCacheConfig> = []; 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 (!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 = qs.parse( api.url.substring(idx + 1, ~hashIdx ? hashIdx : undefined) ); api.url = tokenize(api.url.substring(0, idx + 1), data, '| url_encode') + qsstringify((api.query = dataMapping(params, data))) + (~hashIdx ? 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); } else if (api.method === 'post' || api.method === 'put') { api.body = api.data = cloneObject(data); } // get 类请求,把 data 附带到 url 上。 if (api.method === 'get') { 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 = { ...qs.parse(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 = { ...qs.parse(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; } 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; } } export function responseAdaptor(ret: fetcherResult, api: ApiObject) { const data = ret.data; let hasStatusField = true; if (!data) { throw new Error('Response is empty!'); } // 兼容几种常见写法 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 = { ok: hasStatusField === false || data.status == 0, status: hasStatusField === false ? 0 : data.status, msg: data.msg || data.message, msgTimeout: data.msgTimeout, data: !data.data && !hasStatusField ? data : data.data // 兼容直接返回数据的情况 }; // 兼容返回 schema 的情况,用于 app 模式 if (data && data.type) { payload.data = data; } if (payload.status == 422) { payload.errors = data.errors; } if (payload.ok && api.responseData) { payload.data = dataMapping( api.responseData, createObject( {api}, (Array.isArray(payload.data) ? { items: payload.data } : payload.data) || {} ) ); } return payload; } export function wrapFetcher( fn: (config: fetcherConfig) => Promise<fetcherResult> ): (api: Api, data: object, options?: object) => Promise<Payload | void> { return function (api, data, options) { api = buildApi(api, data, options) as ApiObject; api.requestAdaptor && (api = api.requestAdaptor(api) || api); if (api.data && (hasFile(api.data) || api.dataType === 'form-data')) { 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'; } if (typeof api.cache === 'number' && api.cache > 0) { const apiCache = getApiCache(api); return wrapAdaptor( apiCache ? (apiCache as ApiCacheConfig).cachedPromise : setApiCache(api, fn(api)), api ); } return wrapAdaptor(fn(api), api); }; } export function wrapAdaptor(promise: Promise<fetcherResult>, api: ApiObject) { const adaptor = api.adaptor; return adaptor ? promise .then(async response => { let result = adaptor((response as any).data, response, api); if (result?.then) { result = await result; } return { ...response, data: result }; }) .then(ret => responseAdaptor(ret, api)) : promise.then(ret => responseAdaptor(ret, api)); } 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); } // window.apiCaches = apiCaches;