diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index dc1fdd1..1549492 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -43,6 +43,7 @@ module.exports = { 'installation', '', 'middleware', + 'mock', 'export-utils', '../config/', ], diff --git a/docs/README.md b/docs/README.md index 5f90815..ac42578 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,9 +4,13 @@ actionText: 快速上手 → actionLink: /quick-start/ features: - title: 支持多端 - details: 支持 web 端和小程序端 + details: 支持 web 端、Node 端和小程序端 - title: 支持跨域 details: 默认使用 axios,也支持降级为 jsonp +- title: 可配置 + details: 可配置接口类型、请求方式、默认参数、必填参数等属性 +- title: 支持 mock + details: 接口数据支持 mock,方便开发调试 - title: 中间件 details: koa 风格中间件,方便添加各种特技 footer: MIT Licensed | Copyright © 2018-present StEve Young diff --git a/docs/config/detail.md b/docs/config/detail.md index 81a147a..300f50e 100644 --- a/docs/config/detail.md +++ b/docs/config/detail.md @@ -18,6 +18,27 @@ export default { } ``` +## mock 模拟接口数据 +* 类型:`Object`、`Function` +* 默认值:`{}` + +模拟接口数据,可以直接填数据,或是填函数。函数将收到 `params` 参数对象,即最终发送给接口的数据对象。 + +```js +export default { + // 对象形式 + mock: { code: 0, data: 'some data' }, + + // 函数形式 + mock: (params) => ({ + code: params.mockCode, + data: params.mockData, + }), +} +``` + +详情参阅 [快速上手 - mock 章节](../quick-start/mock.md) + ## prefix 接口中间地址 建议与文件同名,方便维护。 diff --git a/docs/config/own.md b/docs/config/own.md index 3b2cbb3..74adc90 100644 --- a/docs/config/own.md +++ b/docs/config/own.md @@ -32,7 +32,7 @@ export default { } ``` -有时接口地址较长,可以添加 `name` 配置重命名接口,这样就可以这样调用 +有时接口地址较长或不方便直接调用,可以添加 `name` 配置重命名接口,这样就可以这样调用 ```js exampleApi.fooBar({ ... }) @@ -67,4 +67,6 @@ export default { 有时某个接口正好不需要上一级中 `commonParams` 的参数。那么可以传递 `null` 覆盖上一级中的 `commonParams`。 ## 其他参数 -其他参数参阅上一节 [详细配置](./detail.md) +上一节中的所有参数(除了 `pathList` 外)均有效,且优先级最高。 + +详情参阅上一节 [详细配置](./detail.md) diff --git a/docs/quick-start/mock.md b/docs/quick-start/mock.md new file mode 100644 index 0000000..d637a91 --- /dev/null +++ b/docs/quick-start/mock.md @@ -0,0 +1,178 @@ +# 数据 mock +## 静态配置 +即将 mock 数据直接填在该接口的配置中。 + +### 简单对象 +简单粗暴,填数据就完事儿了~ + +```js +{ + pathList: [ + // 以 foo 接口为例 + { + path: 'foo', + + // 对象形式 + mock: { code: 0, data: 'some data' }, + }, + ], +} +``` + +### mock 函数 +使用函数形式,用法上会更灵活一些。 + +```js +{ + pathList: [ + // 以 foo 接口为例 + { + path: 'foo', + + // 函数形式 + mock: (params) => ({ + code: params.mockCode, + data: params.mockData, + }), + }, + ], +} +``` + +::: tip +`params` 即最终传入接口的参数对象。 +::: + +```js +import { exampleApi } from '@/apis/' + +// 填写 mock 数据 +const mockCode = 0 +const mockData = { foo: 'bar' } + +// 请求将收到 mock 数据 +exampleApi.foo({ mockCode, mockData }) + .then(({ code, data }) => { + console.log(code, data) // 0 {foo: "bar"} + }) +``` + +### 多接口公共 mock +mock 属性不仅可以填在各个接口处,也可以将其放在上一级,mock 当前配置中的所有接口。 + +```js +{ + // 公共 mock + mock: ({ __mockData__ }) => __mockData__, + + pathList: [ + // 自身的 mock 配置优先级更高 + { path: 'foo', mock: { code: 0 } }, + + // 没填自身 mock,则默认使用公共 mock + { path: 'bar' }, + + // 禁用 mock + { path: 'null', mock: null }, + ], +} +``` + +```js +import { exampleApi } from '@/apis/' + +const __mockData__ = { code: 123 } + +// 使用自己定义 mock 数据 +exampleApi.foo({ __mockData__ }) + .then(({ code }) => { + console.log(code) // 0 + }) + +// 使用公共的 mock 数据 +exampleApi.bar({ __mockData__ }) + .then(({ code }) => { + console.log(code) // 123 + }) +``` + +更多配置优先级内容请参阅[配置说明](../config/)部分。 + +## 动态配置 +即为每个导出的 `api` 函数添加 `mock` 属性,在业务侧用以下方式调用。 + +```js +import { exampleApi } from '@/apis/' + +// 填写 mock 数据 +exampleApi.foo.mock = { + code: 0, + data: { foo: 'bar' }, +} + +// 同样支持 mock 函数 +exampleApi.foo.mock = () => ({ + code: 0, + data: { foo: 'bar' }, +}) + +// 请求将收到 mock 数据 +exampleApi.foo().then(({ code, data }) => { + console.log(code, data) // 0 {foo: "bar"} +}) +``` + +## 同时配置 +### 优先级 +若是同时配置了静态和动态 mock,动态配置的 mock 数据优先级更高。 + +::: tip +优先级:动态 > 静态 +::: + +* 接口配置 +```js +{ + pathList: [ + { + path: 'foo', + mock: (params) => ({ code: params.mockCode }), + }, + ], +} +``` + +* 业务侧 +```js +import { exampleApi } from '@/apis/' + +// 动态配置的数据将覆盖静态配置的数据 +exampleApi.foo.mock = { code: 1 } + +exampleApi.foo({ mockCode: 0 }) + .then(({ code }) => { + console.log(code) // 1 + }) +``` + +### 关闭 mock +可以通过以下代码实现关闭 mock 功能。 + +```js +import { exampleApi } from '@/apis/' + +// 关闭 mock +exampleApi.foo.mock = null + +// 即使传递 mock 数据也不起作用 +exampleApi.foo({ mockCode: 404 }) + .then(({ code }) => { + console.log(code) // 实际接口的返回值 + }) +``` + +::: tip +其实动态配置 `exampleApi.foo.mock` 的默认值就是静态配置的值,而在 `tua-api` 底层读取的就是 `exampleApi.foo.mock`。 + +所以自然动态配置的优先级更高,并且赋值为 `null` 即可关闭 mock。 +::: diff --git a/examples/apis-mp/index.js b/examples/apis-mp/index.js index b63ab55..49d462c 100644 --- a/examples/apis-mp/index.js +++ b/examples/apis-mp/index.js @@ -13,4 +13,5 @@ tuaApi.use(async (ctx, next) => { // console.log('after: ', ctx) }) +export const mockApi = tuaApi.getApi(require('./mock').default) export const fakeWxApi = tuaApi.getApi(require('./fake-wx').default) diff --git a/examples/apis-mp/mock.js b/examples/apis-mp/mock.js new file mode 100644 index 0000000..8067acd --- /dev/null +++ b/examples/apis-mp/mock.js @@ -0,0 +1,24 @@ +export default { + // 该参数表示请求的公用服务器地址。 + host: 'http://example-base.com/', + + // 该参数表示请求的中间路径,建议与文件同名,以便后期维护。 + prefix: 'mock', + + // 所有请求类型(可选值 OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT) + type: 'get', + + // 公共 mock + mock: ({ __mockData__ }) => __mockData__, + + pathList: [ + // 自身的 mock 配置优先级更高 + { path: 'foo', mock: { code: 500 } }, + + // 没填自身 mock,则默认使用公共 mock + { path: 'bar' }, + + // 禁用 mock + { path: 'null', mock: null }, + ], +} diff --git a/examples/apis-web/fake-get.js b/examples/apis-web/fake-get.js index 55c19c5..910a4da 100644 --- a/examples/apis-web/fake-get.js +++ b/examples/apis-web/fake-get.js @@ -112,5 +112,22 @@ export default { path: 'no-afterFn-data', afterFn: () => {}, }, + /** + * mock-object-data + */ + { + name: 'mockObjectData', + path: 'mock-object-data', + mock: { code: 404, data: {} }, + }, + /** + * mock-function-data + */ + { + name: 'mockFnData', + path: 'mock-function-data', + reqType: 'axios', + mock: ({ mockCode }) => ({ code: mockCode, data: {} }), + }, ], } diff --git a/package.json b/package.json index e77a6bb..edbb093 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tua-api", - "version": "0.3.3", + "version": "0.4.0", "main": "dist/TuaApi.umd.js", "module": "dist/TuaApi.es.js", "jsnext:main": "dist/TuaApi.es.js", diff --git a/src/TuaApi.js b/src/TuaApi.js index b361cba..469b907 100644 --- a/src/TuaApi.js +++ b/src/TuaApi.js @@ -30,7 +30,7 @@ class TuaApi { /** * @param {Object} options * @param {String} options.host 服务器基础地址,例如 https://example.com/ - * @param {String} options.reqType 请求类型 + * @param {String} options.reqType 使用什么工具发(axios/jsonp/wx) * @param {Function[]} options.middleware 中间件函数数组 * @param {Object} options.axiosOptions 透传 axios 配置参数 * @param {Object} options.jsonpOptions 透传 fetch-jsonp 配置参数 @@ -107,11 +107,21 @@ class TuaApi { axiosOptions, ...rest }) { + // check type if (VALID_REQ_TYPES.indexOf(reqType) === -1) { logger.error(`reqType 的有效值为: ${VALID_REQ_TYPES.join(', ')}!`) throw Error('invalid reqType') } + // mock data + if (rest.mock) { + const resData = typeof rest.mock === 'function' + ? rest.mock(data) + : { ...rest.mock } + + return Promise.resolve({ data: resData }) + } + const method = type.toUpperCase() if (reqType === 'wx') { @@ -189,6 +199,7 @@ class TuaApi { * 接受 api 对象,返回待接收参数的单个 api 函数的对象 * @param {Object} options * @param {String} options.type 接口请求类型 get/post... + * @param {Object|Function} options.mock 模拟的响应数据或是生成数据的函数 * @param {String} options.name 自定义的接口名称 * @param {String} options.path 接口路径名称 * @param {String[]} options.params 接口参数数组 @@ -197,10 +208,15 @@ class TuaApi { * @param {Function} options.beforeFn 在请求发起前执行的钩子函数(将被废弃) * @param {Function[]} options.middleware 中间件函数数组 * @param {Boolean} options.useGlobalMiddleware 是否使用全局中间件 + * @param {String} options.host 服务器地址 + * @param {String} options.reqType 使用什么工具发 + * @param {Object} options.axiosOptions 透传 axios 配置参数 + * @param {Object} options.jsonpOptions 透传 fetch-jsonp 配置参数 * @return {Object} 以 apiName 为 key,请求函数为值的对象 */ _getOneReqMap ({ type = 'get', + mock, name, path, params = {}, @@ -241,14 +257,14 @@ class TuaApi { // 请求的上下文信息 const ctx = { - req: { args, type, path, params, prefix, apiName, fullPath, callbackName, reqFnParams: {}, ...rest }, + req: { args, type, path, params, prefix, apiName, fullPath, callbackName, reqFnParams: {}, mock: apiFn.mock, ...rest }, } // 中间件函数 const middlewareFn = this._getMiddlewareFn(middleware, useGlobalMiddleware) // 执行完 beforeFn 后执行的函数 - const beforeFnCallback = (rArgs = {}) => { + const beforeFnCb = (rArgs = {}) => { // 兼容小程序传递请求头(建议还是放在中间件中) if (rArgs.header) { ctx.req.reqFnParams.header = rArgs.header @@ -268,7 +284,7 @@ class TuaApi { } return beforeFn() - .then(beforeFnCallback) + .then(beforeFnCb) // 执行请求中间件函数 .then(() => middlewareFn(ctx)) // 请求执行完成后的钩子 @@ -280,8 +296,8 @@ class TuaApi { ) } - // 将请求的 key 和 params 挂上去,以便在 ssr 时预取数据 apiFn.key = fullPath + apiFn.mock = mock apiFn.params = params return { [apiName]: apiFn } diff --git a/test/__tests__/axios.test.js b/test/__tests__/axios.test.js index 59a05ea..c876a24 100644 --- a/test/__tests__/axios.test.js +++ b/test/__tests__/axios.test.js @@ -15,6 +15,16 @@ const reqOPUrl = `http://example-base.com/fake-post/object-params` const reqOHUrl = `http://example-test.com/fake-post/own-host` const reqTAUrl = `http://example-base.com/fake-get/req-type-axios?asyncCp=asyncCp` const reqEAPUrl = `http://example-base.com/fake-post/empty-array-params` +const reqMFDUrl = `http://example-base.com/fake-get/mock-function-data` + +describe('mock data', () => { + test('mock function data', async () => { + mock.onGet(reqMFDUrl).reply(200, {}) + const resData = await fakeGetApi.mockFnData({ mockCode: 404 }) + + expect(resData.code).toBe(404) + }) +}) describe('error handling', () => { test('non-object params', () => { diff --git a/test/__tests__/jsonp.test.js b/test/__tests__/jsonp.test.js index b9a56ca..1ada724 100644 --- a/test/__tests__/jsonp.test.js +++ b/test/__tests__/jsonp.test.js @@ -7,6 +7,15 @@ jest.mock('fetch-jsonp') const data = [ 0, 'array data' ] const returnVal = { code: 0, data: 'array data' } +describe('mock data', () => { + test('mock object data', async () => { + fetchJsonp.mockResolvedValue({ json: () => data }) + const resData = await fakeGetApi.mockObjectData() + + expect(resData.code).toBe(404) + }) +}) + describe('fake jsonp requests', () => { test('async-common-params', async () => { fetchJsonp.mockResolvedValue({ json: () => data }) diff --git a/test/__tests__/wx.test.js b/test/__tests__/wx.test.js index fb4e4fd..3f4c473 100644 --- a/test/__tests__/wx.test.js +++ b/test/__tests__/wx.test.js @@ -1,11 +1,54 @@ import '../__mocks__/wxMock' import TuaApi from '../../src/TuaApi' import fakeWx from '../../examples/apis-mp/fake-wx' -import { fakeWxApi } from '../../examples/apis-mp/' +import { mockApi, fakeWxApi } from '../../examples/apis-mp/' const testObjData = { code: 0, data: 'object data' } const testArrData = [ 0, 'array data' ] +describe('mock data', () => { + beforeEach(() => { + wx.__TEST_DATA__ = {} + }) + + test('common mock data', async () => { + wx.__TEST_DATA__ = { testData: testObjData } + const resData = await mockApi.bar({ __mockData__: { code: 404, data: {} } }) + + expect(resData.code).toBe(404) + }) + + test('self mock data', async () => { + wx.__TEST_DATA__ = { testData: testObjData } + const resData = await mockApi.foo({ __mockData__: { code: 404, data: {} } }) + + expect(resData.code).toBe(500) + }) + + test('null mock data', async () => { + wx.__TEST_DATA__ = { testData: testObjData } + const resData = await mockApi.null({ __mockData__: { code: 404, data: {} } }) + + expect(resData).toEqual({ code: 0, data: 'object data' }) + }) + + test('dynamic object mock data', async () => { + wx.__TEST_DATA__ = { testData: testObjData } + mockApi.null.mock = { code: 123 } + const resData = await mockApi.null() + + expect(resData.code).toBe(123) + }) + + test('dynamic function mock data', async () => { + wx.__TEST_DATA__ = { testData: testObjData } + mockApi.foo.mock = ({ mockCode }) => ({ code: mockCode }) + const resData = await mockApi.foo({ mockCode: 123 }) + + expect(resData.code).toBe(123) + }) +}) + describe('middleware', () => { const tuaApi = new TuaApi() const fakeWxApi = tuaApi.getApi(fakeWx)