feat: api service support
This commit is contained in:
parent
765bf9daa9
commit
d0b6efaaf5
@ -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<string, Result<any, any>>;
|
||||
|
||||
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<any, any> {
|
||||
return this.services[uid];
|
||||
}
|
||||
|
||||
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D> | ResourceActionOptions): Promise<R> {
|
||||
const { resource, resourceOf, action, params } = config as any;
|
||||
console.log('config', config);
|
||||
@ -63,4 +72,4 @@ export class APIClient {
|
||||
};
|
||||
return new Proxy(target, handler);
|
||||
}
|
||||
}
|
||||
}
|
60
packages/client/src/api-client/demos/demo3.tsx
Normal file
60
packages/client/src/api-client/demos/demo3.tsx
Normal file
@ -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 (
|
||||
<Table
|
||||
pagination={false}
|
||||
rowKey={'id'}
|
||||
loading={loading}
|
||||
dataSource={data?.data}
|
||||
columns={[{ title: 'Name', dataIndex: 'name' }]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ComponentB = () => {
|
||||
console.log('ComponentB');
|
||||
const apiClient = useAPIClient();
|
||||
return <Button onClick={() => apiClient.service('test')?.refresh()}>刷新</Button>;
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<APIClientProvider apiClient={apiClient}>
|
||||
<ComponentB />
|
||||
<br/><br/>
|
||||
<ComponentA />
|
||||
</APIClientProvider>
|
||||
);
|
||||
};
|
118
packages/client/src/api-client/hooks/assign.ts
Normal file
118
packages/client/src/api-client/hooks/assign.ts
Normal file
@ -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<MergeStrategyType, MergeStrategyFunc>();
|
||||
|
||||
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;
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export * from './useAPIClient';
|
||||
export * from './useRequest';
|
||||
export * from './useResource';
|
||||
|
||||
|
@ -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<any>;
|
||||
|
||||
@ -15,20 +18,43 @@ type ResourceActionOptions<P = any> = {
|
||||
|
||||
export function useRequest<P>(
|
||||
service: AxiosRequestConfig<P> | ResourceActionOptions<P> | FunctionService,
|
||||
options?: Options<any, any>,
|
||||
options?: Options<any, any> & { 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;
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ group:
|
||||
class APIClient {
|
||||
// axios 实例
|
||||
axios: AxiosInstance;
|
||||
// 缓存带 uid 的 useRequest({}, {uid}) 的结果,可供其他组件调用
|
||||
services: Record<string, Result<any, any>>;
|
||||
// 构造器
|
||||
constructor(instance?: AxiosInstance | AxiosRequestConfig);
|
||||
// 客户端请求,支持 AxiosRequestConfig 和 ResourceActionOptions
|
||||
@ -42,6 +44,10 @@ const instance = axios.create({
|
||||
const apiClient = new APIClient(instance);
|
||||
```
|
||||
|
||||
`api.service(uid)` 的例子,ComponentB 里刷新 ComponentA 的请求数据
|
||||
|
||||
<code src="./demos/demo3.tsx" />
|
||||
|
||||
## APIClientProvider
|
||||
|
||||
提供 APIClient 实例的上下文。
|
||||
|
Loading…
Reference in New Issue
Block a user