import koaCompose from 'koa-compose'; import { Options as JsonpOptions } from 'fetch-jsonp'; import { AxiosRequestConfig as AxiosOptions, Method } from 'axios'; import { version } from '../package.json'; import { map, isWx, pipe, runFn, logger, mergeAll, combineUrls, isNullOrUndefined, getParamStrFromObj, apiConfigToReqFnParams, } from './utils'; import { ERROR_STRINGS, VALID_REQ_TYPES, } from './constants'; import { getWxPromise, getAxiosPromise, getFetchJsonpPromise, } from './adapters'; import { formatResDataMiddleware, recordReqTimeMiddleware, setReqFnParamsMiddleware, recordStartTimeMiddleware, formatReqParamsMiddleware, } from './middlewareFns'; import { Ctx, CtxReq, CtxRes, ReqType, Middleware, WxApiConfig, WebApiConfig, SelfApiConfig, CtxReqFnParams, RuntimeOptions, DefaultApiConfig, AnyPromiseFunction, } from './interfaces'; class TuaApi { baseUrl: string; reqType: ReqType; middleware: Middleware[]; customFetch: AnyPromiseFunction; axiosOptions: AxiosOptions; jsonpOptions: JsonpOptions; defaultErrorData: any; constructor({ baseUrl, reqType, middleware = [], customFetch, axiosOptions = {}, jsonpOptions = {}, defaultErrorData = { code: 999, msg: '出错啦!' }, }: DefaultApiConfig = {}) { logger.log(`Version: ${version}`); this.baseUrl = baseUrl; this.middleware = middleware; this.customFetch = customFetch; this.axiosOptions = axiosOptions; this.jsonpOptions = jsonpOptions; this.defaultErrorData = defaultErrorData; if (customFetch) { this.reqType = 'custom'; } else { this.reqType = reqType ?? (isWx() ? 'wx' : 'axios'); } this._checkReqType(this.reqType); if (reqType && reqType !== 'custom' && customFetch) { throw TypeError(ERROR_STRINGS.reqTypeAndCustomFetch); } return this; } /* -- 各种对外暴露方法 -- */ /** * 添加一个中间件函数 * @param fn * @return self */ use(fn: Middleware) { if (typeof fn !== 'function') { throw TypeError(ERROR_STRINGS.middleware); } this.middleware.push(fn); return this; } /** * 根据 apiConfig 生成请求函数组成的 map */ getApi(apiConfig: WebApiConfig | WxApiConfig): T { return pipe( apiConfigToReqFnParams, map(this._getOneReqMap.bind(this)), mergeAll, )(apiConfig); } /* -- 各种私有方法 -- */ _reqFn(options: CtxReqFnParams) { const { url, mock, body, header, method: rawMethod, reqType: rawReqType, reqParams, callback, callbackName, axiosOptions, jsonpOptions: rawJsonpOptions, ...rest } = options; this._checkReqType(rawReqType); if (mock) { const resData = { ...runFn(mock, reqParams) }; return Promise.resolve({ data: resData }); } const method = rawMethod.toLowerCase() as Method; const reqType = rawReqType.toLowerCase() as ReqType; if (reqType === 'custom') { return rest.customFetch({ url, data: reqParams, body, method, header, ...rest }); } if (reqType === 'wx') { return getWxPromise({ url, data: reqParams, method, header, ...rest }); } if (reqType === 'axios' || method === 'post') { return getAxiosPromise({ ...axiosOptions, url, data: body, method, params: reqParams, headers: header ?? axiosOptions.headers, }); } const paramsStr = getParamStrFromObj(reqParams); const fullUrl = paramsStr ? `${url}${url.includes('?') ? '&' : '?'}${paramsStr}` : url; const jsonpOptions = { ...rawJsonpOptions, charset: rawJsonpOptions.charset || 'UTF-8', jsonpCallback: callback || rawJsonpOptions.jsonpCallback, jsonpCallbackFunction: callbackName || rawJsonpOptions.jsonpCallbackFunction, }; return getFetchJsonpPromise(fullUrl, jsonpOptions); } /** * 检查 reqType 是否合法 */ _checkReqType(reqType) { if (VALID_REQ_TYPES.indexOf(reqType) !== -1) return; throw TypeError(ERROR_STRINGS.reqTypeFn(reqType)); } /** * 组合生成中间件函数 * @param {function[]} middleware * @param {Boolean} useGlobalMiddleware 是否使用全局中间件 */ _getMiddlewareFn(middleware, useGlobalMiddleware) { const middlewareFns = useGlobalMiddleware ? this.middleware.concat(middleware) : middleware; return koaCompose([ // 记录开始时间 recordStartTimeMiddleware, // 格式化生成请求参数 formatReqParamsMiddleware, // 业务侧中间件函数数组 ...middlewareFns, // 生成 _reqFn 参数 setReqFnParamsMiddleware, // 统一转换响应数据为对象 formatResDataMiddleware, // 记录结束时间 recordReqTimeMiddleware, // 发起请求 (ctx, next) => next() .then(() => this._reqFn(ctx.req.reqFnParams)) // 暂存出错,保证 afterFn 能执行(finally) .catch(error => ({ // 浅拷贝一份默认出错值 data: { ...this.defaultErrorData }, error, })) .then((res) => { ctx.res = res; }), ]); } _getOneReqMap({ mock, name, path, body, prefix = '/', method = 'get', baseUrl = this.baseUrl, params: rawParams = {}, afterFn = ([x]) => x, beforeFn = Promise.resolve.bind(Promise), middleware = [], useGlobalMiddleware = true, ...rest }: SelfApiConfig) { // 优先使用 name const apiName = name ?? String(path); /* 合并全局默认值 */ const defaultOptions = this.mergeByDefault(rest); /** * 被业务侧调用的请求函数 */ const apiFn = ( rawArgs: any, runtimeOptions: RuntimeOptions = {}, ) => { const isUseParams = ['post', 'put'].every(x => x !== method); const args = (() => { const result = isUseParams ? rawArgs : null; return result ?? runtimeOptions.params ?? {}; })(); const params = runFn(rawParams, args); // 最终的运行时配置,runtimeOptions 有最高优先级 const runtimeParams = { path, params, prefix, method, apiName, baseUrl, fullPath: combineUrls(prefix, String(path)), ...defaultOptions, ...runtimeOptions, }; // check required body if (body?.required || body?.isRequired) { const hasNoBodyAsArgs = ( !isUseParams && isNullOrUndefined(rawArgs) && isNullOrUndefined(runtimeParams.body) ); const hasNoBodyAtRuntime = ( isUseParams && isNullOrUndefined(runtimeParams.body) ); if (hasNoBodyAsArgs || hasNoBodyAtRuntime) { return Promise.reject(ERROR_STRINGS.requiredBodyFn(apiName)); } } // 请求的上下文信息 const ctx: { req: Partial, res: Partial, } = { req: { args, mock: apiFn.mock, // @ts-ignore 后续流程再补充其他属性 reqFnParams: {}, ...runtimeParams, body: isUseParams ? runtimeOptions.body : (rawArgs ?? {}), }, res: {}, }; // 执行完 beforeFn 后执行的函数 const beforeFnCb = (rArgs: { header?: Record, params?: Record, } = {}) => { // 传递请求头 if (rArgs.header) { ctx.req.reqFnParams.header = rArgs.header; } if (!rArgs.params) return; // 可以通过给 beforeFn 添加 params 返回值来添加通用参数 ctx.req.params = { ...params, ...rArgs.params }; }; // 中间件函数 const middlewareFn = this._getMiddlewareFn(middleware, useGlobalMiddleware); return beforeFn() .then(beforeFnCb) .then(() => middlewareFn(ctx)) .then(() => afterFn([ctx.res.data, ctx as Ctx])) .then(data => (ctx.res.error ? Promise.reject(ctx.res.error) : data || ctx.res.data)); }; apiFn.key = combineUrls(prefix, apiName); apiFn.url = combineUrls(combineUrls(baseUrl, prefix), String(path)); apiFn.mock = mock; apiFn.params = rawParams; return { [apiName]: apiFn }; } private mergeByDefault(options: { reqType?: ReqType, customFetch?: any, axiosOptions?: AxiosOptions, jsonpOptions?: JsonpOptions, }) { const { reqType, customFetch, axiosOptions, jsonpOptions } = options; const result = { ...options }; if (reqType && customFetch) { if (reqType.toLowerCase() !== 'custom') { logger.warn(ERROR_STRINGS.reqTypeAndCustomFetch); } result.reqType = 'custom'; } else if (customFetch || this.customFetch) { // 没有配置 reqType,但配了公共配置或默认配置的 customFetch result.reqType = 'custom'; } else { // 没有配置 customFetch result.reqType = reqType || this.reqType; } result.customFetch = customFetch || this.customFetch; result.axiosOptions = axiosOptions ? { ...this.axiosOptions, ...axiosOptions } : this.axiosOptions; result.jsonpOptions = jsonpOptions ? { ...this.jsonpOptions, ...jsonpOptions } : this.jsonpOptions; return result; } } export default TuaApi; export * from './exportUtils'; export * from './middlewareFns';