,
modelName: string,
computedName: string,
fn: (...args: any[]) => any,
): ComputedFlag => {
let caches: {
deps: any[];
skipCount: number;
ref: ComputedValue;
}[] = [];
function anonymousFn() {
const args = toArgs(arguments);
let hitCache: (typeof caches)[number] | undefined;
searchCache: for (let i = 0; i < caches.length; ++i) {
const cache = caches[i]!;
if (hitCache) {
++cache.skipCount;
continue;
}
for (let j = 0; j < cache.deps.length; ++j) {
if (args[j] !== cache.deps[j]) {
++cache.skipCount;
continue searchCache;
}
}
cache.skipCount = 0;
hitCache = cache;
}
if (hitCache) return hitCache.ref.value;
if (caches.length > 10) {
caches = caches.filter((cache) => cache.skipCount < 15);
}
hitCache = {
deps: args,
skipCount: 0,
ref: new ComputedValue(modelStore, modelName, computedName, () =>
fn.apply(ctx, args),
),
};
caches.push(hitCache);
return hitCache.ref.value;
}
return anonymousFn as any;
};
================================================
FILE: src/model/enhance-effect.ts
================================================
import {
LoadingAction,
LOADING_CATEGORY,
TYPE_SET_LOADING,
} from '../actions/loading';
import type { EffectCtx } from './types';
import { isPromise } from '../utils/is-promise';
import { toArgs } from '../utils/to-args';
import { loadingStore } from '../store/loading-store';
interface RoomFunc> {
(category: number | string): {
execute(...args: P): R;
};
}
interface AsyncRoomEffect
>
extends RoomFunc
{
readonly _: {
readonly model: string;
readonly method: string;
readonly hasRoom: true;
};
}
interface AsyncEffect
>
extends EffectFunc
{
readonly _: {
readonly model: string;
readonly method: string;
readonly hasRoom: '';
};
/**
* 对同一effect函数的执行状态进行分类以实现独立保存。好处有:
*
* 1. 并发请求同一个请求时不会互相覆盖执行状态。
*
* 2. 可以精确地判断业务中是哪个控件或者逻辑正在执行。
*
* ```typescript
* model.effect.room(CATEGORY).execute(...);
* ```
*
* @see useLoading(effect.room)
* @see getLoading(effect.room)
* @since 0.11.4
*
*/
readonly room: AsyncRoomEffect
;
}
export type PromiseEffect = AsyncEffect;
export type PromiseRoomEffect = AsyncRoomEffect;
interface EffectFunc
> {
(...args: P): R;
}
export type EnhancedEffect
> =
R extends Promise ? AsyncEffect : EffectFunc
;
type NonReadonly = {
-readonly [K in keyof T]: T[K];
};
export const enhanceEffect = (
ctx: EffectCtx,
methodName: string,
effect: (...args: any[]) => any,
): EnhancedEffect => {
const fn: NonReadonly & EffectFunc = function () {
return execute(ctx, methodName, effect, toArgs(arguments));
};
fn._ = {
model: ctx.name,
method: methodName,
hasRoom: '',
};
const room: NonReadonly & RoomFunc = (
category: number | string,
) => ({
execute() {
return execute(ctx, methodName, effect, toArgs(arguments), category);
},
});
room._ = Object.assign({}, fn._, {
hasRoom: true as const,
});
fn.room = room;
return fn;
};
const dispatchLoading = (
modelName: string,
methodName: string,
loading: boolean,
category?: number | string,
) => {
loadingStore.dispatch({
type: TYPE_SET_LOADING,
model: modelName,
method: methodName,
payload: {
category: category === void 0 ? LOADING_CATEGORY : category,
loading,
},
});
};
const execute = (
ctx: EffectCtx,
methodName: string,
effect: (...args: any[]) => any,
args: any[],
category?: number | string,
) => {
const modelName = ctx.name;
const resultOrPromise = effect.apply(ctx, args);
if (!isPromise(resultOrPromise)) return resultOrPromise;
dispatchLoading(modelName, methodName, true, category);
return resultOrPromise.then(
(result) => {
return dispatchLoading(modelName, methodName, false, category), result;
},
(e: unknown) => {
dispatchLoading(modelName, methodName, false, category);
throw e;
},
);
};
================================================
FILE: src/model/guard.ts
================================================
const counter: Record = {};
export const guard = (modelName: string) => {
counter[modelName] ||= 0;
if (process.env.NODE_ENV !== 'production') {
setTimeout(() => {
--counter[modelName]!;
});
}
if (++counter[modelName] > 1) {
throw new Error(`模型名称'${modelName}'被重复使用`);
}
};
================================================
FILE: src/model/types.ts
================================================
import type { UnknownAction } from 'redux';
import type { EnhancedEffect } from './enhance-effect';
import type { PersistMergeMode } from '../persist/persist-item';
export interface ComputedFlag {
readonly _computedFlag: never;
}
export interface GetName {
/**
* 模型名称。请在定义模型时确保是唯一的字符串
*/
readonly name: Name;
}
export interface GetState {
/**
* 模型的实时状态
*/
readonly state: State;
}
export interface GetInitialState {
/**
* 模型的初始状态,每次获取该属性都会执行深拷贝操作
*/
readonly initialState: State;
}
export type ModelPersist = {
/**
* 持久化版本号,数据结构变化后建议立即升级该版本。默认值:`0`
*/
version?: number | string;
/**
* 持久化数据与初始数据的合并方式。默认值以全局配置为准
*
* - replace - 覆盖模式。直接用持久化数据替换初始数据
* - merge - 合并模式。持久化数据与初始数据新增的key进行合并,可理解为`Object.assign`
* - deep-merge - 二级合并模式。在合并模式的基础上,如果某个key的值为对象,则该对象也会执行合并操作
*
* 注意:当数据为数组格式时该配置无效。
* @since 3.0.0
*/
merge?: PersistMergeMode;
} & (
| {
/**
* 模型数据从内存存储到持久化引擎时的过滤函数,允许你只持久化部分数据。
* ```typescript
*
* // state = { firstName: 'tick', lastName: 'tock' }
* dump: (state) => state
* dump: (state) => state.firstName
* dump: (state) => ({ name: state.lastName })
* ```
*
* @since 3.0.0
*/
dump: (state: State) => PersistDump;
/**
* 持久化数据恢复到模型内存时的过滤函数,参数为`dump`返回的值。
* ```typescript
* // state = { firstName: 'tick', lastName: 'tock' }
* {
* dump(state) {
* return state.firstName
* },
* load(firstName) {
* return { ...this.initialState, firstName: firstName };
* }
* }
* ```
*
* @since 3.0.0
*/
load: (this: GetInitialState, dumpData: PersistDump) => State;
}
| {
dump?: never;
load?: never;
}
);
export interface ActionCtx
extends GetName,
GetInitialState {}
export interface EffectCtx
extends ActionCtx,
GetState {
/**
* 立即更改状态,支持**immer**操作
*
* ```typescript
* this.setState((state) => {
* state.count += 1;
* });
* ```
*
* 对于object类型,你可以直接传递 **全部** 或者 **部分** 数据
* ```typescript
* interface State { id: number; name: string };
*
* this.setState({}); // 什么也没修改
* this.setState({ id: 10 }); // 只修改id
* this.setState({ id: 10, name: 'foo' }); // 修改全部
*
* this.setState((state) => {
* return {}; // 什么也没修改
* });
* this.setState((state) => {
* return { id: 10 }; // 只修改id
* });
* this.setState((state) => {
* return { id: 10, name: 'foo' }; // 修改全部
* });
* ```
*
* 对于array类型,直接传递数组就行了
* ```typescript
* this.setState(['a', 'b', 'c']);
* ```
*/
readonly setState: State extends any[]
? (state: State | ((state: State) => State | void)) => UnknownAction
: (
state: SetStateCallback | (Pick | State),
) => UnknownAction;
}
export interface SetStateCallback {
(state: State): Pick | State | void;
}
export interface ComputedCtx
extends GetName,
GetState {}
export interface BaseModel
extends GetState,
GetName {}
type ModelActionItem<
State extends object,
Action extends object,
K extends keyof Action,
> = Action[K] extends (state: State, ...args: infer P) => State | void
? (...args: P) => UnknownAction
: never;
type ModelAction = {
readonly [K in keyof Action]: ModelActionItem;
};
type GetPrivateMethodKeys = {
[K in keyof Method]: K extends `_${string}` ? K : never;
}[keyof Method];
type ModelEffect = {
readonly [K in keyof Effect]: Effect[K] extends (...args: infer P) => infer R
? EnhancedEffect
: never;
};
type ModelComputed = {
readonly [K in keyof Computed]: Computed[K] & ComputedFlag;
};
export type Model<
Name extends string = string,
State extends object = object,
Action extends object = object,
Effect extends object = object,
Computed extends object = object,
> = BaseModel &
// [K in keyof Action as K extends `_${string}` ? never : K]
// 上面这种看起来简洁,业务代码提示也正常,但是业务代码那边无法点击跳转进模型了。
// 所以需要先转换所有的属性,再把私有属性去除。
Omit, GetPrivateMethodKeys> &
Omit, GetPrivateMethodKeys> &
Omit, GetPrivateMethodKeys>;
export type InternalModel<
Name extends string = string,
State extends object = object,
Action extends object = object,
Effect extends object = object,
Computed extends object = object,
> = BaseModel & {
readonly _$opts: DefineModelOptions;
readonly _$persistCtx: GetInitialState;
};
export type InternalAction = {
[key: string]: (state: State, ...args: any[]) => State | void;
};
export interface Event {
/**
* store初始化完成,并且持久化(如果有)的数据也已经恢复。
*
* 上下文 **this** 可以直接调用actions和effects的函数以及computed计算属性。
*/
onInit?: () => void;
/**
* 每当state有变化时的回调通知。
*
* 初始化(onInit)执行之前不会触发该回调。如果在onInit中做了修改state的操作,则会触发该回调。
*
* 上下文 **this** 可以直接调用actions和effects的函数以及computed计算属性,请谨慎执行修改数据的操作以防止死循环。
*/
onChange?: (prevState: State, nextState: State) => void;
/**
* 销毁模型时的回调通知,此时模型已经被销毁。
* 该事件仅在局部模型生效
* @see useIsolate
*/
onDestroy?: (this: never, modelName: string) => void;
}
export interface EventCtx
extends GetName,
GetState {}
export interface DefineModelOptions<
State extends object,
Action extends object,
Effect extends object,
Computed extends object,
PersistDump,
> {
/**
* 初始状态
*
* ```typescript
* cosnt initialState: {
* count: number;
* } = {
* count: 0,
* }
*
* const model = defineModel('model1', {
* initialState
* });
* ```
*/
initialState: State;
/**
* 定义修改状态的方法。参数一自动推断为state类型。支持**immer**操作。支持多参数。
*
* ```typescript
* const model = defineModel('model1', {
* initialState,
* reducers: {
* plus(state, step: number) {
* state.count += step;
* },
* minus(state, step: number, scale: 1 | 2) {
* state.count -= step * scale;
* }
* },
* });
* ```
*/
reducers?: Action & InternalAction & ThisType>;
/**
* 定义普通方法,异步方法等。
* 调用effect方法时,一般会伴随异步操作(请求数据、耗时任务),框架会自动收集当前方法的调用状态。
*
* ```typescript
* const model = defineModel('model1', {
* initialState,
* methods: {
* async foo(p1: string, p2: number) {
* const result = await Promise.resolve();
* this.setState({ x: result });
* return 'OK';
* }
* },
* });
*
* useLoading(model.foo); // 返回值类型: boolean
* ```
*/
methods?: Effect &
ThisType & Effect & Computed & EffectCtx>;
/**
* 定义计算属性。针对需要复杂的计算才能得出结果的场景而设计。如果只是简单的返回,建议使用`methods`
*
* ```typescript
* const initialState = { firstName: 'tick', lastName: 'tock' };
*
* const model = defineModel('model1', {
* initialState,
* computed: {
* fullname() {
* return this.state.firstName + '.' + this.state.lastName;
* },
* names() {
* return this.fullName.value.split('').map((item) => `[${item}]`);
* }
* },
* });
* ```
*
* 可以单独使用:
* ```typescript
* model.fullname; // ComputedRef;
* model.fullname.value; // string;
* ```
*
* 可以配合react hooks使用:
*
* ```typescript
* const fullname = useComputed(model.fullname); // string
* ```
*/
computed?: Computed & ThisType>;
/**
* 是否阻止刷新数据时跳过当前模型,默认即不跳过。
*
* 如果是强制刷新,则该参数无效。
*
* @see store.refresh(force: boolean = false)
*/
skipRefresh?: boolean;
/**
* 定制持久化,请确保已经在初始化store的时候把当前模型加入persist配置,否则当前设置无效
*
* @see store.init()
*/
persist?: ModelPersist & ThisType;
/**
* 生命周期
* @since 0.11.1
*/
events?: Event &
ThisType & Computed & Effect & EventCtx>;
}
================================================
FILE: src/persist/persist-gate.tsx
================================================
import { ReactNode, FC, useState, useEffect } from 'react';
import { modelStore } from '../store/model-store';
import { isFunction } from '../utils/is-type';
export interface PersistGateProps {
loading?: ReactNode;
children?: ReactNode | ((isReady: boolean) => ReactNode);
}
export const PersistGate: FC = (props) => {
const state = useState(() => modelStore.isReady),
isReady = state[0],
setIsReady = state[1];
const { loading = null, children } = props;
useEffect(() => {
isReady ||
modelStore.onInitialized().then(() => {
setIsReady(true);
});
}, []);
/* istanbul ignore else -- @preserve */
if (process.env.NODE_ENV !== 'production') {
if (loading && isFunction(children)) {
console.error('[PersistGate] 当前children为函数类型,loading属性无效');
}
}
return (
<>
{isFunction(children) ? children(isReady) : isReady ? children : loading}
>
);
};
================================================
FILE: src/persist/persist-item.ts
================================================
import type { StorageEngine } from '../engines/storage-engine';
import type {
GetInitialState,
InternalModel,
Model,
ModelPersist,
} from '../model/types';
import { isObject, isPlainObject, isString } from '../utils/is-type';
import { toPromise } from '../utils/to-promise';
import { parseState, stringifyState } from '../utils/serialize';
export interface PersistSchema {
/**
* 版本
*/
v: number | string;
/**
* 数据
*/
d: {
[key: string]: PersistItemSchema;
};
}
export interface PersistItemSchema {
/**
* 版本
*/
v: number | string;
/**
* 数据
*/
d: string;
}
export type PersistMergeMode = 'replace' | 'merge' | 'deep-merge';
export interface PersistOptions {
/**
* 存储唯一标识名称
*/
key: string;
/**
* 存储名称前缀,默认值:`@@foca.persist:`
*/
keyPrefix?: string;
/**
* 持久化数据与初始数据的合并方式。默认值:`merge`
*
* - replace - 覆盖模式。数据从存储引擎取出后直接覆盖初始数据
* - merge - 合并模式。数据从存储引擎取出后,与初始数据多余部分进行合并,可以理解为`Object.assign()`操作
* - deep-merge - 二级合并模式。在合并模式的基础上,如果某个key的值为对象,则该对象也会执行合并操作
*
* 注意:当数据为数组格式时该配置无效。
* @since 3.0.0
*/
merge?: PersistMergeMode;
/**
* 版本号
*/
version: string | number;
/**
* 存储引擎
*/
engine: StorageEngine;
/**
* 允许持久化的模型列表
*/
models: Model[];
}
type CustomModelPersistOptions = Required> & {
ctx: GetInitialState