Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

封装 ViewModel 基类; 从时间旅行中分离持久化 #12

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"lint": "tslint 'src/**/*.ts' && npm test"
},
"dependencies": {
"global": "^4.4.0",
"lodash": "^4.17.4",
"lru-cache": "^4.1.5",
"reflect-metadata": "^0.1.12",
Expand All @@ -46,7 +47,7 @@
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"codecov": "^3.0.4",
"husky": "^0.14.1",
"jest": "^23.1.0",
"jest": "^24.9.0",
"mobx": "^5.0.3",
"sinon": "^6.0.1",
"ts-jest": "^22.4.6",
Expand Down
1 change: 0 additions & 1 deletion src/api/__tests__/configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import Store from '../../core/dependency-inject/decorators/Store';
import useStrict from '../configure';

test('store actions should not return anythings when in strict mode', async () => {

const prev = useStrict(true);

@Store
Expand Down
42 changes: 42 additions & 0 deletions src/base/ViewModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { action, observable, runInAction } from 'mobx';
import { applySnapshot, getSnapshot, onSnapshot } from '../api/snapshot';

export default abstract class ViewModelBase {
@observable
private cursor = 0;

@observable
stack: any[] = [];

@action.bound
enableRedoUndo() {
this.stack.push(getSnapshot());

return onSnapshot(snapshot => {
runInAction(() => {
this.stack.push(snapshot);
this.cursor = this.stack.length - 1;
});
});
}

@action.bound
redo() {
const tmp = this.cursor + 1;
if (tmp >= this.stack.length) {
return;
}

applySnapshot(this.stack[++this.cursor]);
}

@action.bound
undo() {
const tmp = this.cursor - 1;
if (tmp < 0) {
return;
}

applySnapshot(this.stack[--this.cursor]);
}
}
73 changes: 73 additions & 0 deletions src/base/__tests__/ViewModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { action, observable } from 'mobx';

import inject from '../../core/dependency-inject/decorators/inject';
import Store from '../../core/dependency-inject/decorators/Store';
import ViewModelBase from '../ViewModel';

test('undo method based ViewModelBase class should back to prev snapshot; redo method should forward to next snapshot', async () => {
@Store('StoreClass')
class StoreClass {
@observable name = 'anthony';

@action update() {
this.name = 'anthony tank';
}

@action
async asyncUpdate(name: string) {
await name;
this.name = name;
}

getName() {
return this.name;
}
}

class ViewModel extends ViewModelBase {
@inject() store: StoreClass;
}

const vm = new ViewModel();
expect(vm.store.name).toBe('anthony');
const disposer = vm.enableRedoUndo();

expect(vm.store.update()).toBeUndefined();

expect(vm.store.name).toBe('anthony tank');

expect(vm.store.getName()).toBe(vm.store.name);

expect(await vm.store.asyncUpdate('async')).toBeUndefined();
expect(vm.store.name).toBe('async');

vm.undo();

expect(vm.store.name).toBe('anthony tank');

vm.undo();
expect(vm.store.name).toBe('anthony');
expect(vm.store.getName()).toBe(vm.store.name);

vm.undo();
vm.undo();
vm.undo();
vm.undo();
vm.undo();
expect(vm.store.name).toBe('anthony');
vm.redo();
expect(vm.store.name).toBe('anthony tank');

vm.redo();
expect(vm.store.name).toBe('async');

vm.redo();
vm.redo();
vm.redo();
vm.redo();
vm.redo();
vm.redo();
expect(vm.store.name).toBe('async');

disposer();
});
13 changes: 4 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
*/

import useStrict from './api/configure';
import ViewModelBase from './base/ViewModel';
import inject from './core/dependency-inject/decorators/inject';
import postConstruct from './core/dependency-inject/decorators/postConstruct';
import Store from './core/dependency-inject/decorators/Store';
import ViewModel from './core/dependency-inject/decorators/ViewModel';
import instantiate from './core/dependency-inject/instantiate';
import { IMmlpx, modelNameSymbol } from './core/dependency-inject/meta';
import configPersist from './persistence/configPersist';

import mock from './utils/mock';

export { onSnapshot, applySnapshot, patchSnapshot, getSnapshot } from './api/snapshot';
Expand All @@ -19,12 +22,4 @@ export function getModelName<T>(model: IMmlpx<T>) {
return model[modelNameSymbol];
}

export {
inject,
ViewModel,
Store,
postConstruct,
instantiate,
mock,
useStrict,
};
export { inject, ViewModel, Store, postConstruct, instantiate, mock, useStrict, ViewModelBase, configPersist };
67 changes: 67 additions & 0 deletions src/persistence/__tests__/configPersist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { action, observable } from 'mobx';

import inject from '../../core/dependency-inject/decorators/inject';
import Store from '../../core/dependency-inject/decorators/Store';

import ViewModelBase from '../../base/ViewModel';
import configPersist from '../configPersist';
import StorageLoader from '../StorageLoader';

const mockStoragePool: { [propName: string]: any } = { 'mmlpx-snapshot': '{"StoreClass": {"name": "tank"}}' };
const mockLocalStorage = {
setItem: (key: string, value: string) => {
mockStoragePool[key] = value;
},
getItem: (key: string) => {
const value = mockStoragePool[key];
return value;
},
};
StorageLoader.shimLoader(mockLocalStorage);

test('persist of configPersist should enable or disable persisting', async () => {
@Store('StoreClass')
class StoreClass {
@observable name = 'anthony';

@action update(name: string) {
this.name = name;
}

@action
async asyncUpdate(name: string) {
await name;
this.name = name;
}
}

class ViewModel extends ViewModelBase {
@inject() store: StoreClass;
}

const vm = new ViewModel();
expect(vm.store.name).toBe('anthony');

configPersist({ persist: true });

expect(vm.store.name).toBe('tank');

vm.store.update('anthony tank');
expect(vm.store.name).toBe('anthony tank');

expect(StorageLoader.getSnapshot()).toEqual({ StoreClass: { name: 'anthony tank' } });

vm.store.update('...');
expect(vm.store.name).toBe('...');
expect(StorageLoader.getSnapshot()).toEqual({ StoreClass: { name: '...' } });

configPersist({ persist: false });

vm.store.update('test');
expect(vm.store.name).toBe('test');
expect(StorageLoader.getSnapshot()).toEqual({ StoreClass: { name: '...' } });

vm.store.update('tack anthony');
expect(vm.store.name).toBe('tack anthony');
expect(StorageLoader.getSnapshot()).toEqual({ StoreClass: { name: '...' } });
});
78 changes: 78 additions & 0 deletions src/persistence/__tests__/persistence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { action, observable } from 'mobx';

import ViewModelBase from '../../base/ViewModel';
import inject from '../../core/dependency-inject/decorators/inject';
import Store from '../../core/dependency-inject/decorators/Store';
import persistence from '../persistence';
import StorageLoader from '../StorageLoader';
const mockStoragePool: { [propName: string]: any } = {};
const mockLocalStorage = {
setItem: (key: string, value: string) => {
mockStoragePool[key] = value;
},
getItem: (key: string) => {
const value = mockStoragePool[key];
return value;
},
};

StorageLoader.shimLoader(mockLocalStorage, 'anthony-storage');

test('persistence should enable snapshot persisting', async () => {
@Store('StoreClass')
class StoreClass {
@observable name = 'anthony';

@action update() {
this.name = 'anthony tank';
}

@action
async asyncUpdate(name: string) {
await name;
this.name = name;
}
}

class ViewModel extends ViewModelBase {
@inject() store: StoreClass;
}

const vm = new ViewModel();
const persistenceDisposer = persistence();

expect(vm.store.name).toBe('anthony');
const disposer = vm.enableRedoUndo();

expect(vm.store.update()).toBeUndefined();
expect(vm.store.name).toBe('anthony tank');
expect(StorageLoader.getSnapshot()).toEqual({ StoreClass: { name: 'anthony tank' } });

expect(await vm.store.asyncUpdate('async')).toBeUndefined();
expect(vm.store.name).toBe('async');
expect(StorageLoader.getSnapshot()).toEqual({ StoreClass: { name: 'async' } });

vm.undo();
expect(vm.store.name).toBe('anthony tank');
expect(StorageLoader.getSnapshot()).toEqual({ StoreClass: { name: 'async' } });

vm.undo();
vm.undo();
vm.undo();
vm.undo();
vm.undo();
expect(vm.store.name).toBe('anthony');
expect(StorageLoader.getSnapshot()).toEqual({ StoreClass: { name: 'async' } });

vm.redo();
vm.redo();
vm.redo();
vm.redo();
vm.redo();
vm.redo();
expect(vm.store.name).toBe('async');
expect(StorageLoader.getSnapshot()).toEqual({ StoreClass: { name: 'async' } });

disposer();
persistenceDisposer();
});
29 changes: 29 additions & 0 deletions src/persistence/__tests__/storageLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import StorageLoader from '../StorageLoader';

test('shimLoader method of StorageLoader class should implement getSnapshot, saveSnapshot methods and custom snapshotKey', async () => {
const mockStoragePool: { [propName: string]: any } = {};
const mockLocalStorage = {
setItem: (key: string, value: string) => {
mockStoragePool[key] = value;
},
getItem: (key: string) => {
const value = mockStoragePool[key];
return value;
},
};

StorageLoader.shimLoader(mockLocalStorage, 'anthony');

expect(StorageLoader.snapshotKey).toBe('anthony');

expect(StorageLoader.getSnapshot()).toBeUndefined();
expect(StorageLoader.saveSnapshot({ TodosStore22_1: { total22: -88, xtotal22: 0 } })).toBeUndefined();
expect(StorageLoader.getSnapshot()).toEqual({ TodosStore22_1: { total22: -88, xtotal22: 0 } });

expect(StorageLoader.saveSnapshot({ TodosStore: { total22: 8, xtotal22: 0 } })).toBeUndefined();
expect(StorageLoader.getSnapshot()).toEqual({ TodosStore: { total22: 8, xtotal22: 0 } });

StorageLoader.shimLoader(mockLocalStorage, 'anthony-storage');
expect(StorageLoader.snapshotKey).toBe('anthony-storage');
expect(StorageLoader.getSnapshot()).toBeUndefined();
});
27 changes: 27 additions & 0 deletions src/persistence/configPersist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { IReactionDisposer } from 'mobx';
import { applySnapshot } from '../api/snapshot';
import persistence from './persistence';
import StorageLoader from './StorageLoader';

let persistenceDisposer: IReactionDisposer;

interface IPtions {
persist?: boolean;
}

// it should be excuted after injector container initializing, for instance, involved in async task queue
export default function configPersist(options: IPtions = {}) {
const { persist = false } = options;

if (persist) {
const res = StorageLoader.getSnapshot();
if (res) {
applySnapshot(res);
}
persistenceDisposer = persistence();
} else {
if (persistenceDisposer) {
persistenceDisposer();
}
}
}
9 changes: 9 additions & 0 deletions src/persistence/persistence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IReactionDisposer } from 'mobx';
import { onSnapshot } from '../api/snapshot';
import StorageLoader from './StorageLoader';

export default function persistence(): IReactionDisposer {
return onSnapshot(snapshot => {
StorageLoader.saveSnapshot(snapshot);
});
}
Loading