From d0b6efaaf5d9a77d60785fa23aa733c66ac2958a Mon Sep 17 00:00:00 2001 From: chenos Date: Wed, 26 Jan 2022 18:20:31 +0800 Subject: [PATCH] feat: api service support --- packages/client/src/api-client/APIClient.ts | 13 +- .../client/src/api-client/demos/demo3.tsx | 60 +++++++++ .../client/src/api-client/hooks/assign.ts | 118 ++++++++++++++++++ packages/client/src/api-client/hooks/index.ts | 1 + .../client/src/api-client/hooks/useRequest.ts | 56 ++++++--- packages/client/src/api-client/index.md | 6 + 6 files changed, 237 insertions(+), 17 deletions(-) create mode 100644 packages/client/src/api-client/demos/demo3.tsx create mode 100644 packages/client/src/api-client/hooks/assign.ts diff --git a/packages/client/src/api-client/APIClient.ts b/packages/client/src/api-client/APIClient.ts index 36df87650..4a6145472 100644 --- a/packages/client/src/api-client/APIClient.ts +++ b/packages/client/src/api-client/APIClient.ts @@ -1,4 +1,6 @@ -import axios, { Axios, AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios'; +import { observable } from '@formily/reactive'; +import { Result } from 'ahooks/lib/useRequest/src/types'; +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; export interface ActionParams { [key: string]: any; @@ -22,7 +24,10 @@ export interface IResource { export class APIClient { axios: AxiosInstance; + services: Record>; + constructor(instance?: AxiosInstance | AxiosRequestConfig) { + this.services = observable({}); if (typeof instance === 'function') { this.axios = instance; } else { @@ -30,6 +35,10 @@ export class APIClient { } } + service(uid: string): Result { + return this.services[uid]; + } + request, D = any>(config: AxiosRequestConfig | ResourceActionOptions): Promise { const { resource, resourceOf, action, params } = config as any; console.log('config', config); @@ -63,4 +72,4 @@ export class APIClient { }; return new Proxy(target, handler); } -} +} \ No newline at end of file diff --git a/packages/client/src/api-client/demos/demo3.tsx b/packages/client/src/api-client/demos/demo3.tsx new file mode 100644 index 000000000..bf47a55f8 --- /dev/null +++ b/packages/client/src/api-client/demos/demo3.tsx @@ -0,0 +1,60 @@ +import { APIClient, APIClientProvider, useAPIClient, useRequest } from '@nocobase/client'; +import { Button, Table } from 'antd'; +import MockAdapter from 'axios-mock-adapter'; +import React from 'react'; + +const apiClient = new APIClient(); + +const mock = new MockAdapter(apiClient.axios); +const sleep = (value: number) => new Promise((resolve) => setTimeout(resolve, value)); + +mock.onGet('/users:list').reply(async () => { + await sleep(1000); + return [ + 200, + { + data: [ + { id: 1, name: 'John Smith' }, + { id: 2, name: 'James' }, + ], + }, + ]; +}); + +const ComponentA = () => { + console.log('ComponentA'); + const { data, loading } = useRequest( + { + url: 'users:list', + method: 'get', + }, + { + uid: 'test', // 当指定了 uid 的 useRequest 的结果,可以通过 api.service(uid) 获取 + }, + ); + return ( + + ); +}; + +const ComponentB = () => { + console.log('ComponentB'); + const apiClient = useAPIClient(); + return ; +}; + +export default () => { + return ( + + +

+ +
+ ); +}; diff --git a/packages/client/src/api-client/hooks/assign.ts b/packages/client/src/api-client/hooks/assign.ts new file mode 100644 index 000000000..77a7fb298 --- /dev/null +++ b/packages/client/src/api-client/hooks/assign.ts @@ -0,0 +1,118 @@ +import deepmerge from 'deepmerge'; +import uniq from 'lodash/uniq'; + +type MergeStrategyType = 'merge' | 'deepMerge' | 'overwrite' | 'andMerge' | 'orMerge' | 'intersect' | 'union'; +type MergeStrategyFunc = (x: any, y: any) => any; + +export type MergeStrategy = MergeStrategyType | MergeStrategyFunc; + +export interface MergeStrategies { + [key: string]: MergeStrategy; +} + +export default function isPlainObject(value) { + if (Object.prototype.toString.call(value) !== '[object Object]') { + return false; + } + + const prototype = Object.getPrototypeOf(value); + return prototype === null || prototype === Object.prototype; +} + +function getEnumerableOwnPropertySymbols(target: any): any[] { + return Object.getOwnPropertySymbols + ? Object.getOwnPropertySymbols(target).filter((symbol) => target.propertyIsEnumerable(symbol)) + : []; +} + +function getKeys(target: any) { + return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target)); +} + +export const mergeStrategies = new Map(); + +mergeStrategies.set('overwrite', (_, y) => { + return y; +}); + +mergeStrategies.set('andMerge', (x, y) => { + if (!x && !y) { + return; + } + if (!x) { + return y; + } + if (!y) { + return x; + } + return { + $and: [x, y], + }; +}); + +mergeStrategies.set('orMerge', (x, y) => { + if (!x && !y) { + return; + } + if (!x) { + return y; + } + if (!y) { + return x; + } + return { + $or: [x, y], + }; +}); + +mergeStrategies.set('deepMerge', (x, y) => { + return isPlainObject(x) && isPlainObject(y) + ? deepmerge(x, y, { + arrayMerge: (x, y) => y, + }) + : y; +}); + +mergeStrategies.set('merge', (x, y) => { + return isPlainObject(x) && isPlainObject(y) ? Object.assign(x, y) : y; +}); + +mergeStrategies.set('union', (x, y) => { + if (typeof x === 'string') { + x = x.split(','); + } + if (typeof y === 'string') { + y = y.split(','); + } + return uniq((x || []).concat(y || [])); +}); + +mergeStrategies.set('intersect', (x, y) => { + if (typeof x === 'string') { + x = x.split(','); + } + if (typeof y === 'string') { + y = y.split(','); + } + if (!Array.isArray(x) || x.length === 0) { + return y || []; + } + if (!Array.isArray(y) || y.length === 0) { + return x || []; + } + return x.filter((v) => y.includes(v)); +}); + +export function assign(target: any, source: any, strategies: MergeStrategies = {}) { + getKeys(source).forEach((sourceKey) => { + const strategy = strategies[sourceKey]; + let func = mergeStrategies.get('deepMerge'); + if (typeof strategy === 'function') { + func = strategy; + } else if (typeof strategy === 'string' && mergeStrategies.has(strategy as any)) { + func = mergeStrategies.get(strategy as any); + } + target[sourceKey] = func(target[sourceKey], source[sourceKey]); + }); + return target; +} diff --git a/packages/client/src/api-client/hooks/index.ts b/packages/client/src/api-client/hooks/index.ts index 078c89886..8c26c77dd 100644 --- a/packages/client/src/api-client/hooks/index.ts +++ b/packages/client/src/api-client/hooks/index.ts @@ -1,3 +1,4 @@ export * from './useAPIClient'; export * from './useRequest'; export * from './useResource'; + diff --git a/packages/client/src/api-client/hooks/useRequest.ts b/packages/client/src/api-client/hooks/useRequest.ts index 69ebd87d9..165eb01c2 100644 --- a/packages/client/src/api-client/hooks/useRequest.ts +++ b/packages/client/src/api-client/hooks/useRequest.ts @@ -1,8 +1,11 @@ -import { useContext } from 'react'; -import { AxiosRequestConfig } from 'axios'; -import { Options } from 'ahooks/lib/useRequest/src/types'; +import { merge } from '@formily/shared'; import { default as useReq } from 'ahooks/lib/useRequest'; +import { Options } from 'ahooks/lib/useRequest/src/types'; +import { AxiosRequestConfig } from 'axios'; +import cloneDeep from 'lodash/cloneDeep'; +import { useContext } from 'react'; import { APIClientContext } from '../context'; +import { assign } from './assign'; type FunctionService = (...args: any[]) => Promise; @@ -15,20 +18,43 @@ type ResourceActionOptions

