From 0b6106b259621df2351e4c52c9cf2f7d83e83d43 Mon Sep 17 00:00:00 2001 From: StEve Young <2747745470@qq.com> Date: Tue, 21 Jul 2020 15:47:56 +0800 Subject: [PATCH] feat: v1.6.0 suport customFetch, support array args, add ctx.res.rawData (#47) * feat: add customFetch, add ctx.res.rawData * feat: support array args * fix(utils): combineUrls will always return a string * build: update deps, release v1.6.0 * fix(utils): combineUrls will treat null as empty string --- .eslintrc.js | 8 +++- docs/guide/middleware.md | 6 ++- examples/apis-web/fake-post.js | 10 ++++- package.json | 64 ++++++++++++++--------------- rollup.config.js | 21 ++++++---- src/constants.js | 3 +- src/index.d.ts | 16 +++++--- src/index.js | 73 +++++++++++++++++++++++----------- src/middlewareFns.js | 4 +- src/utils/combineUrls.js | 9 +++-- test/__tests__/axios.test.js | 18 ++++++--- test/__tests__/custom.test.js | 51 ++++++++++++++++++++++++ test/__tests__/utils.test.js | 4 ++ test/__tests__/wx.test.js | 1 + 14 files changed, 204 insertions(+), 84 deletions(-) create mode 100644 test/__tests__/custom.test.js diff --git a/.eslintrc.js b/.eslintrc.js index 79589e0..bed9867 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,9 +1,13 @@ module.exports = { extends: 'standard', - parser: 'babel-eslint', + parserOptions: { + ecmaVersion: 10, + parser: 'babel-eslint', + }, rules: { - 'indent': [2, 4], + indent: [2, 4], 'promise/param-names': 0, + 'template-curly-spacing': 'off', 'comma-dangle': [2, 'always-multiline'], }, globals: { diff --git a/docs/guide/middleware.md b/docs/guide/middleware.md index 664656d..8933a8e 100644 --- a/docs/guide/middleware.md +++ b/docs/guide/middleware.md @@ -28,7 +28,8 @@ function (ctx, next) { return next().then(() => { // 注意这里才有响应! ctx.res // 响应对象 - ctx.res.data // 响应的数据 + ctx.res.data // 响应格式化后的数据 + ctx.res.rawData // 响应的原始数据 ctx.reqTime // 请求花费的时间 ctx.endTime // 收到响应的时间 }) @@ -68,7 +69,8 @@ async function (ctx, next) { | req.reqFnParams | 发起请求时的参数对象(上面那些参数都会被放进来作为属性) | | --- | --- | | res | 响应 | -| res.data | 响应的数据 | +| res.data | 响应格式化后的数据 | +| res.rawData | 响应的原始数据 | | res.error | 错误对象(可以取 stack 和 message) | | res.* | [透传 axios 的配置](https://github.com/axios/axios#response-schema) | | --- | --- | diff --git a/examples/apis-web/fake-post.js b/examples/apis-web/fake-post.js index b3dd0e1..54668b9 100644 --- a/examples/apis-web/fake-post.js +++ b/examples/apis-web/fake-post.js @@ -78,7 +78,7 @@ export default { name: 'ct', path: 'custom-transformRequest', axiosOptions: { - transformRequest: () => `ct`, + transformRequest: () => 'ct', }, }, /** @@ -88,5 +88,13 @@ export default { name: 'pj', path: 'post-json', }, + /** + * raw-data + */ + { + name: 'rd', + path: 'raw-data', + afterFn: ([, ctx]) => ctx.res.rawData, + }, ], } diff --git a/package.json b/package.json index 62d9352..2912d84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tua-api", - "version": "1.5.0", + "version": "1.6.0", "description": "🏗 A common tool helps converting configs to api functions", "main": "dist/TuaApi.cjs.js", "module": "dist/TuaApi.esm.js", @@ -50,42 +50,42 @@ "koa-compose": "^4.1.0" }, "devDependencies": { - "@babel/core": "^7.8.4", - "@babel/plugin-external-helpers": "^7.8.3", - "@babel/plugin-proposal-decorators": "^7.8.3", - "@babel/plugin-proposal-object-rest-spread": "^7.8.3", - "@babel/preset-env": "^7.8.4", - "@commitlint/cli": "^8.3.5", - "@commitlint/config-conventional": "^8.3.4", - "@types/jest": "^25.1.2", - "all-contributors-cli": "^6.13.0", - "axios-mock-adapter": "^1.17.0", + "@babel/core": "^7.10.5", + "@babel/plugin-external-helpers": "^7.10.4", + "@babel/plugin-proposal-decorators": "^7.10.5", + "@babel/plugin-proposal-object-rest-spread": "^7.10.4", + "@babel/preset-env": "^7.10.4", + "@commitlint/cli": "^9.1.1", + "@commitlint/config-conventional": "^9.1.1", + "@rollup/plugin-babel": "^5.1.0", + "@rollup/plugin-commonjs": "^14.0.0", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-node-resolve": "^8.4.0", + "@rollup/plugin-replace": "^2.3.3", + "@types/jest": "^26.0.5", + "all-contributors-cli": "^6.16.1", + "axios-mock-adapter": "^1.18.2", "babel-core": "^7.0.0-bridge.0", - "babel-eslint": "^10.0.3", - "babel-jest": "^25.1.0", - "codecov": "^3.6.5", - "cross-env": "^7.0.0", - "eslint": "^6.8.0", - "eslint-config-standard": "^14.1.0", - "eslint-plugin-import": "^2.20.1", - "eslint-plugin-node": "^11.0.0", + "babel-eslint": "^10.1.0", + "babel-jest": "^26.1.0", + "codecov": "^3.7.1", + "cross-env": "^7.0.2", + "eslint": "^7.5.0", + "eslint-config-standard": "^14.1.1", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1", - "gh-pages": "^2.2.0", - "husky": "^4.2.3", - "jest": "^25.1.0", - "lint-staged": "^10.0.7", + "gh-pages": "^3.1.0", + "husky": "^4.2.5", + "jest": "^26.1.0", + "lint-staged": "^10.2.11", "rimraf": "^3.0.2", - "rollup": "^1.31.1", - "rollup-plugin-babel": "^4.3.3", - "rollup-plugin-commonjs": "^10.1.0", + "rollup": "^2.22.1", "rollup-plugin-eslint": "^7.0.0", - "rollup-plugin-json": "^4.0.0", - "rollup-plugin-node-resolve": "^5.2.0", - "rollup-plugin-replace": "^2.2.0", - "rollup-plugin-uglify": "^6.0.4", - "typescript": "^3.7.5", - "vuepress": "^1.3.0" + "rollup-plugin-terser": "^6.1.0", + "typescript": "^3.9.7", + "vuepress": "^1.5.2" }, "repository": { "type": "git", diff --git a/rollup.config.js b/rollup.config.js index 1602043..c92aabd 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,10 +1,10 @@ -import json from 'rollup-plugin-json' -import babel from 'rollup-plugin-babel' -import replace from 'rollup-plugin-replace' -import commonjs from 'rollup-plugin-commonjs' +import json from '@rollup/plugin-json' +import babel from '@rollup/plugin-babel' +import replace from '@rollup/plugin-replace' +import commonjs from '@rollup/plugin-commonjs' import { eslint } from 'rollup-plugin-eslint' -import { uglify } from 'rollup-plugin-uglify' -import nodeResolve from 'rollup-plugin-node-resolve' +import { terser } from 'rollup-plugin-terser' +import nodeResolve from '@rollup/plugin-node-resolve' import pkg from './package.json' @@ -40,7 +40,7 @@ const plugins = [ json(), nodeResolve(), commonjs(), - babel(), + babel({ babelHelpers: 'bundled' }), ] const env = 'process.env.NODE_ENV' const external = ['axios', 'fetch-jsonp'] @@ -68,6 +68,11 @@ export default [{ plugins: [ ...plugins, replace({ [env]: '"production"' }), - uglify(), + terser({ + output: { + /* eslint-disable */ + ascii_only: true, + }, + }), ], }] diff --git a/src/constants.js b/src/constants.js index e690976..5f7605b 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,5 +1,5 @@ // 支持的请求类型 -const VALID_REQ_TYPES = ['wx', 'axios', 'jsonp'] +const VALID_REQ_TYPES = ['wx', 'axios', 'jsonp', 'custom'] // 小程序中合法的请求方法 const WX_VALID_METHODS = ['OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'CONNECT'] @@ -12,6 +12,7 @@ const ERROR_STRINGS = { noData: 'no data!', argsType: 'the first parameter must be an object!', middleware: 'middleware must be a function!', + reqTypeAndCustomFetch: 'reqType or customFetch only!', reqTypeFn: (reqType) => `invalid reqType: "${reqType}", ` + `support these reqTypes: ["${VALID_REQ_TYPES.join('", "')}"].`, diff --git a/src/index.d.ts b/src/index.d.ts index 099af9e..587359b 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -22,6 +22,7 @@ export type ReqType = ( | 'wx' | 'WX' | 'axios' | 'AXIOS' | 'jsonp' | 'JSONP' + | 'custom' | 'CUSTOM' ) export type Method = ( @@ -66,6 +67,7 @@ export interface CtxReq { } export interface CtxRes extends AxiosResponse { data: any + rawData: any error?: Error [k: string]: any } @@ -93,6 +95,7 @@ export interface BaseApiConfig { afterFn?: (args: [U?, Ctx?]) => Promise beforeFn?: () => Promise middleware?: Middleware[] + customFetch?: AnyPromiseFunction commonParams?: object axiosOptions?: AxiosOptions jsonpOptions?: JsonpOptions @@ -121,14 +124,14 @@ export interface RuntimeOptionsOnly { fullPath?: string callbackName?: string } -export interface WxRuntimeOptions extends WxApiConfig, RuntimeOptionsOnly {} -export interface WebRuntimeOptions extends WebApiConfig, RuntimeOptionsOnly {} +export interface WxRuntimeOptions extends WxApiConfig, RuntimeOptionsOnly { } +export interface WebRuntimeOptions extends WebApiConfig, RuntimeOptionsOnly { } export interface Api { key: string mock: Mock params: ParamsConfig - ( + ( params?: U, runtimeOptions?: RuntimeOptions ): Promise @@ -137,13 +140,14 @@ export interface Apis { [k: string]: SyncFnMap } export interface SyncFnMap { [k: string]: Api } export interface TuaApiClass { - new (args?: { + new(args?: { // deprecated host?: string baseUrl?: string reqType?: string middleware?: Middleware[] + customFetch?: AnyPromiseFunction axiosOptions?: AxiosOptions jsonpOptions?: JsonpOptions defaultErrorData?: any @@ -157,8 +161,8 @@ export interface TuaApiInstance { /* -- export utils -- */ -export function getSyncFnMapByApis(apis: Apis): SyncFnMap -export function getPreFetchFnKeysBySyncFnMap(syncFnMap: SyncFnMap): Api[] +export function getSyncFnMapByApis (apis: Apis): SyncFnMap +export function getPreFetchFnKeysBySyncFnMap (syncFnMap: SyncFnMap): Api[] /* -- export default -- */ diff --git a/src/index.js b/src/index.js index f40e772..396c0ed 100644 --- a/src/index.js +++ b/src/index.js @@ -36,6 +36,7 @@ class TuaApi { * @param {string} [options.baseUrl] 服务器基础地址,例如 https://example.com/ * @param {string} [options.reqType] 使用什么工具发(axios/jsonp/wx) * @param {function[]} [options.middleware] 中间件函数数组 + * @param {function} [options.customFetch] 自定义请求函数 * @param {object} [options.axiosOptions] 透传 axios 配置参数 * @param {object} [options.jsonpOptions] 透传 fetch-jsonp 配置参数 * @param {object} [options.defaultErrorData] 出错时的默认数据 @@ -43,15 +44,19 @@ class TuaApi { constructor ({ host, baseUrl = host, - reqType = isWx() ? 'wx' : 'axios', + reqType, middleware = [], + customFetch, axiosOptions = {}, jsonpOptions = {}, defaultErrorData = { code: 999, msg: '出错啦!' }, } = {}) { this.baseUrl = baseUrl - this.reqType = reqType + this.reqType = reqType !== undefined + ? reqType.toLowerCase() + : (isWx() ? 'wx' : 'axios') this.middleware = middleware + this.customFetch = customFetch this.axiosOptions = axiosOptions this.jsonpOptions = jsonpOptions this.defaultErrorData = defaultErrorData @@ -64,6 +69,9 @@ class TuaApi { '[host] 属性将被废弃, 请用 [baseUrl] 替代!', ) } + if (reqType && reqType !== 'custom' && customFetch) { + throw TypeError(ERROR_STRINGS.reqTypeAndCustomFetch) + } return this } @@ -109,28 +117,31 @@ class TuaApi { * @param {string} options.reqType 使用什么工具发(axios/jsonp/wx) * @param {object} options.reqParams 请求参数 * @param {object} options.header 请求的 header + * @param {function} [options.customFetch] 自定义请求函数 * @param {string} options.callback 使用 jsonp 时标识回调函数的名称 * @param {string} options.callbackName 使用 jsonp 时的回调函数名 * @param {object} options.axiosOptions 透传 axios 配置参数 * @param {object} options.jsonpOptions 透传 fetch-jsonp 配置参数 * @return {Promise} */ - _reqFn ({ - url, - mock, - header, - method, - fullUrl, - reqType, - reqParams: data, - callback, - callbackName, - axiosOptions, - jsonpOptions, - ...rest - }) { + _reqFn (options) { + const { + url, + mock, + header, + method: _method, + fullUrl, + reqType: _reqType, + reqParams: data, + callback, + callbackName, + axiosOptions, + jsonpOptions, + ...rest + } = options + // check type - this._checkReqType(reqType) + this._checkReqType(_reqType) // mock data if (mock) { @@ -141,7 +152,12 @@ class TuaApi { return Promise.resolve({ data: resData }) } - method = method.toLowerCase() + const method = _method.toLowerCase() + const reqType = _reqType.toLowerCase() + + if (reqType === 'custom') { + return rest.customFetch({ url, data, method, header, ...rest }) + } if (reqType === 'wx') { return getWxPromise({ url, fullUrl, data, method, header, ...rest }) @@ -161,7 +177,6 @@ class TuaApi { // 防止接口返回非英文时报错 jsonpOptions.charset = jsonpOptions.charset || 'UTF-8' - jsonpOptions.jsonpCallback = callback || jsonpOptions.jsonpCallback jsonpOptions.jsonpCallbackFunction = callbackName || jsonpOptions.jsonpCallbackFunction @@ -226,6 +241,7 @@ class TuaApi { * @param {function} options.afterFn 在请求完成后执行的钩子函数(将被废弃) * @param {function} options.beforeFn 在请求发起前执行的钩子函数(将被废弃) * @param {function[]} options.middleware 中间件函数数组 + * @param {function} [options.customFetch] 自定义请求函数 * @param {Boolean} options.useGlobalMiddleware 是否使用全局中间件 * @param {string} options.baseUrl 服务器地址 * @param {string} options.reqType 使用什么工具发 @@ -261,9 +277,21 @@ class TuaApi { // 向前兼容 type = method - // 合并全局默认值 + /* 合并全局默认值 */ + if (rest.reqType && rest.customFetch) { + if (rest.reqType.toLowerCase() !== 'custom') { + logger.warn(ERROR_STRINGS.reqTypeAndCustomFetch) + } + rest.reqType = 'custom' + } else if (rest.customFetch || this.customFetch) { + // 没有配置 reqType,但配了公共配置或默认配置的 customFetch + rest.reqType = 'custom' + } else { + // 没有配置 customFetch + rest.reqType = rest.reqType || this.reqType + } rest.baseUrl = rest.baseUrl || this.baseUrl - rest.reqType = rest.reqType || this.reqType + rest.customFetch = rest.customFetch || this.customFetch rest.axiosOptions = rest.axiosOptions ? { ...this.axiosOptions, ...rest.axiosOptions } : this.axiosOptions @@ -294,9 +322,8 @@ class TuaApi { // 向前兼容 runtimeParams.host = runtimeParams.host || runtimeParams.baseUrl - runtimeParams.baseUrl = runtimeParams.baseUrl || runtimeParams.host - runtimeParams.method = runtimeParams.method || runtimeParams.type + runtimeParams.baseUrl = runtimeParams.baseUrl || runtimeParams.host // 请求的上下文信息 const ctx = { diff --git a/src/middlewareFns.js b/src/middlewareFns.js index 70da86b..31f7095 100644 --- a/src/middlewareFns.js +++ b/src/middlewareFns.js @@ -38,6 +38,7 @@ const recordReqTimeMiddleware = (ctx, next) => { */ const formatResDataMiddleware = (ctx, next) => next().then(() => { const jsonData = ctx.res.data + ctx.res.rawData = ctx.res.data if (!jsonData) return Promise.reject(Error(ERROR_STRINGS.noData)) @@ -65,7 +66,7 @@ const formatReqParamsMiddleware = (ctx, next) => { throw TypeError(ERROR_STRINGS.argsType) } - if (isFormData(args)) { + if (isFormData(args) || Array.isArray(args)) { ctx.req.reqParams = args return next() @@ -97,6 +98,7 @@ const setReqFnParamsMiddleware = (ctx, next) => { ctx.req.reqFnParams = { url, + baseUrl, fullUrl, reqParams, ...rest, diff --git a/src/utils/combineUrls.js b/src/utils/combineUrls.js index 46ca644..9411bb1 100644 --- a/src/utils/combineUrls.js +++ b/src/utils/combineUrls.js @@ -5,12 +5,15 @@ * @returns {string} The combined URL */ function combineUrls (baseUrl = '', relativeUrl = '') { - if (!relativeUrl) return baseUrl + const strBaseUrl = baseUrl === null ? '' : String(baseUrl) + const strRelativeUrl = relativeUrl === null ? '' : String(relativeUrl) + + if (!strRelativeUrl) return strBaseUrl return ( - baseUrl.replace(/\/+$/, '') + + strBaseUrl.replace(/\/+$/, '') + '/' + - relativeUrl.replace(/^\/+/, '') + strRelativeUrl.replace(/^\/+/, '') ) } diff --git a/test/__tests__/axios.test.js b/test/__tests__/axios.test.js index e8fc981..0b4e909 100644 --- a/test/__tests__/axios.test.js +++ b/test/__tests__/axios.test.js @@ -7,10 +7,7 @@ import { fakeGetApi, fakePostApi } from '@examples/apis-web/' const mock = new MockAdapter(axios) -const params = { - param1: 'steve', - param2: 'young', -} +const params = { param1: 'steve', param2: 'young' } const reqAPUrl = 'http://example-base.com/fake-post/array-params' const reqOPUrl = 'http://example-base.com/fake-post/object-params' @@ -22,6 +19,7 @@ const reqMFDUrl = 'http://example-base.com/fake-get/mock-function-data' const reqBFCUrl = 'http://example-base.com/fake-get/beforeFn-cookie' const reqCTUrl = 'http://example-base.com/fake-post/custom-transformRequest' const reqPjUrl = 'http://example-base.com/fake-post/post-json' +const reqRdUrl = 'http://example-base.com/fake-post/raw-data' describe('middleware', () => { test('change baseUrl before request', async () => { @@ -128,10 +126,12 @@ describe('fake post requests', () => { test('empty-array-params', async () => { const data = { code: 0, data: 'object data' } + const arrayArgs = [1, 2] mock.onPost(reqEAPUrl).reply(200, data) - const resData = await fakePostApi.eap() + const resData = await fakePostApi.eap(arrayArgs) expect(resData).toEqual(data) + expect(mock.history.post[0].data).toEqual(JSON.stringify(arrayArgs)) }) test('array-params', async () => { @@ -190,4 +190,12 @@ describe('fake post requests', () => { expect(data).toBe(JSON.stringify(fakePostConfig.commonParams)) expect(mock.history.post[0].headers['Content-Type']).toBe('application/json;charset=utf-8') }) + + test('raw-data', async () => { + const data = [0, 'array data'] + mock.onPost(reqRdUrl).reply(200, data) + const resData = await fakePostApi.rd() + + expect(resData).toEqual(data) + }) }) diff --git a/test/__tests__/custom.test.js b/test/__tests__/custom.test.js new file mode 100644 index 0000000..2426859 --- /dev/null +++ b/test/__tests__/custom.test.js @@ -0,0 +1,51 @@ +import TuaApi from '@/index' +import { ERROR_STRINGS } from '@/constants' + +const customFetch = jest.fn(() => Promise.resolve({ data: Math.random() })) + +describe('customFetch', () => { + const tuaApi = new TuaApi() + const fooApi = tuaApi.getApi({ + prefix: 'foo', + pathList: [ + { path: 'bar', customFetch }, + { path: 'axios', customFetch, reqType: 'axios' }, + { path: 'customAxios', customFetch, reqType: 'custom' }, + ], + }) + + test('both customFetch and reqType', () => { + expect(() => new TuaApi({ reqType: 'axios', customFetch })).toThrow(TypeError(ERROR_STRINGS.reqTypeAndCustomFetch)) + }) + + test('global customFetch should be called', async () => { + const tuaApi = new TuaApi({ customFetch }) + const fooApi = tuaApi.getApi({ + prefix: 'foo', + pathList: [ + { path: 'globalCustomFetch' }, + ], + }) + await fooApi.globalCustomFetch() + + expect(customFetch).toBeCalled() + }) + + test('local customFetch should be called', async () => { + await fooApi.bar() + + expect(customFetch).toBeCalled() + }) + + test('local customFetch should be called with reqType', async () => { + await fooApi.axios() + + expect(customFetch).toBeCalled() + }) + + test('local customFetch should be called with `custom` reqType', async () => { + await fooApi.customAxios() + + expect(customFetch).toBeCalled() + }) +}) diff --git a/test/__tests__/utils.test.js b/test/__tests__/utils.test.js index a885bd4..2abc9d9 100644 --- a/test/__tests__/utils.test.js +++ b/test/__tests__/utils.test.js @@ -9,6 +9,10 @@ import { } from '@/utils' test('combineUrls', () => { + expect(combineUrls(0, 0)).toBe('0/0') + expect(combineUrls(1, 1)).toBe('1/1') + expect(combineUrls(1, null)).toBe('1') + expect(combineUrls(null, 1)).toBe('/1') expect(combineUrls(undefined, undefined)).toBe('') expect(combineUrls(undefined, 'users')).toBe('/users') expect(combineUrls('https://api.github.com', undefined)).toBe('https://api.github.com') diff --git a/test/__tests__/wx.test.js b/test/__tests__/wx.test.js index 7cdc944..5972e07 100644 --- a/test/__tests__/wx.test.js +++ b/test/__tests__/wx.test.js @@ -76,6 +76,7 @@ describe('middleware', () => { expect(ctx.endTime).toBeDefined() expect(ctx.res.data).toBeDefined() + expect(ctx.res.rawData).toBeDefined() }) tuaApi.use(globalMiddlewareFn)