优化
This commit is contained in:
126
client/shared/manager/__tests__/buildReg.spec.ts
Normal file
126
client/shared/manager/__tests__/buildReg.spec.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { buildCachedRegFn, buildRegFn } from '../buildReg';
|
||||
|
||||
describe('buildRegFn should be ok', () => {
|
||||
test('normal', () => {
|
||||
const [get, set] = buildRegFn('test');
|
||||
const fn = jest.fn();
|
||||
set(fn);
|
||||
|
||||
get();
|
||||
get(1);
|
||||
get(2);
|
||||
|
||||
const fn2 = jest.fn();
|
||||
set(fn2);
|
||||
|
||||
get(3);
|
||||
|
||||
expect(fn.mock.calls.length).toBe(3);
|
||||
expect(fn.mock.calls[0]).toEqual([]);
|
||||
expect(fn.mock.calls[1]).toEqual([1]);
|
||||
expect(fn.mock.calls[2]).toEqual([2]);
|
||||
|
||||
expect(fn2.mock.calls[0]).toEqual([3]);
|
||||
});
|
||||
|
||||
test('with default', () => {
|
||||
const fn = jest.fn();
|
||||
const [get, set] = buildRegFn('test', fn);
|
||||
get();
|
||||
get(1);
|
||||
get(2);
|
||||
|
||||
const fn2 = jest.fn();
|
||||
set(fn2);
|
||||
get(3);
|
||||
|
||||
expect(fn.mock.calls.length).toBe(3);
|
||||
expect(fn.mock.calls[0]).toEqual([]);
|
||||
expect(fn.mock.calls[1]).toEqual([1]);
|
||||
expect(fn.mock.calls[2]).toEqual([2]);
|
||||
|
||||
expect(fn2.mock.calls[0]).toEqual([3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildCachedRegFn should be ok', () => {
|
||||
test('normal', () => {
|
||||
const [get, set] = buildCachedRegFn('test');
|
||||
const fn = jest.fn();
|
||||
set(fn);
|
||||
get(1);
|
||||
|
||||
const fn2 = jest.fn();
|
||||
set(fn2);
|
||||
get(2);
|
||||
|
||||
expect(fn.mock.calls[0]).toEqual([1]);
|
||||
expect(fn2.mock.calls[0]).toEqual([2]);
|
||||
});
|
||||
|
||||
test('should be cache value', () => {
|
||||
const [get, set] = buildCachedRegFn('test');
|
||||
const fn = jest.fn((v) => v);
|
||||
set(fn);
|
||||
const res1 = get(1);
|
||||
const res2 = get(1);
|
||||
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
expect(fn.mock.calls[0]).toEqual([1]);
|
||||
expect(res1).toBe(res2);
|
||||
});
|
||||
|
||||
test('should be refresh if re-set', () => {
|
||||
const [get, set] = buildCachedRegFn('test');
|
||||
const fn = jest.fn((v) => v);
|
||||
set(fn);
|
||||
get(1);
|
||||
get(1);
|
||||
|
||||
const fn2 = jest.fn((v) => v);
|
||||
set(fn2);
|
||||
get(1);
|
||||
get(1);
|
||||
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
expect(fn.mock.calls[0]).toEqual([1]);
|
||||
|
||||
expect(fn2.mock.calls.length).toBe(1);
|
||||
expect(fn2.mock.calls[0]).toEqual([1]);
|
||||
});
|
||||
|
||||
test('should call forever if return null', () => {
|
||||
const [get, set] = buildCachedRegFn('test');
|
||||
const fn = jest.fn(() => null);
|
||||
set(fn);
|
||||
|
||||
get();
|
||||
get();
|
||||
get();
|
||||
expect(fn.mock.calls.length).toBe(3);
|
||||
});
|
||||
|
||||
test('should call forever if return undefined', () => {
|
||||
const [get, set] = buildCachedRegFn('test');
|
||||
const fn = jest.fn(() => undefined);
|
||||
set(fn);
|
||||
|
||||
get();
|
||||
get();
|
||||
get();
|
||||
expect(fn.mock.calls.length).toBe(3);
|
||||
});
|
||||
|
||||
test('should cache promise fn', () => {
|
||||
const [get, set] = buildCachedRegFn('test');
|
||||
const fn = jest.fn(async () => undefined);
|
||||
set(fn);
|
||||
|
||||
get();
|
||||
get();
|
||||
get();
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
get(1);
|
||||
expect(fn.mock.calls.length).toBe(2);
|
||||
});
|
||||
});
|
||||
173
client/shared/manager/buildReg.ts
Normal file
173
client/shared/manager/buildReg.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import _isFunction from 'lodash/isFunction';
|
||||
import _isEqual from 'lodash/isEqual';
|
||||
|
||||
/**
|
||||
* 构建一对get set 方法
|
||||
* 用于在不同平台拥有统一方式调用体验
|
||||
*/
|
||||
export function buildRegFn<F extends (...args: any[]) => any>(
|
||||
name: string,
|
||||
defaultFunc?: F
|
||||
) {
|
||||
let func: F | undefined;
|
||||
|
||||
const get = (...args: Parameters<F>): ReturnType<F> => {
|
||||
if (!func) {
|
||||
if (_isFunction(defaultFunc)) {
|
||||
return defaultFunc(...args);
|
||||
}
|
||||
|
||||
throw new Error(`${name} not regist`);
|
||||
}
|
||||
return func(...args);
|
||||
};
|
||||
|
||||
const set = (fn: F): void => {
|
||||
func = fn;
|
||||
};
|
||||
|
||||
const reset = (): void => {
|
||||
func = defaultFunc;
|
||||
};
|
||||
|
||||
return [get, set, reset] as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建带事件监听的get set 方法
|
||||
*/
|
||||
export function buildRegFnWithEvent<F extends (...args: any[]) => any>(
|
||||
name: string,
|
||||
defaultFunc?: F
|
||||
) {
|
||||
const [get, _set] = buildRegFn(name, defaultFunc);
|
||||
const listenerList: ((v: F) => void)[] = [];
|
||||
const onSet = (cb: (v: F) => void): void => {
|
||||
listenerList.push(cb);
|
||||
};
|
||||
|
||||
const set = (fn: F): void => {
|
||||
_set(fn);
|
||||
listenerList.forEach((listener) => listener(fn));
|
||||
};
|
||||
|
||||
return [get, set, onSet] as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存版本的buildRegFn
|
||||
*/
|
||||
export function buildCachedRegFn<F extends (...args: any) => any>(
|
||||
name: string,
|
||||
defaultFunc?: F
|
||||
) {
|
||||
const [get, set] = buildRegFn(name, defaultFunc);
|
||||
|
||||
let _result: any = null; // 缓存的返回值
|
||||
let _lastArgs: any;
|
||||
|
||||
function isSame(args: any[]) {
|
||||
// 当有缓存的返回值且两次参数一致
|
||||
return _result !== null && _isEqual(args, _lastArgs);
|
||||
}
|
||||
|
||||
// 根据是否为 promise 做区分
|
||||
const cachedGet: any = (...args: any) => {
|
||||
if (isSame(args)) {
|
||||
return _result;
|
||||
} else {
|
||||
const result = get(...args);
|
||||
_result = result ?? null;
|
||||
_lastArgs = args;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshCache = () => {
|
||||
_result = null;
|
||||
};
|
||||
|
||||
const cachedSet = (fn: F) => {
|
||||
set(fn);
|
||||
refreshCache();
|
||||
};
|
||||
|
||||
return [cachedGet, cachedSet, refreshCache];
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存版本的buildRegFn
|
||||
* Promise 版本
|
||||
*/
|
||||
export function buildCachedRegFnAsync<F extends (...args: any) => any>(
|
||||
name: string,
|
||||
defaultFunc?: F
|
||||
) {
|
||||
const [get, set] = buildRegFn(name, defaultFunc);
|
||||
|
||||
let _result: any = null; // 缓存的返回值
|
||||
let _lastArgs: any;
|
||||
|
||||
function isSame(args: any[]) {
|
||||
// 当有缓存的返回值且两次参数一致
|
||||
return _result !== null && _isEqual(args, _lastArgs);
|
||||
}
|
||||
|
||||
// 根据是否为 promise 做区分
|
||||
const cachedGet: any = async (...args: any) => {
|
||||
if (isSame(args)) {
|
||||
return _result;
|
||||
} else {
|
||||
const result = await get(...args);
|
||||
_result = result ?? null;
|
||||
_lastArgs = args;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshCache = () => {
|
||||
_result = null;
|
||||
};
|
||||
|
||||
const cachedSet = (fn: F) => {
|
||||
set(fn);
|
||||
refreshCache();
|
||||
};
|
||||
|
||||
return [cachedGet, cachedSet, refreshCache];
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建一组注册列表的方式
|
||||
* 用于从其他地方统一获取数据
|
||||
*/
|
||||
export function buildRegList<T>(): [T[], (item: T) => void] {
|
||||
const list: T[] = [];
|
||||
|
||||
const reg = (item: T) => {
|
||||
list.push(item);
|
||||
};
|
||||
|
||||
return [list, reg];
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建一组注册Mapping的方式
|
||||
* 用于从其他地方统一获取数据
|
||||
*/
|
||||
export function buildRegMap<T>(): [
|
||||
Record<string, T>,
|
||||
(name: string, item: T) => void
|
||||
] {
|
||||
const mapping: Record<string, T> = {};
|
||||
|
||||
const reg = (name: string, item: T) => {
|
||||
if (mapping[name]) {
|
||||
console.warn('[buildRegMap] 重复注册:', name);
|
||||
}
|
||||
|
||||
mapping[name] = item;
|
||||
};
|
||||
|
||||
return [mapping, reg];
|
||||
}
|
||||
9
client/shared/manager/request.ts
Normal file
9
client/shared/manager/request.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { buildRegFn, buildCachedRegFnAsync } from './buildReg';
|
||||
|
||||
export const [getErrorHook, setErrorHook] = buildRegFn<(err: any) => boolean>(
|
||||
'requestErrorHook',
|
||||
() => true
|
||||
);
|
||||
|
||||
export const [tokenGetter, setTokenGetter, refreshTokenGetter] =
|
||||
buildCachedRegFnAsync<() => Promise<string>>('requestTokenGetter');
|
||||
8
client/shared/manager/service.ts
Normal file
8
client/shared/manager/service.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { buildRegFnWithEvent } from './buildReg';
|
||||
|
||||
/**
|
||||
* 获取服务器地址相关逻辑
|
||||
* @default window.location.origin (前后端一体)
|
||||
*/
|
||||
export const [getServiceUrl, setServiceUrl, onServiceUrlChange] =
|
||||
buildRegFnWithEvent<() => string>('serverUrl', () => window.location.origin);
|
||||
8
client/shared/manager/socket.ts
Normal file
8
client/shared/manager/socket.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { buildRegList } from './buildReg';
|
||||
|
||||
interface PluginSocketEventListener {
|
||||
eventName: string;
|
||||
eventFn: (...args: any[]) => void;
|
||||
}
|
||||
export const [socketEventListeners, regSocketEventListener] =
|
||||
buildRegList<PluginSocketEventListener>();
|
||||
66
client/shared/manager/storage.ts
Normal file
66
client/shared/manager/storage.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { buildRegFn } from './buildReg';
|
||||
import { useCallback, useLayoutEffect, useState } from 'react';
|
||||
|
||||
export interface StorageObject {
|
||||
/**
|
||||
* NOTICE: 与save不同, set存储 1 天
|
||||
*/
|
||||
set: (key: string, data: any) => Promise<void>;
|
||||
get: (key: string, defaultVal?: any) => Promise<any>;
|
||||
remove: (key: string) => Promise<void>;
|
||||
save: (key: string, data: any) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化存储相关逻辑
|
||||
*/
|
||||
export const [getStorage, setStorage] =
|
||||
buildRegFn<() => StorageObject>('storage');
|
||||
|
||||
/**
|
||||
* 持久化hook的缓存, 减少因一步获取数据导致的gap
|
||||
*/
|
||||
const useStorageCache = new Map<string, any>();
|
||||
|
||||
/**
|
||||
* 管理持久化存储数据
|
||||
* @param key 存储键
|
||||
* @param defaultValue 默认值
|
||||
*/
|
||||
export function useStorage<T = any>(
|
||||
key: string,
|
||||
defaultValue?: T
|
||||
): [T | undefined, { set: (v: T) => void; save: (v: T) => void }] {
|
||||
const [value, setValue] = useState<T | undefined>(
|
||||
useStorageCache.get(key) ?? defaultValue
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
getStorage()
|
||||
.get(key)
|
||||
.then((data: T) => {
|
||||
setValue(data ?? defaultValue);
|
||||
useStorageCache.set(key, data ?? defaultValue);
|
||||
});
|
||||
}, [key]);
|
||||
|
||||
const set = useCallback(
|
||||
(newVal: T) => {
|
||||
setValue(newVal);
|
||||
getStorage().set(key, newVal);
|
||||
useStorageCache.set(key, newVal);
|
||||
},
|
||||
[key]
|
||||
);
|
||||
|
||||
const save = useCallback(
|
||||
(newVal: T) => {
|
||||
setValue(newVal);
|
||||
getStorage().save(key, newVal);
|
||||
useStorageCache.set(key, newVal);
|
||||
},
|
||||
[key]
|
||||
);
|
||||
|
||||
return [value, { set, save }];
|
||||
}
|
||||
57
client/shared/manager/ui.ts
Normal file
57
client/shared/manager/ui.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { t } from '../i18n';
|
||||
import { buildRegFn } from './buildReg';
|
||||
|
||||
/**
|
||||
* 通用UI api设置
|
||||
*/
|
||||
|
||||
type ToastsType = 'info' | 'success' | 'error' | 'warning';
|
||||
export const [showToasts, setToasts] =
|
||||
buildRegFn<(message: string, type?: ToastsType) => void>('toasts');
|
||||
|
||||
/**
|
||||
* 一个封装方法, 用于直接抛出错误
|
||||
* @param error 错误信息
|
||||
*/
|
||||
export function showErrorToasts(error: unknown) {
|
||||
let msg = '';
|
||||
if (error instanceof Error) {
|
||||
msg = error.message;
|
||||
} else {
|
||||
msg = String(error);
|
||||
}
|
||||
|
||||
showToasts(msg, 'error');
|
||||
}
|
||||
|
||||
/**
|
||||
* 展示成功消息
|
||||
*/
|
||||
export function showSuccessToasts(msg = t('操作成功')) {
|
||||
showToasts(msg, 'success');
|
||||
}
|
||||
|
||||
interface AlertOptions {
|
||||
message: React.ReactNode;
|
||||
onConfirm?: () => void | Promise<void>;
|
||||
}
|
||||
export const [showAlert, setAlert] =
|
||||
buildRegFn<(options: AlertOptions) => void>('alert');
|
||||
|
||||
/**
|
||||
* 全局Loading提示
|
||||
* 返回移除函数
|
||||
*/
|
||||
export const [showGlobalLoading, setGlobalLoading] = buildRegFn<
|
||||
(text: string) => () => void
|
||||
>('global-loading', () => {
|
||||
return () => {};
|
||||
});
|
||||
|
||||
/**
|
||||
* 提示通知,返回关闭函数
|
||||
*/
|
||||
export const [showNotification, setNotification] =
|
||||
buildRegFn<(message: React.ReactNode, duration?: number) => () => void>(
|
||||
'notification'
|
||||
);
|
||||
Reference in New Issue
Block a user