如何在真实世界构建你的Mobx应用程序

原文出处 How to structure your MobX app for the real world

本文是受到@mwestrase许多文章的启发, 以及经过几个星期将一个大型的Backbone应用程序重构为React + MobX应用程序,是将Mobx添加到一个普通的React项目的“衣钵传人”。 或许这不是构建Mobx应用程序的 最佳方式 ,但这种方式到目前为止一直在我的项目中很好的运作。

设计本架构的目标是:

  • 样板最小化

  • 使应用程序成为一个状态机

  • 灵活移动元素

我们将通过React上下文和MobX的出色的inject函数来实现样板最小化。这样的组合使数据集无需进行任何接线和props传递就可以在应用程序的任何地方使用。 如果组件需要访问状态机, 注入到store中。

你的UI设计师大可以根据其想法重新调整页面,而你所要做的只是更改组件的位置。 除非业务逻辑本身改变,否则不需要重新接线业务逻辑。 对我而言,这曾经是React的一个难题。 使用这种方法,你的组件变得真正独立,你可以做任何你想要的。

这非常有趣,我甚至为此早早来上班! 如果你了解我,你会知道我有多喜欢过属于自己的早晨。

这是如何完成的

通过上下文和注入给了我们灵活性,并消除样板。 那么状态机呢?

我们把MobX的store视为应用程序中的唯一真实来源,并且我们将把actions置于其中。通过actions改变状态,并可以在任何地方调用。将它们放在store中以减少样板,并确保所有组件都可以访问它们,这会让你可以把整个状态机看成一个单文件。

这也会使你的应用程序更容易测试。 如果你的状态机工作正常,那么就一切正常。 这是因为:

  • 在严格模式下MobX保证状态只会在actions中改变

  • React保证DOM是一个纯粹的状态表达式

是的,MobX运作通过可变状态,改变观察者以及所有有趣的东西。 如果在过去几年你一直听言关于不可变状态的函数式编程的倡导,那么这听起来很糟糕。

但是你知道什么才是酷的? 无论如何,你可以获得所有的好处。 MobX将你的状态变化包装在getter和setter中,但它也可以支持那些具有不可变数据结构的应用程序,并且actions可以为时间旅行调试器建立一个更新日志。

这可能会是一个有趣的项目。 我可以使用MobX的时间旅行调试器?

model是另一个难题。

model表示整个数据结构中的特定对象。 MobX的store会关注你的整个应用程序的状态;model是为了说明一个特定的React组件所关注的特定实例。

这听起来model应该在组件状态中,对吧? 但这是一个糟糕的主意,因为这样会让你的东西更难测试并且会打破状态机的理想状态。

model也是存放与后端交互的actions的好地方。 诸如保存,获取,更新。


让我们构建一盒柿子

你可以把它想象成伪代码。

让我们构建一盒 柿子。 现在我的厨房柜台上就有一盒,因为柿子就长在我女朋友的妈妈的后院。 谁知道? ¯(ツ)

盒子可以被打开或关闭。盒子里有柿子时,可以用胶带密封它, 它的状态机看起来像这样:

一旦盒子被打开, 只要你想,你可以放入或取出尽可能多的柿子。你可以从任何N kaki状态关闭这个盒子,但你必须通过打开它来放入或取出柿子。 但只有在关闭时才能密封,只有在启封时才能打开。

盒子,model

作为MobX的store和model,你的盒子可能像这样:

// src/models/Box.js

import { observable, computed, action, extendObservable } from 'mobx';

export class Box {
    @observable sealed = true;
    @observable closed = true;
    @observable kakis = [];

    constructor(store, initialState) {
        this.store = store;
        extendObservable(this, initialState);
    }

    @computed get canSeal() {
        return this.closed;
    }

    @computed get canOpen() {
        return !this.sealed;
    }

    @computed get canManipulatekakis() {
        return !this.closed;
    }

    @action addkaki() {
        this.kakis.push(new kaki());
    }

    @action takekaki() {
        this.kakis.pop();
    }