= { export function useRequest

( service: AxiosRequestConfig

| ResourceActionOptions

| FunctionService, - options?: Options, + options?: Options & { uid?: string }, ) { const api = useContext(APIClientContext); if (typeof service === 'function') { - return useReq(service, options); + const result = useReq(service, { + ...options, + onSuccess(...args) { + options.onSuccess && options.onSuccess(...args); + if (options.uid) { + api.services[options.uid] = result; + } + }, + }); + return result; } - return useReq(async (params) => { - const { resource } = service as ResourceActionOptions; - if (resource) { - Object.assign(service, { params }); - } else { - Object.assign(service, params); - } - const response = await api.request(service); - return response?.data; - }, options); + const result = useReq( + async (params = {}) => { + const { resource } = service as ResourceActionOptions; + let args = cloneDeep(service); + if (resource) { + args.params = args.params || {}; + assign(args.params, params); + } else { + args = merge(args, params); + } + const response = await api.request(args); + return response?.data; + }, + { + ...options, + onSuccess(...args) { + options.onSuccess && options.onSuccess(...args); + if (options.uid) { + api.services[options.uid] = result; + } + }, + }, + ); + return result; } diff --git a/packages/client/src/api-client/index.md b/packages/client/src/api-client/index.md index e8c8d9808..a7dbbd45e 100644 --- a/packages/client/src/api-client/index.md +++ b/packages/client/src/api-client/index.md @@ -13,6 +13,8 @@ group: class APIClient { // axios 实例 axios: AxiosInstance; + // 缓存带 uid 的 useRequest({}, {uid}) 的结果,可供其他组件调用 + services: Record>; // 构造器 constructor(instance?: AxiosInstance | AxiosRequestConfig); // 客户端请求,支持 AxiosRequestConfig 和 ResourceActionOptions @@ -42,6 +44,10 @@ const instance = axios.create({ const apiClient = new APIClient(instance); ``` +`api.service(uid)` 的例子,ComponentB 里刷新 ComponentA 的请求数据 + + + ## APIClientProvider 提供 APIClient 实例的上下文。