Skip to content

Commit

Permalink
feat!: persist support merge mode and seralize hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
geekact committed Oct 1, 2023
1 parent 0901809 commit 689fee5
Show file tree
Hide file tree
Showing 15 changed files with 911 additions and 397 deletions.
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@
### 破坏性更新

- 删除hooks函数 `useDefined`
- 模型删除`onDestroy`事件钩子
- 持久化删除`maxAge`配置

```diff
store.init({
persist: [
{
key: 'key',
- maxAge: 3600,
engines: engines.localStorage,
models: [],
}
]
})
```

- 非hooks状态下计算属性使用执行函数的方式获取

```diff
Expand Down Expand Up @@ -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`<small>(默认)</small>, `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)&nbsp;&nbsp;(2023-08-10)

Expand Down
170 changes: 109 additions & 61 deletions docs/persist.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ store.init({
key: '$PROJECT_$ENV',
version: 1,
engine: engines.localStorage,
// 模型白名单列表
models: [userModel, agentModel],
},
],
Expand All @@ -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

如果有必要,你也可以自己实现一个引擎:

Expand All @@ -61,106 +59,156 @@ 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';

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],
Expand Down
22 changes: 15 additions & 7 deletions src/model/cloneModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,36 @@ type OverrideOptions<
Action extends object,
Effect extends object,
Computed extends object,
> = Pick<DefineModelOptions<State, Action, Effect, Computed>, EditableKeys>;
PersistDump,
> = Pick<
DefineModelOptions<State, Action, Effect, Computed, PersistDump>,
EditableKeys
>;

export const cloneModel = <
Name extends string,
State extends object,
Action extends object,
Effect extends object,
Computed extends object,
PersistDump,
>(
uniqueName: Name,
model: Model<string, State, Action, Effect>,
model: Model<string, State, Action, Effect, Computed>,
options?:
| Partial<OverrideOptions<State, Action, Effect, Computed>>
| Partial<OverrideOptions<State, Action, Effect, Computed, PersistDump>>
| ((
prev: OverrideOptions<State, Action, Effect, Computed>,
) => Partial<OverrideOptions<State, Action, Effect, Computed>>),
): Model<Name, State, Action, Effect> => {
prev: OverrideOptions<State, Action, Effect, Computed, PersistDump>,
) => Partial<
OverrideOptions<State, Action, Effect, Computed, PersistDump>
>),
): Model<Name, State, Action, Effect, Computed> => {
const realModel = model as unknown as InternalModel<
string,
State,
Action,
Effect
Effect,
Computed
>;

const prevOpts = realModel._$opts;
Expand Down
4 changes: 3 additions & 1 deletion src/model/defineModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ export const defineModel = <
Action extends object,
Effect extends object,
Computed extends object,
PersistDump,
>(
uniqueName: Name,
options: DefineModelOptions<State, Action, Effect, Computed>,
options: DefineModelOptions<State, Action, Effect, Computed, PersistDump>,
): Model<Name, State, Action, Effect, Computed> => {
guard(uniqueName);

Expand Down Expand Up @@ -269,6 +270,7 @@ export const defineModel = <
{
name: uniqueName,
_$opts: options,
_$persistCtx: getInitialState({}),
},
getState,
),
Expand Down
Loading

0 comments on commit 689fee5

Please sign in to comment.