diff --git a/CHANGELOG.md b/CHANGELOG.md
index a9fdf6c..8e74f40 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,22 @@
### 破坏性更新
- 删除hooks函数 `useDefined`
+- 模型删除`onDestroy`事件钩子
+- 持久化删除`maxAge`配置
+
+```diff
+store.init({
+ persist: [
+ {
+ key: 'key',
+- maxAge: 3600,
+ engines: engines.localStorage,
+ models: [],
+ }
+ ]
+})
+```
+
- 非hooks状态下计算属性使用执行函数的方式获取
```diff
@@ -50,9 +66,42 @@ const App: FC = () => {
}
```
+- 持久化增加 **dump** 和 **load** 两个过滤函数
+
+```diff
+const model = defineModel('model', {
+ initialState: { firstName: 'tick', lastName: 'tock' },
+ persist: {
++ dump(state) {
++ return state.firstName;
++ },
++ load(dumpData) {
++ return { ...this.initialState, firstName: dumpData };
++ },
+ }
+});
+```
+
+- 持久化新增合并模式 `replace`, `merge`(默认), `deep-merge`
+
+```diff
+store.init({
+ persist: [
+ {
+ key: 'item1',
+ version: '1.0',
++ merge: 'deep-merge',
+ engine: engines.localStorage,
+ models: [],
+ },
+ ]
+})
+```
+
### 其它
- npm包转译为ES5语法以兼容更早的浏览器 (#41)
+- immer版本从 10.0.2 降级为 9.0.21
## [2.0.1](https://github.com/foca-js/foca/compare/v2.0.0...v2.0.1) (2023-08-10)
diff --git a/docs/persist.md b/docs/persist.md
index fbac401..9f90a94 100644
--- a/docs/persist.md
+++ b/docs/persist.md
@@ -20,6 +20,7 @@ store.init({
key: '$PROJECT_$ENV',
version: 1,
engine: engines.localStorage,
+ // 模型白名单列表
models: [userModel, agentModel],
},
],
@@ -28,25 +29,22 @@ store.init({
把需要持久化的模型扔进去,foca 就能自动帮你存取数据。
-# 参数说明
+`key`即为存储路径,最好采用**项目名-环境名**的形式组织。纯前端项目如果和其他前端项目共用一个域名,或者在同一域名下,则有可能使用共同的存储空间,因此需要保证`key`是唯一的值。
-#### key
+# 存储引擎
-key 即为存储路径,最好采用`项目名-环境名`的形式组织。纯前端项目如果和其他前端项目共用一个域名,或者在同一域名下,你用 **index.html** 和 **index-test.html** 来访问不同的环境,这很容易导致 key 被覆盖。
-
-#### engine
-
-存储引擎。不同的引擎会把数据存储到不同的空间,使用哪个引擎取决于项目跑在什么环境。为了统一操作,引擎操作都是异步的,目前内置的引擎有:
+不同的引擎会把数据存储到不同的空间,使用哪个引擎取决于项目跑在什么环境。为了统一操作,引擎操作都是异步的,目前内置的引擎有:
- localStorage
- sessionStorage
- memoryStorage
-如果内置引擎无法满足,那么下面列举的第三方库也可以**直接当作**存储引擎:
+如果内置引擎无法满足,那么安装下面列举的第三方库也可以**直接当作**存储引擎:
-- Taro:[foca-taro-storage](https://github.com/foca-js/foca-taro-storage)
-- React-Native:[@react-native-async-storage/async-storage](https://www.npmjs.com/package/@react-native-async-storage/async-storage)
-- 浏览器:[localforage](https://www.npmjs.com/package/localforage)(localStorage, IndexedDB, WebSQL) | [foca-cookie-storage](https://github.com/foca-js/foca-cookie-storage)
+- [localforage](https://www.npmjs.com/package/localforage) (localStorage, IndexedDB, WebSQL) - 浏览器专用
+- [@react-native-async-storage/async-storage](https://www.npmjs.com/package/@react-native-async-storage/async-storage) - React-Native专用
+- [foca-taro-storage](https://github.com/foca-js/foca-taro-storage) - Taro专用
+- [foca-cookie-storage](https://github.com/foca-js/foca-cookie-storage) - 浏览器专用,存储到cookie
如果有必要,你也可以自己实现一个引擎:
@@ -61,92 +59,142 @@ export const customEngine: StorageEngine = {
};
```
-#### version
+# 设置版本号
-版本号。如果数据结构有变化,那么可以直接升级版本号。
+当数据结构变化,我们不得不升级版本号来`删除`持久化数据,版本号又分为`全局版本`和`模型版本`两种。当修改模型内版本号时,仅删除该模型的持久化数据,而修改全局版本号时,白名单内所有模型的持久化数据都被删除。
-注意,**升级版本号会把相关模型的持久化数据都清除掉**,所以请谨慎操作!比如某个持久化模型存有用户 token 信息,那么升级 version 之后,用户再访问你的应用就得重新登录了。所以建议 tokenModel 放在独立的一个配置下,这样不容易被影响到:
+建议优先修改模型内版本号!!
+
+```diff
+const stockModel = defineModel('stock', {
+ initialState: {},
+ persist: {
+ // 模型版本号,影响当前模型
++ version: '2.0',
+ },
+});
-```typescript
store.init({
persist: [
{
key: '$PROJECT_normal_$ENV',
- version: 3.6,
+ // 全局版本号,影响白名单全部模型
++ version: '3.6',
engine: engines.localStorage,
- // 普通的数据放一起
models: [musicModel, stockModel],
},
+ ],
+});
+```
+
+# 数据合并
+
+> v3.0.0
+
+在项目的推进过程中,难免需要根据产品需求更新模型数据结构,结构变化后,我们可以简单粗暴地通过`版本号+1`的方式来删除持久化的数据。但如果只是新增了某一个字段,我们希望持久化恢复时能自动识别。试试推荐的`合并模式`吧:
+
+```diff
+store.init({
+ persist: [
{
- key: '$PROJECT_token_$ENV',
+ key: 'myproject-a-prod',
version: 1,
++ merge: 'merge',
engine: engines.localStorage,
- // 十分重要的数据建议单独存放
- models: [tokenModel],
- },
- {
- key: '$PROJECT_important_$ENV',
- version: 2.1,
- engine: engines.localStorage,
- // 十分重要的数据建议单独存放
- models: [profileModel],
+ models: [userModel],
},
],
});
```
-修改全局配置的 version 总是危险的一意孤行的方式,因为没有人愿意改动某个模型后就大动干戈地清理掉所有模型的缓存数据,这是严重的内耗问题。所以现在,让我们试试模型里的持久化配置:
+很轻松就设置上了,合并模式目前有3种可选的类型:
-```typescript
+- `replace` - 覆盖模式。数据从存储引擎取出后直接覆盖初始数据
+- `merge` - 合并模式(默认)。数据从存储引擎取出后,与初始数据多余部分进行合并,可以理解为 **Object.assign()** 操作
+- `deep-merge` - 二级合并模式。在合并模式的基础上,如果某个key的值为对象,则该对象也会执行合并操作
+
+如果某个模型比较特殊,我们也可以在里面单独设置合并模式。
+
+```diff
const userModel = defineModel('user', {
- initialState: [],
- // 覆盖全局配置,只对当前模型有效
+ initialState: {},
persist: {
- version: 1,
++ merge: 'deep-merge',
},
});
```
-是的,如果有需要,每个模型都可以再次覆盖持久化配置以达到定制的需求。不要太方便呢!
-
-#### maxAge
+接下来看看它的具体表现:
-缓存存活时间。很显然这是针对每个模型的,因为不同模型的数据存入时间不一样,中途还可能更新缓存,所以模型的存活时间从最后一次更新开始计算。
+```typescript
+const persistState = { obj: { test1: 'persist' } };
+const initialState = { obj: { test2: 'initial' }, foo: 'bar' };
+
+// replace 效果
+const state = { obj: { test1: 'persist' } };
+// merge 效果
+const state = { obj: { test1: 'persist' }, foo: 'bar' };
+// deep-merge 效果
+const state = { obj: { test1: 'persist', test2: 'initial' }, foo: 'bar' };
+```
-你可以在模型中单独指定 maxAge 以达到定制效果:
+需要注意的是合并模式对`数组无效`,当持久化数据和初始数据都为数组类型时,会强制使用持久化数据。当持久化数据和初始数据任何一边为数组类型时,会强制使用初始化数据。
```typescript
-// File: store.ts
-store.init({
- persist: [
- {
- ...
- maxAge: 30 * 60 * 1000,
- models: [userModel, agentModel, ...],
- }
- ]
-});
+const persistState = [1, 2, 3]; ✅
+const initialState = [4, 5, 6, 7]; ❌
+// 合并效果
+const state = [1, 2, 3];
-// File: user.model.ts
-const userModel = defineModel({
- initialState: [],
- persist: {
- maxAge: 10 * 60 * 1000,
- },
-});
+// -------------------------
+
+const persistState = [1, 2, 3]; ❌
+const initialState = { foo: 'bar' }; ✅
+// 合并效果
+const state = { foo: 'bar' };
+
+// -------------------------
+
+const persistState = { foo: 'bar' }; ❌
+const initialState = [1, 2, 3]; ✅
+// 合并效果
+const state = [1, 2, 3];
```
-所有模型都会在 30 分钟后失效,除非它们在此期间更新了数据。其中有一个`userModel`的模型会在 10 分钟后就失效,这是你特别指定的。
+# 系列化钩子
+
+> v3.0.0
+
+数据在模型与持久化引擎互相转换期间,我们希望对它进行一些额外操作以满足业务需求。比如:
-!> 不填 maxAge 代表永久缓存。
+- 只缓存部分字段,避免存储尺寸超过存储空间限制
+- 改变数据结构或者内容
+- 更新时间等动态信息
-#### models
+foca提供了一对实用的过滤函数`dump`和`load`。**dump** 即 model->persist,**load** 即 persist->model。
-缓存白名单。把你认为应该缓存的模型填进去。
+```typescript
+const model = defineModel('model', {
+ initialState: {
+ mode: 'foo', // 本地的设置,需要持久化缓存
+ hugeDate: [], // API请求数据,数据量太大
+ },
+ persist: {
+ // 系列化
+ dump(state) {
+ return state.mode;
+ },
+ // 反系列化
+ load(mode) {
+ return { ...this.initialState, mode };
+ },
+ },
+});
+```
# 分组
-我们注意到 persist 其实是个数组,这意味着你可以多填几组配置上去,把不同的模型数据存储到不同的地方。这看起来很酷,但我猜你不一定需要。
+我们注意到 persist 其实是个数组,这意味着你可以多填几组配置上去,把不同的模型数据存储到不同的地方。这看起来很酷,但我猜你不一定需要!
```typescript
import { store, engines } from 'foca';
@@ -154,13 +202,13 @@ import { store, engines } from 'foca';
store.init({
persist: [
{
- key: 'myproject-prod',
+ key: 'myproject-a-prod',
version: 1,
engine: engines.localStorage,
models: [userModel],
},
{
- key: 'myproject-prod',
+ key: 'myproject-b-prod',
version: 5,
engine: engines.sessionStorage,
models: [agentModel, teacherModel],
diff --git a/src/model/cloneModel.ts b/src/model/cloneModel.ts
index 92a17a6..b4b9c89 100644
--- a/src/model/cloneModel.ts
+++ b/src/model/cloneModel.ts
@@ -16,7 +16,11 @@ type OverrideOptions<
Action extends object,
Effect extends object,
Computed extends object,
-> = Pick, EditableKeys>;
+ PersistDump,
+> = Pick<
+ DefineModelOptions,
+ EditableKeys
+>;
export const cloneModel = <
Name extends string,
@@ -24,20 +28,24 @@ export const cloneModel = <
Action extends object,
Effect extends object,
Computed extends object,
+ PersistDump,
>(
uniqueName: Name,
- model: Model,
+ model: Model,
options?:
- | Partial>
+ | Partial>
| ((
- prev: OverrideOptions,
- ) => Partial>),
-): Model => {
+ prev: OverrideOptions,
+ ) => Partial<
+ OverrideOptions
+ >),
+): Model => {
const realModel = model as unknown as InternalModel<
string,
State,
Action,
- Effect
+ Effect,
+ Computed
>;
const prevOpts = realModel._$opts;
diff --git a/src/model/defineModel.ts b/src/model/defineModel.ts
index c30def5..cac57b4 100644
--- a/src/model/defineModel.ts
+++ b/src/model/defineModel.ts
@@ -34,9 +34,10 @@ export const defineModel = <
Action extends object,
Effect extends object,
Computed extends object,
+ PersistDump,
>(
uniqueName: Name,
- options: DefineModelOptions,
+ options: DefineModelOptions,
): Model => {
guard(uniqueName);
@@ -269,6 +270,7 @@ export const defineModel = <
{
name: uniqueName,
_$opts: options,
+ _$persistCtx: getInitialState({}),
},
getState,
),
diff --git a/src/model/types.ts b/src/model/types.ts
index 24189d3..247780f 100644
--- a/src/model/types.ts
+++ b/src/model/types.ts
@@ -1,5 +1,6 @@
import type { AnyAction } from 'redux';
import type { EnhancedEffect } from './enhanceEffect';
+import type { PersistMergeMode } from '../persist/PersistItem';
export interface ComputedFlag {
readonly _computedFlag: never;
@@ -26,21 +27,61 @@ export interface GetInitialState {
readonly initialState: State;
}
-export interface ModelPersist {
+export type ModelPersist = {
/**
- * 持久化版本号,数据结构变化后建议立即升级该版本。默认值:0
+ * 持久化版本号,数据结构变化后建议立即升级该版本。默认值:`0`
*/
version?: number | string;
+
/**
- * 持久化数据活跃时间(ms),默认:Infinity
- */
- maxAge?: number;
- /**
- * 持久化数据恢复到模型时的过滤函数,此时可修改数据以满足业务需求。
- * 如果数据结构变化,则建议直接更新版本号。
+ * 持久化数据与初始数据的合并方式。默认值以全局配置为准
+ *
+ * - replace - 覆盖模式。直接用持久化数据替换初始数据
+ * - merge - 合并模式。持久化数据与初始数据新增的key进行合并,可理解为`Object.assign`
+ * - deep-merge - 二级合并模式。在合并模式的基础上,如果某个key的值为对象,则该对象也会执行合并操作
+ *
+ * 注意:当数据为数组格式时该配置无效。
+ * @since 3.0.0
*/
- decode?: (state: State) => State | void;
-}
+ 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,
@@ -148,7 +189,8 @@ export type InternalModel<
Effect extends object = object,
Computed extends object = object,
> = BaseModel & {
- readonly _$opts: DefineModelOptions;
+ readonly _$opts: DefineModelOptions;
+ readonly _$persistCtx: GetInitialState;
};
export type InternalAction = {
@@ -181,6 +223,7 @@ export interface DefineModelOptions<
Action extends object,
Effect extends object,
Computed extends object,
+ PersistDump,
> {
/**
* 初始状态
@@ -282,7 +325,7 @@ export interface DefineModelOptions<
*
* @see store.init()
*/
- persist?: ModelPersist & ThisType;
+ persist?: ModelPersist & ThisType;
/**
* 生命周期
* @since 0.11.1
diff --git a/src/persist/PersistItem.ts b/src/persist/PersistItem.ts
index 718df61..ca45a83 100644
--- a/src/persist/PersistItem.ts
+++ b/src/persist/PersistItem.ts
@@ -1,6 +1,11 @@
import type { StorageEngine } from '../engines';
-import type { InternalModel, Model, ModelPersist } from '../model/types';
-import { isObject, isString } from '../utils/isType';
+import type {
+ GetInitialState,
+ InternalModel,
+ Model,
+ ModelPersist,
+} from '../model/types';
+import { isObject, isPlainObject, isString } from '../utils/isType';
import { parseState, stringifyState } from '../utils/serialize';
export interface PersistSchema {
@@ -21,29 +26,34 @@ export interface PersistItemSchema {
* 版本
*/
v: number | string;
- /**
- * 存储时间
- */
- t: number;
/**
* 数据
*/
d: string;
}
+export type PersistMergeMode = 'replace' | 'merge' | 'deep-merge';
+
export interface PersistOptions {
- /**
- * 每个模型的最大持久化时间,默认:Infinity。
- */
- maxAge?: number;
/**
* 存储唯一标识名称
*/
key: string;
/**
- * 存储名称前缀,默认值:@@foca.persist:
+ * 存储名称前缀,默认值:`@@foca.persist:`
*/
keyPrefix?: string;
+ /**
+ * 持久化数据与初始数据的合并方式。默认值:`merge`
+ *
+ * - replace - 覆盖模式。数据从存储引擎取出后直接覆盖初始数据
+ * - merge - 合并模式。数据从存储引擎取出后,与初始数据多余部分进行合并,可以理解为`Object.assign()`操作
+ * - deep-merge - 二级合并模式。在合并模式的基础上,如果某个key的值为对象,则该对象也会执行合并操作
+ *
+ * 注意:当数据为数组格式时该配置无效。
+ * @since 3.0.0
+ */
+ merge?: PersistMergeMode;
/**
* 版本号
*/
@@ -58,9 +68,11 @@ export interface PersistOptions {
models: Model[];
}
-type CustomModelPersistOptions = Required>;
+type CustomModelPersistOptions = Required> & {
+ ctx: GetInitialState