zhifan

Redux vs MobX: 哪一个最合适你的项目? — SitePoint

原文链接: www.sitepoint.com

Pieces of data shaped like birds, migrating Redux to MobX

对于很多 JavaScript 开发者,对 redux 最大的抱怨来主要在于当使用 redux 来实现需求的时候需要写大量的模板代码。一个更好的选择是 mobx,mobx 提供了类似的功能但是只需要写更少量的代码。

关于作者

对于 Mobx 新手,你可以快速浏览下这篇由 Mobx 的创造者写的介绍。你同样可以通过这个 教程 来获得一些实践经验。

本篇文章的目标是帮助 JavaScript 开发者来决定这两种状态管理方案哪一种最适合自己的项目。我已经迁移了这个 CRUD Redux 项目MobX 版本来作为这篇文章的例子。我将会讨论下使用 Mobx 的优势与缺点,同时我会用实际的代码片段来展示这个两个版本的不同之处。

本文提到的项目的代码可以在 GitHub 上找到:

Redux 和 MobX 有哪些共同点?

首先,我们先看看他们都有的共同点:

  • 都是开源的

  • 提供客户端状态管理

  • 支持时间旅行调试工具 redux-devtools-extension

  • 不依赖于某个特定框架

  • 对 React/React Native 有着良好的支持

使用 Mobx 的四个理由

现在我们来看看 Redux 与 Mobx 间的主要不同点。

1. 易于学习和使用

对于一个初学者,学 30 分钟就能够学会怎么使用 Mobx。一旦你学习了 Mobx 的基本概念,仅此而已,那就够了。你不需要在学别的新东西。相比起来,Redux 的基本概念同样简单,然而,当你开始构建更加复杂的应用,你就不得不面对:

  • 使用 redux-thunk 处理异步 action

  • 使用 redux-saga 简化代码

  • 定义 selectors 来处理计算值等.

使用 MobX, 所有的这些情况都被魔法般的考虑到。你不需要额外的库去处理这些情况。

2. 写更少的代码

在 Redux 里要实现一个特性,你需要更改至少 4 个部件。这包括 reducers,actions,容器组件和组件的代码。如果你在一个小型项目上使用你会特别烦恼。Mobx 只要求你更改最少 2 个部件(比如:store 和 视图组件)。

3. 完整支持面向对象编程

如果你更趋向于写面向对象的代码,你会很高兴的知道在 Mobx 里你可以使用 OOP 来实现应用逻辑的状态管理。通过对形如 @observable@observer 的装饰器的使用,你可以很简单的使你的普通的 JavaScript 组件和 stores 变得响应式的。如果你更喜欢函数式编程,没问题,那也很好的被支持。而 Redux 则处于另一面,很大程度上是朝向着函数式编程的原则。但是,如果你想要一个基于 class 类的形式,你也可以使用 redux-connect-decorator

4. 易于处理嵌套数据

在大多数的 JavaScript 应用中,你会发现你与关联的数据或者嵌套的数据一起打交道。为了能够在 Redux store里使用,你不得不先对数据进行 normalize . 然后, 你不得不 多写一些代码 来管理被 normalize 后的数据中的关联的引用。 在 Mobx 中,在 store 里存储未 normalize 的数据是受 推荐 的。Mobx 可以保持数据的引用,这样你可以自动的重新渲染变化。通过使用领域对象来存储数据,你可以在另外的 store 里直接的引用不同的领域数据。此外你可以使用 (@)computed decoratorsobservables 的修饰器 来十分方便的解决复杂的数据挑战。

不使用 Mobx 的三个理由

1. 过于自由

Redux 是一个对如何写状态代码提供了严格的指导方针的框架。这意味着你可以轻易的写测试和开发可维护的代码。Mobx 则是对怎么写代码没有任何的规则和限制。所以用 Mobx 的风险在于你很容易就走捷径和应用快速修复的代码,这些会导致不可维护的代码。

2. 难于调试

Mbox 通过内部的“魔法”方法来处理大量的逻辑,使得你的应用是响应式的。当你在 store 和组件之间传递数据,这里有一些区域是你看不到的,当你的应用出现 bug 的时候你就很难调试。如果你没有使用 @actions 装饰器就在组件内直接改变 state, 你将很难精确的定位到一个错误的来源。

