feat: api service support

This commit is contained in:
chenos 2022-01-26 18:20:31 +08:00
parent 765bf9daa9
commit d0b6efaaf5
6 changed files with 237 additions and 17 deletions

View File

@ -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);
}
}
}

View 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>
);
};

View 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;
}

View File

@ -1,3 +1,4 @@
export * from './useAPIClient';
export * from './useRequest';
export * from './useResource';

View File

@ -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;
}

View File

@ -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 实例的上下文。