文档结构  
翻译进度:已翻译     翻译赏金:5 元 (?)    ¥ 我要打赏

对于很多 JavaScript 开发人员来说,对 Redux 最大的抱怨无非就是需要大量实现功能的样板代码。另外一个更好的替代品 MobX 提供了类似的功能,但是代码量要更少一些。

对 MobX 新手来说,可以先快速看看 MobX 创始人的 介绍文档。你也可以通过这个 教程 来获取一些实践经验。

本文的目的是帮助 JavaScript 开发者决定这两个状态管理解决方案,到底哪个更适合自己的项目。我已经移植了这个 CRUD Redux project 到 MobX 之上来作为本文的例子。我们将首先讨论使用 MobX 的利弊,然后再演示相同示例的两个不同版本来展示二者的差异。

第 1 段(可获 1.6 积分)

本文提及的示例代码可从 Github 中获取:

Redux 和 MobX 有什么共同之处?

首先来看看两个框架的共同点,包括:

  • 都是开源库
  • 提供客户端状态管理
  • 支持通过 redux-devtools-extension 实现穿越调试
  • 不依赖于特定的框架
  • 为 React/React Native 框架提供扩展支持

使用 MobX 的 4 个理由

首先来看看 Redux 和 MobX 的主要不同的地方。

1. 易学易用

对一个初学者来说,你可以在 30 分钟内就能学好如何使用 MobX。一旦你掌握了基础,就不需要学习其他新的。Redux 的基础同样很容易学会。不过,一旦你开始构建更复杂的应用时,你必须处理这些问题:

第 2 段(可获 1.76 积分)
  • 使用 redux-thunk 处理异步 actions
  • 使用 redux-saga 来简化代码
  • 定义选择器来处理计算值

而在 MobX 中,所有的这些情况都被神奇的照顾到,你必须要额外的库来处理这种情况。

2. 编写更少的代码

为了在 Redux 实现一个功能,你需要更新至少 4 个组件,包含为 reduces、actions、containers 和 components 编写代码。如果你开发的是一个小项目,这会让人很恼火。MobX 只要求你更新 2 个组件(存储和视图组件)。

第 3 段(可获 1.2 积分)

3. 支持面向对象编程

如果你更喜欢编写面向对象代码,那么你对能用面向对象实现 MobX 的状态管理逻辑会感到很高兴。通过使用诸如 @observable 和 @observer 你可以轻松的开发自己纯 JavaScript 组件和存储 Reactive。如果你喜欢函数式编程,没问题,这个也支持的很好。而 Redux 主要侧重于函数式编程的原则。不过你想要一个基于类的方法,可以使用 redux-connect-decorator 库来实现。

第 4 段(可获 1.09 积分)

4. 轻松处理嵌套数据

在大多数 JavaScript 应用中,你可能会经常需要处理关系或者是嵌套的数据。为了在 Redux 存储中使用这些数据,你必须首先进行 规范化 。然后需要编写 更多代码 来管理和跟踪规范化数据的引用。

而在 MobX,推荐存储中的数据必须是规范化的。MobX 会帮你实现对关系的跟踪并自动在更改时重新渲染。通过使用域对象来存储数据,你可以直接引用在其他存储中定义的域对象。此外,你可以使用 (@)computed decorators 和 modifiers for observables 来轻松解决复杂数据的要求。

第 5 段(可获 1.48 积分)

不使用 MobX 的 3 个理由

1. 太过自由

Redux 框架提供了非常严格的指导方针来告诉你如何写状态代码。这意味着你可以轻松编写测试和开发可维护的代码。而 MobX 是一个没有任何关于实现规则的库。这种走捷径方式和快速编码的危险就在于编写出来的代码很容易就变得不可维护。

2. 难以调试

MobX 内部的代码自动处理了很多让你应用变得 Reactive 的逻辑。你的数据在存储和组件之间传递的过程是不可见的,所以当遇见问题时就变得非常难以调试。如果你直接在组件中修改状态而没有使用 @actions, 那么你将会很难去确定一个错误发生的根源。

第 6 段(可获 1.71 积分)

3. 应该有一个 MobX 更好的替代品

在软件开发中,新兴技术层出不穷。在短短的几年内,当前的软件技术已经有过时的势头。目前 Redux 和 MobX 已经有很多可以与之竞争的解决方案。例如 Relay/Apollo & GraphQL, Alt.js 和 Jumpsuit。这些技术都有潜力成为最受欢迎的一个。如果你想知道哪个最适合你,你只能是把所有的方案都尝试一遍。