    @action open() {
        if (this.canOpen) {
            this.opened = true;
        }
    }

    @action close() {
        this.closed = true;
    }

    @action seal() {
        if (this.canSeal) {
            this.sealed = true;
        }
    }

    @action unseal() {
        this.sealed = false;
    }
}

@ observable是一个MobX装饰器,MobX会使得属性可观察。extendObservable是在对象上设置许多可观察值的一种方便的方法。@ computed会标记一些纯粹从状态派生出的属性,MobX可以记忆它们。@action会将方法标记为动作。

看看那张状态机的图片。观察者和计算属性放在一起是圆圈(状态),动作是箭头(状态之间的转换)。

主应用程序组件

为了一个盒子做了很多工作吧? 看看我们把它放在React组件里会发生什么:

// src/components/App.js

import { Provider } from 'mobx-react';

import { Box } from './models/Box';

// would normally go in src/stores/...
class MainStore {
    @observable box = null;

    @action getBoxFromMail() {
        this.box = new Box({
            sealed: true,
            closed: true,
            kakis: [new kaki(), new kaki()]
        });
    }
}

class App extends Component {
    mainStore = new MainStore();

    render() {
        const mainStore = this.mainStore;

        return (
            <Provider mainStore={mainStore}>
                <div>
                    <Button onClick={mainStore.getBoxFromMail.bind(mainStore)}>
                        Get Mail
                    </Button>

                    <Box />
                </div>
            </Provider>
        );
    }
}

我们建立了一个新的MainStore,它保存了我们应用程序的主要状态。诸如是否从邮箱取出一盒柿子,柜台是否是黑色,冰箱是否在工作之类的。

在render函数中,我们使用ProvidermainStore添加到React上下文。 如果需要,Provider中的任何组件都可以访问mainStore

将actions与store和model捆绑在一起好处是,你可以在onClick处理程序中使用它们。 这意味着你的大部分组件可以是无状态的功能组件。

盒子, React组件

我来给你展示。

// src/components/Box.js

import { inject, observer } from 'mobx-react';

const SealedOrOpened = observer(({ box }) => (
    if (box.sealed) {
        return 'Sealed and Closed';
    }else if (!box.sealed && !box.opened) {
        return 'You should open the box';
    }else{
        return 'Take some kakis!';
    }
));

// this helper normally goes somewhere else
const If = ({ cond, children }) => cond ? children : null;

const Box = inject('mainStore')(observer(({ mainStore }) => {
    const box = mainStore.box;

    return (
    <div>
        <SealedOrOpened box={box} />
        <If cond={box.canOpen}>
            <Button onClick={box.open.bind(box)}>Open box</Button>
        </If>

        <If cond={box.opened}>
            <ul>
                {box.kakis.map(kaki => <kaki />)}
            </ul>
            <Button onClick={box.takekaki.bind(box)}>
                Take a kaki
            </Button>
        </If>
    </div>
    );
));

这就是本质上的Box组件。它会渲染一些文本,告诉你如何处理这个盒子,并给你一个打开它的按钮。是的,我知道应该有一个按钮来取下胶带。

一旦你打开盒子,就会出现一个柿子列表和一个按钮,来把它们一个接一个地取出来。

请注意,Box是一个函数式无状态的组件。 你可以这样做,因为所有的状态和所有的操作都在Box,即MobX的model中。 没有必要在本地建立点击处理程序。

您可能还会注意到,我们已经将inject('mainStore')注入到Box组件中。这使得我们可以将组件移动到Provider内的DOM树中的任何位置,并且它将继续工作。不需要任何的修改。

❤️ 总结

你的UI设计师会喜欢这种灵活性,你的PM会决定你的速度超乎想象。

哦,那个observer呢? 它可以确保你的组件在状态更改时重新渲染。

下周,我要写一篇关于将这些更改保留到某种后端的文章。 我仍在努力如何使这部分省事且整洁。

也许我应该写一本关于React和MobX构建Web应用程序的书? 如果你感兴趣,请告诉我.