3. 可能还有比 Mobx 更好的选择

在软件开发中,新趋势会一直发生。在短短的几年内,当前的软件技术可能会很快的失去力量。目前,还有几种解决方案与 Redux 和 Mobx 相竞争。一些例子: Relay/Apollo & GraphQL, Alt.jsJumpsuit. 所有的这些技术都有潜力变成最流行的方案,如果你真的想要知道那个方案最适合你,你不得不尝试所有这些方案。

代码比较: Redux vs MobX

理论已经说的够多了,让我们来看看代码。首先,我们先比较每个版本的代码是怎么做初始引导的。

初始引导

Redux 版本:

在 Redux 里,我们首先要定义 store,然后通过 Provider 把 store 传入 App 内。同时,我们需要定义 redux-thunk he redux-promise-middleware 来处理异步函数。redux-devtools-extension 允许我们能够进行时间旅行模式来调试 store。

// src/store.js
import { applyMiddleware, createStore } from "redux";
import thunk from "redux-thunk";
import promise from "redux-promise-middleware";
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from "./reducers";

const middleware = composeWithDevTools(applyMiddleware(promise(), thunk));

export default createStore(rootReducer, middleware);

-------------------------------------------------------------------------------

// src/index.js
....
ReactDOM.render(
  <BrowserRouter>
    <Provider store={store}>
      <App />
    </Provider>
  </BrowserRouter>,
  document.getElementById('root')
);

MobX 版本:

在 Mobx 里,,我们需要建立多个 store。在这种情况下,我只使用一个 store,这个 store 被我放在一个名叫 allStores 的集合。然后由 Provider 来传递这些 stores 给 App. 正如之前提到的,Mobx 不需要外部库来处理异步操作,所以只要很少的代码。但是,我们需要 mobx-remotedev 来连接到 redux-devtools-extension 调试工具。

// src/stores/index.js
import remotedev from 'mobx-remotedev';
import Store from './store';

const contactConfig = {
  name:'Contact Store',
  global: true,
  onlyActions:true,
  filters: {
    whitelist: /fetch|update|create|Event|entity|entities|handleErrors/
  }
};

const contactStore = new Store('api/contacts');

const allStores = {
  contactStore: remotedev(contactStore, contactConfig)
};

export default allStores;

-------------------------------------------------------------------------------

// src/index.js
...
ReactDOM.render(
  <BrowserRouter>
    <Provider stores={allStores}>
      <App />
    </Provider>
  </BrowserRouter>,
  document.getElementById('root')
);

在这里二者的代码量大致相等,Mobx 比 Redux 少一些 import 语句。

属性注入

Redux 版本:

在 Redux 中,state 和 actions 通过 react-redux 的 connect() 函数传给 props In Redux, state and actions are passed to props using react-redux’s connect() function.

// src/pages/contact-form-page.js
...
  // accessing props
  <ContactForm
    contact={this.props.contact}
    loading={this.props.loading}
    onSubmit={this.submit}
  />
...

// function for injecting state into props
function mapStateToProps(state) {
  return {
    contact: state.contactStore.contact,
    errors: state.contactStore.errors
  }
}

// injecting both state and actions into props
export default connect(mapStateToProps, { newContact,
  saveContact,
  fetchContact,
  updateContact
})(ContactFormPage);

MobX 版本:

在 Mobx,我们只需要注入 stores 集合。我们在容器或则组件类的上面使用 @inject 来做属性注入。这让 stores 可以在 props 上可以访问到。同时,反过来,我们可以访问一个特定的 store 并且将它传给子组件。state 和 actions 可以通过 在 props 上的 store 访问到,所以不需要像 Redux 要单独去传入到组件。

// src/pages/contact-form-page.js

...
@inject("stores") @observer // injecting store into props
class ContactFormPage extends Component {
...
  // accessing store via props
  const { contactStore:store } = this.props.stores;
  return (
      <ContactForm
        store={store}
        form={this.form}
        contact={store.entity}
      />
  )
...
}