Redux 和 MobX 的代码比较

理论已经讲了很多了,下面来看看代码。首先我们比较二者的启动代码。

第 7 段(可获 1.34 积分)

引导

Redux 版本:
在 Redux 中我们首先定义好存储,然后将之通过 Provider 传递给 App。我们同时需要定义 redux-thunk 和 redux-promise-middleware 来处理异步函数。redux-devtools-extension 可以让我们在穿越模式下调试存储。 

// 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')
);
第 8 段(可获 0.48 积分)

MobX 版本:
在 MobX 中我们需要设置多个存储。在这个例子我只使用一个存储,我把它放到一个名为 allStores 的集合中。然后用一个 Provider 将存储集合传递给 App。

刚才我们提到 MobX 不需要外部库来处理异步的 actions,所以代码行更少一些。但是我们需要 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')
);
第 9 段(可获 0.83 积分)

这里两个版本的代码量大致相当。MobX 的 import 语句要更少一些。

道具注入

Redux 版本:
在 Redux 中,状态和动作通过 react-redux 的 connect() 函数传递给道具。

// 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);
第 10 段(可获 0.45 积分)

MobX 版本:
在 MobX 我们简单的注入 stores 集合。在容器或者组件类的顶部使用 @inject 来实现注入过程。这个让 stores 在 props 中可用,这样我们也可以访问指定的存储并传递给子组件。状态和 actions 动作都可以通过 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}
      />
  )
...
}
第 11 段(可获 0.85 积分)

MobX 版本看起来更容易理解。但是我们可以使用 redux-connect-decorators 来简化 Redux 代码。在这个地方没有明显的谁优谁劣。

定义存储、动作和 Reduces

为了保持文章的精简,下面是一个只有一个动作的代码示例。

Redux 版本:
在 Redux 中我们需要定义动作和 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 }
      }
    }
}
...
第 12 段(可获 0.74 积分)

MobX 版本:
在 MobX 中 action 和 reducer 的逻辑在一个类中就可以完成。我已经在接收到 response 后定义了一个异步的 action 调用另外一个 action —— entities fetched 。

由于 MobX 使用 OOP 风格,这里定义的 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);
  }
}
...
第 13 段(可获 0.9 积分)

信不信与否,两个版本定义的逻辑所做的事情是一样的:

  • 更新 UI 的加载状态
  • 异步的获取数据
  • 捕获异常并更新状态

在 Redux 中有 33 行代码,而 MobX 只需大约 14 行代码就可以完成相同的工作。MobX 主要的优势在于你可以在几乎所有的域存储类中重用基础代码,这个只需少量的修改设置是不做任何修改。这意味着你可以更快的构建应用。

其他不同之处

为了在 Redux 中创建表单,我使用的是 redux-form. 而在 MobX 用的是 mobx-react-form。两个库都很成熟,可以帮你轻松处理业务逻辑。个人而言,我更倾向于 mobx-react-form ,因为它可以让你使用插件来对表单字段进行校验。有了 redux-form,你也可以编写自己的校验代码,或者导入一个校验包来实现这个功能。

第 14 段(可获 1.85 积分)

MobX 一个小小的缺陷就是你不能在可观察对象中直接访问特定函数,因为它们不是真正纯 JavaScript 对象。幸运的是它们提供了 toJS() 函数可以将可观察对象转成 JavaScript 对象。

结论

很明显 MobX的代码要精简得多。通过 OOP 风格和良好的开发实践,你可以快速的构建各种应用。最大的弊病是很容易编写糟糕的不可维护的代码。

而 Redux 更受欢迎一些,而且 特别适合构建大型复杂应用。这是一个规定严格的框架,其规则确保开发人员可以编写易于测试和可维护的代码。但是,的确不适合开发小项目。

第 15 段(可获 1.6 积分)

尽管 MobX 有不少缺陷,但如果你遵循良好的习惯仍可以用来构建大的项目。正如爱因斯坦所说的 “ 让一切事物尽可能的简单,但不要简单”。

希望我已经提供了足够的信息,让你决定是否迁移到 MobX 或者是坚持使用 Redux。最终的决定取决于你的项目类型以及可用的资源。

如果你需要任何 MobX 的示例代码,可以在评论里说明。我相信你应该会在下一个 React 项目中试试 MobX 的。

第 16 段(可获 1.28 积分)

文章评论