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 {
|
export interface ActionParams {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
@ -22,7 +24,10 @@ export interface IResource {
|
|||||||
export class APIClient {
|
export class APIClient {
|
||||||
axios: AxiosInstance;
|
axios: AxiosInstance;
|
||||||
|
|
||||||
|
services: Record<string, Result<any, any>>;
|
||||||
|
|
||||||
constructor(instance?: AxiosInstance | AxiosRequestConfig) {
|
constructor(instance?: AxiosInstance | AxiosRequestConfig) {
|
||||||
|
this.services = observable({});
|
||||||
if (typeof instance === 'function') {
|
if (typeof instance === 'function') {
|
||||||
this.axios = instance;
|
this.axios = instance;
|
||||||
} else {
|
} 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> {
|
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D> | ResourceActionOptions): Promise<R> {
|
||||||
const { resource, resourceOf, action, params } = config as any;
|
const { resource, resourceOf, action, params } = config as any;
|
||||||
console.log('config', config);
|
console.log('config', config);
|
||||||
@ -63,4 +72,4 @@ export class APIClient {
|
|||||||
};
|
};
|
||||||
return new Proxy(target, handler);
|
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 './useAPIClient';
|
||||||
export * from './useRequest';
|
export * from './useRequest';
|
||||||
export * from './useResource';
|
export * from './useResource';
|
||||||
|
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { useContext } from 'react';
|
import { merge } from '@formily/shared';
|
||||||
import { AxiosRequestConfig } from 'axios';
|
|
||||||
import { Options } from 'ahooks/lib/useRequest/src/types';
|
|
||||||
import { default as useReq } from 'ahooks/lib/useRequest';
|
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 { APIClientContext } from '../context';
|
||||||
|
import { assign } from './assign';
|
||||||
|
|
||||||
type FunctionService = (...args: any[]) => Promise<any>;
|
type FunctionService = (...args: any[]) => Promise<any>;
|
||||||
|
|
||||||
@ -15,20 +18,43 @@ type ResourceActionOptions<P = any> = {
|
|||||||
|
|
||||||
export function useRequest<P>(
|
export function useRequest<P>(
|
||||||
service: AxiosRequestConfig<P> | ResourceActionOptions<P> | FunctionService,
|
service: AxiosRequestConfig<P> | ResourceActionOptions<P> | FunctionService,
|
||||||
options?: Options<any, any>,
|
options?: Options<any, any> & { uid?: string },
|
||||||
) {
|
) {
|
||||||
const api = useContext(APIClientContext);
|
const api = useContext(APIClientContext);
|
||||||
if (typeof service === 'function') {
|
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 result = useReq(
|
||||||
const { resource } = service as ResourceActionOptions;
|
async (params = {}) => {
|
||||||
if (resource) {
|
const { resource } = service as ResourceActionOptions;
|
||||||
Object.assign(service, { params });
|
let args = cloneDeep(service);
|
||||||
} else {
|
if (resource) {
|
||||||
Object.assign(service, params);
|
args.params = args.params || {};
|
||||||
}
|
assign(args.params, params);
|
||||||
const response = await api.request(service);
|
} else {
|
||||||
return response?.data;
|
args = merge(args, params);
|
||||||
}, options);
|
}
|
||||||
|
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 {
|
class APIClient {
|
||||||
// axios 实例
|
// axios 实例
|
||||||
axios: AxiosInstance;
|
axios: AxiosInstance;
|
||||||
|
// 缓存带 uid 的 useRequest({}, {uid}) 的结果,可供其他组件调用
|
||||||
|
services: Record<string, Result<any, any>>;
|
||||||
// 构造器
|
// 构造器
|
||||||
constructor(instance?: AxiosInstance | AxiosRequestConfig);
|
constructor(instance?: AxiosInstance | AxiosRequestConfig);
|
||||||
// 客户端请求,支持 AxiosRequestConfig 和 ResourceActionOptions
|
// 客户端请求,支持 AxiosRequestConfig 和 ResourceActionOptions
|
||||||
@ -42,6 +44,10 @@ const instance = axios.create({
|
|||||||
const apiClient = new APIClient(instance);
|
const apiClient = new APIClient(instance);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`api.service(uid)` 的例子,ComponentB 里刷新 ComponentA 的请求数据
|
||||||
|
|
||||||
|
<code src="./demos/demo3.tsx" />
|
||||||
|
|
||||||
## APIClientProvider
|
## APIClientProvider
|
||||||
|
|
||||||
提供 APIClient 实例的上下文。
|
提供 APIClient 实例的上下文。
|
||||||
|
Loading…
Reference in New Issue
Block a user