使用mobx实现react的MVVM框架mobx-roof,比redux简单太多

#1

Mobx-Roof

Mobx-Roof是基于mobx的简单React MVVM框架, 目标是通过更ORM化的思维来管理数据, 如通过继承, 重载 等面向对象方式来实现数据模型的扩展, 并通过Relation来实现数据间的关联.

下边完整的例子可以在项目example目录中找到

基础篇

先看下要实现的效果

image

1.创建模型

我们先通过createModel创建一个用户登录数据模型:

  • name: 定义类名, 类名首字母大写
  • data: 可以通过对象声明或者函数声明, 函数返回的数据会被转换成mobx的observable data, 函数的第一个参数可以当成Model实例化的初始数据
  • actions: 定义模型的方法, 可以使用async/await处理异步方法, 方法返回值会转换成Promise, 其中对象提供了set方法可以快速修改多个数据, 而toJS 方法可以将数据转换成JSON格式
  • autorun: 可以在所依赖数据变动时候自动运行定义的函数, 下边例子当User数据发生变化时候会自动保存到localStorage
import { createModel } from 'mobx-roof';
import * as api from '../api';
const STORE_KEY = 'mobx-roof';

export default createModel({
  name: 'User',
  data(initData) {
    // 从localStorage初始化数据
    let data = localStorage.getItem(STORE_KEY);
    data = data ? JSON.parse(data) : {};
    return {
      isLogin: false,
      userId: null,
      loginError: '',
      // ...
      ...data,
    };
  },
  actions: {
    async login(username, password) {
      const res = await api.login(username, password);
      if (res.success) {
        // 使用set只会触发一次数据变动事件
        this.set({
          userId: res.id,
          isLogin: true,
          // ...
        });
      } else {
        // 直接赋值会触发一次数据变动事件
        this.loginError = res.message;
      }
    },
  },
  autorun: {
    saveToLocalStorage() {
      localStorage.setItem(STORE_KEY, JSON.stringify(this.toJS()));
    },
  },
});

2.绑定到react组件

通过@context创建一个隔离的数据空间, 并在创建的时候实例化所有声明的Model.

import React, { Component, PropTypes } from 'react';
import { context } from 'mobx-roof';
import UserModel from './models/User';

@context({ user: UserModel })
export default class App extends Component {
  static propTypes = {
    user: PropTypes.instanceOf(UserModel).isRequired,
  }
  login() {
    this.props.user.login(this.refs.username.value, this.refs.password.value);
  }
  render() {
    const { user } = this.props;
    if (!user.isLogin) {
      return (
        <div className="container">
          <div>
            username:  <input ref="username" type="text" placeholder="Jack"/>
            password:  <input ref="password" type="password" placeholder="123"/>
            <button onClick={::this.login}>login</button>
            <span style={{color: 'red'}}>{user.loginError}</span>
          </div>
        </div>
      );
    }
    return (
      <div className="container">
        Welcome! {user.username}
      </div>
    );
  }
}

3.获取action执行状态

通过getActionState可以获取任意的action执行状态, 状态里有loadingerror两个字段, 当action开始执行时候状态loading为true, 而如果执行失败错误信息会存入error中.

@context({ user: UserModel })
export default class App extends Component {
  static propTypes = {
    user: PropTypes.instanceOf(UserModel).isRequired,
  }
  login() {
    this.props.user.login(this.refs.username.value, this.refs.password.value);
  }
  render() {
    const { user } = this.props;
    const { loading: loginLoading, error: loginError } = user.getActionState('login');
    if (!user.isLogin) {
      return (
        <div className="container">
          <div>
            username:<input ref="username" type="text" placeholder="Jack"/>
            password:<input ref="password" type="password" placeholder="123"/>
            <button onClick={::this.login}>login</button>
            {loginLoading
              ? <span>loading...</span>
              : <span style={{ color: 'red' }}>{(loginError && loginError.message) || user.loginError}</span>
            }
          </div>
        </div>
      );
    }
    return (
      <div className="container">
        Welcome! {user.username}
      </div>
    );
  }
}

4.拆分react组件, 实现组件间数据共享

下边例子从App组件拆分出了UserLoginUserDetail组件, 并通过@observer 来订阅父节点context中的数据, @observer可以通过字符串, 数组字符串Model类声明, 字符串会从父context查找数据, 而类声明会做数据强类型校验

import React, { Component, PropTypes } from 'react';
import { context, observer } from 'mobx-roof';
import UserModel from './models/User';

@observer('user')
class UserLogin extends Component {
  static propTypes = {
    user: PropTypes.object.isRequired,
  }
  login() {
    this.props.user.login(this.refs.username.value, this.refs.password.value);
  }
  render() {
    const { user } = this.props;
    const { loading: loginLoading, error: loginError } = user.getActionState('login');
    return (
      <div className="container">
        <div>
          username:<input ref="username" type="text" placeholder="Jack"/>
          password:<input ref="password" type="password" placeholder="123"/>
          <button onClick={::this.login}>login</button>
          {loginLoading
            ? <span>loading...</span>
            : <span style={{ color: 'red' }}>{(loginError && loginError.message) || user.loginError}</span>
          }
        </div>
      </div>
    );
  }
}

// 这里如果user字段从context获取的类型不是UserModel则会报错
@observer({ user: UserModel })
class UserDetail extends Component {
  static propTypes = {
    user: PropTypes.object.isRequired,
  }
  logout() {
    this.props.user.logout();
  }
  render() {
    return (
      <div className="container">
        Welcome! {this.props.user.username}
        <button onClick={::this.logout}>logout</button>
      </div>
    );
  }
}