MobX 版本看起来比 Redux 版本更加容易阅读,然而,我们可以使用redux-connect-decorators 来简化 Redux 代码。 在这个情况下并没有明显的胜者。

定义 stores, actions, 和 reducers

为了使文章精简,我会只展示一个操作的示例代码。

Redux 版本:

在 Redux 里, 我们需要定义 actions 和 reducers.

// src/actions/contact-actions.js
...
export function fetchContacts(){
  return dispatch => {
    dispatch({
      type: 'FETCH_CONTACTS',
      payload: client.get(url)
    })
  }
}
...

// src/reducers/contact-reducer
...
switch (action.type) {
    case 'FETCH_CONTACTS_FULFILLED': {
      return {
        ...state,
        contacts: action.payload.data.data || action.payload.data,
        loading: false,
        errors: {}
      }
    }

    case 'FETCH_CONTACTS_PENDING': {
      return {
        ...state,
        loading: true,
        errors: {}
      }
    }

    case 'FETCH_CONTACTS_REJECTED': {
      return {
        ...state,
        loading: false,
        errors: { global: action.payload.message }
      }
    }
}
...

MobX 版本:

在 MobX 里, action 和 reducer 的逻辑在一个类里被实现. 我们定义一个异步操作,当 response 收到后,调用另外一个 实体已获取 的 action。 因为 Mobx 使用的是 OPP 风格,Store 类在这里被定义允许方便地创建多个 store 使用类的构造函数。因此这里展示的是不依赖于其他存储域的基本代码。

// src/stores/store.js
...
@action
fetchAll = async() => {
  this.loading = true;
  this.errors = {};
  try {
    const response = await this.service.find({})
    runInAction('entities fetched', () => {
      this.entities = response.data;
      this.loading = false;
    });
  } catch(err) {
      this.handleErrors(err);
  }
}
...

信或者不信,这两个版本的代码逻辑可以同样的任务。分别是

  • 更新 UI 加载状态

  • 获取异步数据

  • 捕获异常和更新状态

在 Redux 里,我们用 33 行代码。 在 MobX 里, 我们大约用 14 行代码 来达到相同的效果! 用 Mobx 的一个主要的好处是你几乎可以在所有的 store 类内稍加改动或者都不用修改就能重用这些基本的代码。这意味着你可以更快的构建的你的应用。

其他不同

在 Redux 里创建表单,我们有 redux-form. 在 MobX 里, 我们有 mobx-react-form. 二者都很成熟可以帮组你更加简单的处理逻. 个人来说,我更喜欢 mobx-react-form 因为它允许你通过插件来验证输入。 而 redux-form, 你要么自己编写验证代码或者你可以引入一个验证库来处理验证。

Mobx 一个微小的缺点是,在观察模式下你不能直接访问一个确定的函数,因为他们已经不是真正的 JavaScript 对象,幸运的是,我们提供了 toJS() 函数,你可以使用它来把观察对象转换成 JavaScript 对象。

总结

很明显,你可以看到使用 Mobx 的代码十分精简的。使用 OOP 风格和良好的开发实践,你可以快熟的构建应用。主要的缺点在于它很容易编写出质量低,难以维护的代码。

另一方面,Redux, 更受欢迎, 非常适合构建大型复杂的项目。它是个严格的框架,带有保障措施保障每个开发者写出的代码易于测试和维护。然而,它并不适合一个小的项目。

尽管 Mobx 的缺点, 如果你遵守良好的开发实践,你任然可以用它来构建大型应用。用爱因斯坦的话来说, “让一切尽可能的简单,而不是仅仅是简单”。

我希望我已经提供了足够的信息让你可以明确的知道是否应该迁移到 Mobx 或者继续使用 Redux。最终的决定取决于你工作的项目的类型和你能够得到的资源。

如果你需要任何对 Mobx 示例代码的澄清,随时在下面的评论里提问,不知道我有没有给你再下一个 React 项目中尝试 Mobx 的信心。

这篇文章由同行 Dominic MyersVildan Softic 二人做评审。感谢所有在 SitePoint 上评审的同行,使 SitePoint 能做出最好的内容。