GithubHelp home page GithubHelp logo

mmlpx's Introduction

mmlpx

npm version coverage npm downloads Build Status

mmlpx is an abbreviation of mobx model layer paradigm, inspired by CQRS and Android Architecture Components, aims to provide a mobx-based generic layered architecture for single page application.

undefiend

Installation

npm i mmlpx -S

or

yarn add mmlpx

Requirements

  • MobX: ^3.2.1 || ^4.0.0 || ^5.0.0

Boilerplates

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

Features

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 system is the spontaneous choice.

mmlpx DI system was deep inspired by spring ioc.

Typescript Usage

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

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.

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.

@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<T>(Clazz: IMmlpx<T>, mockInstance: T, name?: string) : recover
@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.

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 and 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:

@Store('UserStore')
class UserStore {}

Fortunately mmlpx had provided 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 and the demo source code for details.

Layered Architecture Overview

Store

Business logic and rules definition, equate to the model in mvvm architecture, singleton in an application. Also known as domain object in DDD, always represent the single source of truth of the application.

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.

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.

class UserLoader {
    async getUsers() {
        const users = await this.http.get<User[]>('/users');
        return users.map(user => ({
            name: user.userName,
            age: user.userAge,
        }))
    }
}

Component

export default App extends Component {
    
    @inject()
    vm: AppViewModel;
    
    render() {
        const { loading, userName } = this.vm;
        return (
            <div>
                {loading ? <Loading/> : <p>{userName}</p>} 
            </div>
        );
    }
}

mmlpx's People

Contributors

dependabot[bot] avatar kuitos avatar xiaodongzai avatar yoglib avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

mmlpx's Issues

api文档完善建议

postConstruct,instantiate 这两个api文档中缺少使用说明,建议完善下。

不能用在react-native里

lru-cache 调用了 node 的 utils 模块,然而 react-native 里边无法使用这个模块,改成 v5 估计没问题

mmplx在vue的最佳实践?

不知道大佬请问最近有对mmplx在vue上进行过实践,最近我在写mobx-mp,打算开坑mobx-vue,与我正在写的mobx-mp以mmplx作为核心的小程序以及vue的同构(部分?),不知道大佬是否有什么建议或者意见,谢谢指教

How watch @observable in mobx-vue?

export default class DatasourceViewModel {
 @observable public activePid: string = 'loading';
}
export default class Datasource extends Vue {
  @inject() public store: DatasourceViewModel;
  activePid: string = 'loading';
  $refs: {
    tableStruct: TableStruct,
    tableData: TableData,
  };
 //@Watch('store.activePid')
  @Watch('activePid')
  onActivePidChanged(val: string, oldVal: string) {
    this.$refs.tableStruct.packageId = val;
  }
}

HMR

There is any example to integrate mmlpx with HMR in javascript project?

useStrict与async action的一个问题

遵循CQRS规范,action中如果有返回值则抛出异常。async会返回Promise,就会报错,这种情况怎么处理比较好?
useStrict(true);

      @action.bound
	replaceAll(todos: Todo[]) {
		this.list = todos;
	}

	// async action
	@action
	async loadTodos() {
                this.isPending = true;
		const r = await api.getTodos();
		this.replaceAll(r.data.d);
               // ...
	}

结合 Reacr Hooks

我体验了 mmlpx, 感觉非常赞! 但是这种装饰器依赖注入的方式应该如何结合 hooks 呢? 或者说容器组件还是使用 class, 普通组件使用 function

MobX 6 support

Mobx version 6 just released. And I see this great tool is partially working with Mobx6 and not working with TS 4. Do you have a plan to upgrade mmlpx project accordingly?

关于ViewModel的一点困惑

Page interaction logic definition, live around the component lifecycle, ViewModel instance can not be stored in ioc container.

指的是依赖ViewModel的Compopent unmount后,ViewModel也会被销毁吗?但稍微看了源码(initializers目录下),还是没看明白,是怎么实现的, 它和Store的实现差异在哪里。希望得到大佬的解答。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.