@context({ user: UserModel })
export default class App extends Component {
  static propTypes = {
    user: PropTypes.instanceOf(UserModel).isRequired,
  }
  render() {
    const { user } = this.props;
    if (!user.isLogin) {
      return <UserLogin />;
    }
    return <UserDetail />;
  }
}

高级篇

Model的扩展

  • 1.model的继承及重载, 继承后的新Model会拥有父类所有的data, actionsautorun 方法
import { extendModel } from 'mobx-roof';
import User from './User'
export default extendModel(User, {
  name: 'ChineseUser',
  data: {
    chinese: {
      zodiac: 'dragon',
    },
  },
  actions: {
    async fetchUserInfo() {
      // 重载了 user.fetchUserInfo 方法
      await User.actions.fetchUserInfo.apply(this, arguments);
    },
  },
});

  • 2.model的嵌套使用

下边例子Todos嵌套了TodoItem, 嵌套的Model通过toJS方法会自动遍历所有Model类数据并做转换

import * as api from '../api';

const TodoItem = createModel({
  name: 'TodoItem',
  data({ text, userId, completed, id }) {
    return {
      text,
      userId,
      completed,
      id,
    };
  }
});

export default createModel({
  name: 'Todos',
  data() {
    return {
      list: [],
    };
  },
  actions: {
    add(text, userId) {
      // Add Other model
      this.list.push(new TodoItem({ text, userId }));
    },
  },
});

Relation

当多个model之间需要互动时候, mobx-roof提供了Relation方式, 下边初始化了一个Relation, 其中 relation.init 方法会在第一次创建context之后执行, Relation可以被使用在多个context中且不互相影响

import { Relation } from 'mobx-roof';
const relation = new Relation;

relation.init((context) => {
  const { user, todos } = context;
  console.log(user); // userModel instance
  console.log(todos); // todoModel instance
});
export default relation;

relation加到context中;

import { context } from 'mobx-roof';
import middleware from './middlewares';
import relation from './relations';

@context({ user: UserModel, todos: TodosModel }, { middleware, relation })
export default class App extends Component {
  //...
}

relation提供了多种数据监听方式, 如下, 其中回调结果中payload 为action返回结果, action 为action对应的名字, context为对应作用域

  • 1.监听一个
relation.listen('user.login', ({ context, payload, action }) => {
  console.log('[relation] user.login: ', payload, context);
});
  • 2.多个匹配

可以使用正则匹配, 或者用;分开列举

relation.listen(/^user/, ({ action }) => {
  console.log('[relation] user action name: ', action);
});

relation.listen('user.login; user.fetchUserInfo', ({ action }) => {
  // ...
});

  • 3.多行方式

-> 表示串联执行, => 会将前一个action结果数据传递到后一个action当成参数

relation.listen(`
  # 注释
  user.login -> user.fetchUserInfo;
  user.login => todos.getByUserId
`);
  • 4.过滤器
const relation = new Relation({
  filters: {
    filter1(payload) {
      return payload;
    },
    filter2(payload) {
      return payload;
    },
  },
});
relation.listen(`
  ## comment
  user.login -> user.fetchUserInfo;
  user.login | filter1 => filter2 | todos.getByUserId
`);

  • 5.relation.autorun

relation 还提供全局的autorun方法, 可以添加多个, 用于处理多个model的复杂关系逻辑

relation.autorun((context) => {
  console.log('[autorun] ', context.user.toJS());
  console.log('[autorun] ', context.todos.toJS());
});

中间件的使用

下边是一个简单的日志打印中间件, before after error 分别对应action执行前, 执行后及执行错误, filter 可以对action进行过滤, fitler可以是字符串, 正则表达式 或者 函数;

// Before exec action
function preLogger({ type, payload }) {
  console.log(`${type} params: `, payload.join(', '));
  return payload;
}

// Action exec fail
function errorLogger({ type, payload }) {
  console.log(`${type} error: `, payload.message);
  // 这里如果返回null将阶段后续的错误
  return payload;
}

// After exec action
function afterLogger({ type, payload }) {
  console.log(`${type} result: `, payload);
  return payload;
}

export default {
  filter({ type }) {
    return /^User/.test(type);
  },
  before: preLogger,
  after: afterLogger,
  error: errorLogger,
};

添加到@context中:

import { Middleware, context } from 'mobx-roof';
import logger from './logger';
const middleware = new Middleware;
middleware.use(
  logger,
);
@context({ user: UserModel }, { middleware })
export default class App extends Component {
  //...
}
1 Like
#2

如果我用 mobx-react 那 怎么用 react-router store怎么传下去

1 Like
#3

用MVVM的框架和redux比较好像不太适合,因为redux的开发理念跟MVVM还是差别挺大的,主要是redux是单向数据流,而MVVM是通过ViewModel这一层实现双向绑定。之所以会有单向数据流的架构出来,就是因为双向绑定会让数据的变更不可预测。所以这不是代码简单不简单的问题,redux实现了单向数据流,保证了数据变更的可控和可预测,肯定会在代码结构上有所开销的。

#4

这个库是 proxy 版本的 mobx:https://github.com/ascoders/dynamic-object

#5

redux也不难吧

#6

这个 mobx-roof 能在 ReactNative 项目上使用吗?github上都有一年多没更新了,是不是停止更新了。为什么我在 ReactNative 项目里导入 就报错了。