Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/mmlpxjs/mmlpx
π mobx model layer paradigm
https://github.com/mmlpxjs/mmlpx
architecture layers mobx mvvm paradigm snapshot time-travel
Last synced: 2 months ago
JSON representation
π mobx model layer paradigm
- Host: GitHub
- URL: https://github.com/mmlpxjs/mmlpx
- Owner: mmlpxjs
- License: mit
- Created: 2017-08-04T04:07:55.000Z (about 7 years ago)
- Default Branch: master
- Last Pushed: 2022-12-07T17:33:35.000Z (almost 2 years ago)
- Last Synced: 2024-04-07T00:53:32.274Z (6 months ago)
- Topics: architecture, layers, mobx, mvvm, paradigm, snapshot, time-travel
- Language: TypeScript
- Homepage:
- Size: 1.11 MB
- Stars: 180
- Watchers: 6
- Forks: 10
- Open Issues: 20
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# mmlpx
[![npm version](https://img.shields.io/npm/v/mmlpx.svg?style=flat-square)](https://www.npmjs.com/package/mmlpx)
[![coverage](https://img.shields.io/codecov/c/github/mmlpxjs/mmlpx.svg?style=flat-square)](https://codecov.io/gh/mmlpxjs/mmlpx)
[![npm downloads](https://img.shields.io/npm/dt/mmlpx.svg?style=flat-square)](https://www.npmjs.com/package/mmlpx)
[![Build Status](https://img.shields.io/travis/mmlpxjs/mmlpx.svg?style=flat-square)](https://travis-ci.org/mmlpxjs/mmlpx)mmlpx is an abbreviation of **mobx model layer paradigm**, inspired by [CQRS](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs) and [Android Architecture Components](https://developer.android.google.cn/topic/libraries/architecture/), aims to provide a mobx-based generic layered architecture for single page application.
> ![undefiend](https://cdn.nlark.com/yuque/0/2019/png/200577/1566621752079-3713029f-0357-4c4d-a3e2-4eea7080312e.png)
## Installation```bash
npm i mmlpx -S
```or
```bash
yarn add mmlpx
```## Requirements
* MobX: ^3.2.1 || ^4.0.0 || ^5.0.0
## Boilerplates
- [mmlpx-todomvc](https://github.com/mmlpxjs/mmlpx-todomvc)
## Motivation
Try to explore the possibilities for building a view-framework-free data layer based on mobx, summarize the generic model layer paradigm, and provide the relevant useful toolkits to make it easier and more intuitive.
## Articles
* [Turn On Time-Travelling Engine For MobX](https://itnext.io/turn-on-time-travelling-for-mobx-c3f267a46f10) ([δΈζ](https://zhuanlan.zhihu.com/p/39354507))
## Features
```ts
import { inject, onSnapshot, getSnapshot, applySnapshot } from 'mmlpx'
import Store from './Store'@observer
class App extends Component {
@inject() store: Store
stack: any[]
cursor = 0
disposer: IReactionDisposer
componentDidMount() {
this.stack.push(getSnapshot());
this.disposer = onSnapshot(snapshot => {
this.stack.push(snapshot)
this.cursor = this.stack.length - 1
this.store.saveSnapshot(snapshot)
})
}
componentWillUmount() {
this.disposer();
}
redo() {
applySnapshot(this.stack[++this.cursor])
}
undo() {
applySnapshot(this.stack[--this.cursor])
}
}
```### DI System
It is well known that MobX is an value-based reactive system which lean to oop paradigm, and we defined our states with a class facade usually. To avoid constructing the instance everytime we used and to enjoy the other benifit (unit test and so on), a [di](https://en.wikipedia.org/wiki/Dependency_injection) system is the spontaneous choice.
mmlpx DI system was deep inspired by [spring ioc](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans).
#### Typescript Usage
```ts
import { inject, ViewModel, Store } from 'mmlpx';@Store
class UserStore {}@ViewModel
class AppViewModel {
@inject() userStore: UserStore;
}
```Due to we leverage the metadata description ability of typescript, you need to make sure that you had configured `emitDecoratorMetadata: true` in your `tsconfig.json`.
#### Javascript Usage
```js
import { inject, ViewModel, Store } from 'mmlpx';@Store
class UserStore {}@ViewModel
class AppViewModel {
@inject(UserStore) userStore;
}
```#### More Advanced
##### inject
Sometimes you may need to intialize your dependencies dynamically, such as the constructor parameters came from router query string. Fortunately `mmlpx` supported the ability via `inject`.
```js
import { inject, ViewModel } from 'mmlpx'@ViewModel
class ViewModel {
@observable.ref
user = {};
constructor(projectId, userId) {
this.projectId = projectId;
this.userId = userId;
}
loadUser() {
this.user = this.http.get(`/projects/${projectId}/users/${userId}`);
}
}class App extends Component {
@inject(ViewModel, app => [app.props.params.projectId, app.props.params.userId])
viewModel;
componentDidMount() {
this.viewModel.loadUser();
}
}
````inject` decorator support four recipes initilizaztion:
* `inject() viewModel: ViewModel;` only for typescript.
* `inject(ViewModel) viewModel;` generic usage.
* `inject(ViewModel, 10, 'kuitos') viewModel;` initialized with static parameters for `ViewModel` constrcutor.
* `inject(ViewModel, instance => instance.router.props) viewModel;` initialized with dynamic instance props for `ViewModel` constructor.**Notice that all the `Store` decorated classes are singleton by default so that the dynamic initial params injection would be ignored by di system**, if you wanna make your state live around the component lifecycle, always decorated them with `ViewModel` decorator.
##### instantiate
While you are limited to use `decorator` in some scenario, you could use `instantiate` to instead of `@inject`.
```ts
@ViewModel
class UserViewModel {
constructor(name, age) {
this.name = name;
this.age = age;
}
}const userVM = instantiate(UserViewModel, 'kuitos', 18);
```#### Test Support
`mmlpx` di system also provided the mock method to support unit test.
* `function mock(Clazz: IMmlpx, mockInstance: T, name?: string) : recover`
```js
@Store
class InjectedStore {
name = 'kuitos';
}class ViewModel {
@inject() store: InjectedStore;
}// mock the InjectedStore
const recover = mock(InjectedStore, { name: 'mock'});const vm = new ViewModel();
expect(vm.store.name).toBe('mock');
// recover the di system
recover();const vm2 = new ViewModel();
expect(vm2.store.name).toBe('kuitos');
```### Strict Mode
If you wanna strictly follow the CQRS paradigm to make your state changes more predictable, you could enable the strict mode by invoking `useStrcit(true)`, then your actions in `Store` or `ViewModel` will throw an exception while you declaring a return statement.
```ts
import { useStrict } from 'mmlpx';
useStrict(true);@Store
class UserStore {
@observable name = 'kuitos';
@action updateName(newName: string) {
this.name = newName;
// return statement will throw a exceptin when strict mode enabled
return this.name;
}
}
```### Time Travelling
Benefit from the power of model management by di system, mmlpx supported time travelling out of box.
All you need are the three apis: `getSnapshot`, `applySnapshot` and `onSnapshot`.
* `function getSnapshot(injector?: Injector): Snapshot;`
`function getSnapshot(modelName: string, injector?: Injector): Snapshot;`
* `function applySnapshot(snapshot: Snapshot, injector?: Injector): void;`
* `function onSnapshot(onChange: (snapshot: Snapshot) => void, injector?: Injector): IReactionDisposer;`
`function onSnapshot(modelName: string, onChange: (snapshot: Snapshot) => void, injector?: Injector): IReactionDisposer;`That's to say, mmlpx makes mobx do [HMR](https://github.com/mmlpxjs/mmlpx/blob/master/src/api/makeHot.ts) and [SSR](https://ssr.vuejs.org/#what-is-server-side-rendering-ssr) possible as well!
As we need to serialize the stores to persistent object, and active stores with deserialized json, we should give a name to our `Store`:
```ts
@Store('UserStore')
class UserStore {}
```Fortunately mmlpx had provided [ts-plugin-mmlpx](https://github.com/mmlpxjs/ts-plugin-mmlpx) to generate store name automatically, you don't need to name your stores manually.
You can check the [mmlpx-todomvc redo/undo demo](https://mmlpxjs.github.io/mmlpx-todomvc) and the [demo source code](https://github.com/mmlpxjs/mmlpx-todomvc/blob/master/src/containers/TodoApp/ViewModel.ts#L124) for details.
## Layered Architecture Overview
### Store
Business logic and rules definition, equate to the model in [mvvm architecture](https://msdn.microsoft.com/en-us/library/hh848246.aspx), singleton in an application. Also known as domain object in [DDD](https://en.wikipedia.org/wiki/Domain-driven_design), always represent the single source of truth of the application.
```ts
import { observable, action, observe } from 'mobx';
import { Store, inject } from 'mmlpx';
import UserLoader from './UserLoader';@Store
class UserStore {
@inject() loader: UserLoader;
@observable users: User[];
@action
async loadUsers() {
const users = await this.loader.getUsers();
this.users = users;
}
@postConstruct
onInit() {
observe(this, 'users', () => {})
}
}
```*Method decorated by `postConstruct` will be invoked when `Store` initialized by DI system.*
### ViewModel
Page interaction logic definition, live around the component lifecycle, `ViewModel` instance can not be stored in ioc container.
The only direct consumer of `Store`, besides the UI-domain/local states, others are derived from `Store` via `@computed` in `ViewModel`.
The global states mutation are resulted by store **command** invocation in `ViewModel`, and the separated **queries** are represented by transparent subscriptions with `computed` decorator.
```ts
import { observable, action } from 'mobx';
import { postConstruct, ViewModel, inject } from 'mmlpx';@ViewModel
class AppViewModel {
@inject() userStore: UserStore;
@observable loading = true;
@computed
get userNames() {
return this.userStore.users.map(user => user.name);
}
@action
setLoading(loading: boolean) {
this.loading = loading;
}
}
```### Loader
Data accessor for remote or local data fetching, converting the data structure to match definited models.
```ts
class UserLoader {
async getUsers() {
const users = await this.http.get('/users');
return users.map(user => ({
name: user.userName,
age: user.userAge,
}))
}
}
```### Component
```jsx
export default App extends Component {
@inject()
vm: AppViewModel;
render() {
const { loading, userName } = this.vm;
return (
{loading ? :{userName}
}
);
}
}
```