Skip to content

Latest commit

 

History

History
777 lines (551 loc) · 36.1 KB

File metadata and controls

777 lines (551 loc) · 36.1 KB

五、存储模式

JavaScript 中围绕虚拟存储构建的模式包含决定在应用程序中显示什么所需的一切。在我看来,这是很好地理解 Flux 所必需的最重要的部分,因此,我专门用了一章来存储模式,通过许多示例和比较备选方案。由于 React 本机应用程序通常需要脱机工作,我们还将学习如何将 JavaScript 存储转换为用户移动设备上的持久存储。这将使我们的应用程序在用户体验方面达到一个新的水平。

在本章中,您将学习以下内容:

  • 如何将 Redux 集成到 Flux 体系结构中
  • Redux 与经典 Flux 的区别以及新方法的好处
  • Redux 的核心原则
  • 如何创建一个将成为唯一真相来源的商店
  • 作用模式和副作用是什么

使用 Redux 存储

我花了一段时间才想出如何向你宣传 Redux。最有可能的情况是,您期望它是某种将在 Flux 中使用的存储实现。这是真的;然而,事情远不止这些。Redux 是一段出色的代码,是一个很棒的工具。此工具可以在许多不同的项目中以多种方式使用。在这本书中,我努力教你在 React 和 Redux 中思考。

This introduction was inspired by a useful talk from Cheng Lou, called Taming the Meta Language, and delivered at React Conf 2017. Watch it at https://goo.gl/2SkWAj.

最小 Redux 应用程序示例

在我向您展示 Redux 架构之前,让我们先看看它的实际应用。了解 ReduxAPI 的外观是至关重要的。一旦我们在 Redux 中开发了最简单的 hello world 应用程序,我们将进入更高层次的概述。

我们将要构建的 hello world 应用程序是一个计数器应用程序,只需两个按钮(增加和减少)和一个指示当前计数的文本即可。

在开始之前,让我们使用以下命令安装两个软件包:

yarn add redux react-redux

好的,首先,让我们创建一些我们已经知道的基本 Flux 片段,但这次使用 Redux API:

  • ActionTypes
// Chapter 5 / Example 1 / src / flux / AppActionTypes.js

const ActionTypes = {
    INC_COUNTER: 'INC_COUNTER',
    DEC_COUNTER: 'DEC_COUNTER'
};

export default ActionTypes;
  • Store
// Chapter 5 / Example 1 / src / flux / AppStore.js

import { combineReducers, createStore } from 'redux';
import counterReducer from '../reducers/counterReducer';

const rootReducer = combineReducers({
    count: counterReducer            // reducer created later on
});

const store = createStore(rootReducer);

export default store;

注意两个新单词——ReducerrootReducerrootReducer将所有其他减速器合并为一个减速器。Reducer负责根据已发生的动作生成状态的新版本。如果当前操作与特定的Reducer不相关,减速机也可以返回旧版本的状态。

  • CounterReducer
// Chapter 5 / Example 1 / src / reducers / counterReducer.js

import types from '../flux/AppActionTypes';

const counterReducer = (state = 0, action) => {
    switch (action.type) {
    case types.INC_COUNTER:
        return state + 1;
    case types.DEC_COUNTER:
        return state - 1;
    default:
        return state;
    }
};

export default counterReducer;
  • Dispatcher
// Chapter 5 / Example 1 / src / flux / AppDispatcher.js
import store from './AppStore';

export default store.dispatch;

很好,我们已经有了所有的 Flux 块,所以我们现在可以继续实际的实现。

让我们先从简单的事情开始,视图。它应该显示两个Button和一个Text组件。按下按钮时,计数器应增加或减少,如下所示:

// Chapter 5 / Example 1 / src / views / CounterView.js

const CounterView = ({ inc, dec, count }) => (
    <View style={styles.panel}>
        <Button title="-" onPress={dec} />
        <Text>{count}</Text>
        <Button title="+" onPress={inc} />
    </View>
);

const styles = StyleSheet.create({
    panel: {
        // Check chapter 3: "Style patterns" to learn more on styling
        flex: 1,
        marginTop: 40,
        flexDirection: 'row'
    },
});

export default CounterView;

是时候为视图提供必要的依赖项了:incdeccounter道具。前两个问题相当简单:

// Chapter 5 / Example 1 / src / Counter.js
const increaseAction = () => dispatch({ type: types.INC_COUNTER });
const decreaseAction = () => dispatch({ type: types.DEC_COUNTER });

现在我们将它们传递给视图。在这里,将使用一些特定的 reduxapi 组件。Provider用于提供store连接呼叫。这是可选的,如果您确实想手动执行此操作,您可以直接将store传递给connect。我强烈建议使用Provider. Connect在分派和状态周围创建一个外观。如果状态发生更改,组件将自动重新渲染。

Facade is another pattern entirely. It is a structural design pattern created to interact with complex APIs. If the typical user is not interested in all of the capabilities, it is handy to provide a function with some defaults already preset on behalf of the user. Such a function is called a facade function and is also exposed in the API. The end user can use it a lot quicker, with none of the additional deep-diving that is required for complex and optimized projects.

检查以下代码片段中如何使用ProviderConnect

// Chapter 5 / Example 1 / src / Counter.js
...
import { Provider, connect } from 'react-redux';
...

const mapStateToProps = state => ({
    count: state.count,
    inc: increaseAction,
    dec: decreaseAction
});

const CounterContainer = connect(mapStateToProps)(CounterView);

const CounterApp = () => (
    <Provider store={store}><CounterContainer /></Provider>
);

export default CounterApp;

就这样。我们已经完成了第一个 Redux 应用程序。

Redux 如何适应 Flux

我们创建Counter应用程序的步骤包括连接焊剂块。让我们看看我们使用的图表:

首先,我们有动作已发送。然后运行 rootReducer函数,每个 reducer 确定是否需要更改状态。根目录减速机返回新版本的状态并将状态传递给视图根目录。connect函数确定是否应重新渲染特定视图。

Please note that the previous diagram follows the Flux architecture. The actual Redux implementation, as you could spot in the Counter example, is a little different. The dispatcher is encapsulated by the Store API and exposed as a store function.

移动到 Redux

Redux 可以做的不仅仅是非常简单的状态管理。众所周知,它在具有巨大状态对象和大量业务模型的应用程序中具有良好的性能和优势。这就是说,让我们重构我们的任务应用程序以进行 Redux。

The Tasks application was developed in the previous chapters. If you have jumped straight to this chapter, please have a look at the application located at src / Chapter 4 / Example 1_ Todo app with Flux, in the GitHub repository.

重构步骤将类似。将现有焊剂片更换为重复焊剂片:

  • ActionTypes实际执行还可以:
const ActionTypes = {
    ADD_TASK: 'ADD_TASK'
};

export default ActionTypes;
  • TaskStore.js:重命名为AppStore.js。现在,store只有一个实例。 此外,我们需要将reduce函数移动到一个单独的 reducer 文件中。剩下的内容应转换为新语法:
// Chapter 5 / Example 2 / src / data / AppStore.js

const rootReducer = combineReducers({ tasks: taskReducer});
const store = createStore(rootReducer);
export default store;
  • AppDispatcher.js:调度员现在是商店的一部分
// Chapter 5 / Example 2 / src / data / AppDispatcher.js
import store from './AppStore';
export default store;
// ATTENTION: To stay consistent with Flux API
// and previous implementation, I return store.
// Store contains dispatch function that is expected.
  • taskReducer.js:这是我们需要创建的新文件。但其内容是从之前的reduce功能复制而来:
// Chapter 5 / Example 2 / src / reducers / taskReducer.js
...
import data from '../data/tasks.json';

const taskReducer = (state = Immutable.List([...data.tasks]), action) => {
    switch (action.type) {
    case TasksActionTypes.ADD_TASK:
        if (!action.task.name) {
            return state;
        }
        return state.push({
            name: action.task.name,
            description: action.task.description,
            likes: 0
        });
    default:
        return state;
    }
};

export default taskReducer;

最后一个必需步骤是更改应用程序容器,如下所示:

// Chapter 5 / Example 2 / src / App.js

const mapStateToProps = state => ({ tasks: state.tasks });
const AppContainer = connect(mapStateToProps)(AppView);
const TasksApp = () => (
    <Provider store={store}><AppContainer /></Provider>
);

export default TasksApp;

到目前为止,一切顺利。它起作用了。但这里有几件事我们略过了。我将向您展示我们可以做得更好的地方,但首先,让我们学习一些 Redux 原则。

将 Redux 作为一种模式

如果操作正确,Redux 将提供出色的功能,例如时间旅行和热重新加载。时间旅行允许我们根据操作日志查看应用程序在一段时间内的外观。另一方面,热重新加载允许我们在不重新加载应用程序的情况下替换部分代码。

在本节中,我们将学习 Redux 的核心原则和一些常用的推荐方法。

Please make the effort to read the Redux documentation. It is a great and free resource to learn how to think in React and Redux. It will also help you expand your use of Redux beyond the React ecosystem, and is available at: https://redux.js.org/introduction/examples. 

Redux 的核心原则

单一真相来源:整个应用程序的状态存储在单个存储中的对象树中。理想情况下,应该有一个 Redux 存储,可以引导视图呈现整个应用程序。这意味着您应该使所有状态远离类组件,并将它们直接放在 Redux 存储中。这将简化在测试中或进行时间旅行时恢复视图的方法。

对于一些开发人员来说,只有一个地方来存储东西是不自然的,这很可能是因为,多年来,在后端,我们已经了解到它导致了单片架构。然而,在应用程序环境中并非如此。应用程序窗口不应垂直缩放以处理高负载的用户。它也不应该被数百个用户同时在一台设备上使用。

状态为只读:更改状态的唯一方法是向对象发出一个描述发生了什么的动作。重要的是,我们有一个单一的流,可以影响我们的商店。存储是应用程序状态的一种表示,不应该被随机代码改变。相反,任何有兴趣更改状态的代码都应该提交一份名为动作对象签名文件。此动作对象表示在我们的库中注册的已知动作,称为动作类型。还原器是决定状态更改的逻辑。具有单个修改操作流的不可变状态更易于维护和监督。判断某事是否发生了变化以及何时发生了变化会更快。我们可以轻松创建审计数据库,尤其是在银行等敏感行业,这是一个巨大的优势。

使用纯函数进行更改:要指定操作如何转换状态树,您需要编写纯还原器。这是一个我们尚未讨论的概念。减缩器必须是纯函数。纯函数保证外部环境不会影响函数的结果。简而言之,reducer 不能执行 I/O 代码、时间受限代码或依赖可变范围数据的代码。

A pure function is a function that satisfies two requirements:

  • 给定相同的输入参数,它返回相同的输出
  • 函数执行不会产生任何副作用

A good example are common math functions. For instance, an addition function given 1 and 3 always returns 4.

可能不清楚为什么所有这些都是有益的,并且应该被视为原则之一。想象一下,在开发阶段,一个 bug 被无意中引入到您的项目中。或者,更糟糕的是,它泄漏到生产中,并在用户的一次会话中炸毁了一个关键应用程序。最有可能的是,您有一些错误跟踪,您可以获得异常和堆栈跟踪,这显示了通过缩小代码的一条漫长而模糊的路径。但是,您需要修复它,因此您尝试在您的本地机器上重播完全相同的情况,最终连续三天才意识到问题是一些无聊的比赛条件。相反,想象一下,您有一个单独的操作流(没有随机交换未跟踪的条件)可以跟踪和记录。此外,您的整个应用程序依赖于一个只能根据动作流更改的状态。在失败的情况下,您需要存储的所有内容都是操作跟踪,以便对该情况作出响应。瞧,我刚刚救了你一两天。

When I learned Redux with similar examples, I still struggled to understand why pure functions are so important here. Playing with time-traveling within the Redux tab for Chrome allowed me to see it more clearly in the flesh. When you go back and forth with actions, some components that are stateful (that is, that rely on an internal state instead of the Redux one) will not be following along. This is a huge issue as it breaks your time travel, leaving some parts in a future state. 

转向单一的真理来源

是锻炼身体的时候了。我们的新目标是重构 Tasks 应用程序,使其拥有一个存储,这是唯一的真实来源。

为此,我们需要寻找依赖组件状态而不是 Redux 存储的地方。到目前为止,我们有三种观点:

  • AppView.js:这有一个相当简单的划分为页眉、页脚和主要内容。 这是一个表示组件,不包含任何状态。它的道具由AppContainer提供,它已经使用了 Redux 商店。AppView将主要内容委托给以下两个子视图。
  • TaskList.js:这是一个演示组件,负责在一个简单的可滚动列表中显示待办任务。其道具由AppViewAppContainer转发。
  • AddTaskForm.js:这是一个容器组件,基于TextInput组件。此部件使用内部状态。如果可能的话,我们应该重构它。

If you have ever read about React and Redux, you may find this example pretty similar to what you can find for web pages—however, it is not. If you followed this book for the first chapters, you may have a gut instinct as to why; if not, I highly recommend going back for a while to Chapter 2 > Building Forms > Uncontrolled Inputs.

我们的目标是以某种方式将状态从AddTaskForm转移到 Redux 商店。这就是问题开始的地方。您可能已经发现TextInput是 React 原生 API 的一部分,我们无法更改它。但是TextInput是一个有状态的组件。这是在构建 React 本机应用程序时,您应该了解的关于 Redux 的第一件事,某些部分需要有状态,而您无法解决它。

幸运的是,TextInput的有状态部分只管理焦点。您不太可能需要在 Redux 应用商店中存储相关信息。所有其他状态都属于我们的AddTaskForm组件,我们可以解决它。让我们马上做吧。

In idiomatic Redux, your state should be normalized similarly to the databases. There are known normalization techniques used in SQL databases that usually are based on ID references between entities. You can adopt this approach in your Redux store by using, for the instance, Normalizr library.

首先,我们将重建AddTaskForm组件。它需要发送一个新的操作,该操作将触发一个新的 reducer,并在 Redux 存储中更改一个新的密钥(我们稍后将开发后面的部分):

// Chapter 5 / Example 3 / src / views / AddTaskForm.js
class AddTaskForm extends React.Component {
    // ...
    handleSubmit = () => {
        if (this.props.taskForm.name) {
            TaskActions.addTask({
                name: this.props.taskForm.name,
                description: this.props.taskForm.description
            });
            this.nameInput.clear();
            this.descriptionInput.clear();
        }
    };

    render = () => (
        <View style={styles.container}>
            <TextInput
                style={styles.input}
                placeholder="Name"
                ref={(input) => { this.nameInput = input; }}
                onChangeText={
 name => TaskActions.taskFormChange({
 name,
                        description: this.props.taskForm.description
                    })
 }
                value={this.props.taskForm.name}
            />
            <TextInput
                style={styles.input}
                placeholder="Description"
                ref={(input) => { this.descriptionInput = input; }}
 onChangeText={
 desc => TaskActions.taskFormChange({
 name: this.props.taskForm.name,
                        description: desc
 })
 }
 value={this.props.taskForm.description}
            />
            <Button
                title="Add task"
                onPress={() => this.handleSubmit()}
            />
        </View>
    );
}

最困难的部分已经过去了。是时候创建一个全新的taskFormReducer,如下所示:

// Chapter 5 / Example 3 / src / reducers / taskFormReducer.js

export const INITIAL_ADD_TASK_FORM_STATE = {
    name: '',
    description: ''
};

const taskFormReducer = (
    state = INITIAL_ADD_TASK_FORM_STATE,
    action
) => {
    switch (action.type) {
    case TasksActionTypes.TASK_FORM_CHANGE:
        return action.newFormState;
    default:
        return state;
    }
};

export default taskFormReducer;

接下来,向TasksActionTypes添加一个新的动作类型,如本片段所示:

// Chapter 5 / Example 3 / src / data / TasksActionTypes.js
const ActionTypes = {
    ADD_TASK: 'ADD_TASK',
    TASK_FORM_CHANGE: 'TASK_FORM_CHANGE'
};

然后,添加操作本身,如下所示:

// Chapter 5 / Example 3 / src / data / TaskActions.js
const Actions = {
    // ...
    taskFormChange(newFormState) {
        AppDispatcher.dispatch({
            type: TasksActionTypes.TASK_FORM_CHANGE,
            newFormState
        });
    }
};

接下来,在AppStore中注册一个新的减速器,如下所示:

// Chapter 5 / Example 3 / src / data / AppStore.js
const rootReducer = combineReducers({
    tasks: taskReducer,
    taskForm: taskFormReducer
});

最后,我们需要通过新的状态:

// Chapter 5 / Example 3 / src / App.js
const mapStateToProps = state => ({
    tasks: state.tasks,
    taskForm: state.taskForm
});

我们将其沿着组件树传递到AppView,如下所示:

// Chapter 5 / Example 3 / src / views / AppView.js
const AppView = props => (
        // ...
        <AddTaskForm taskForm={props.taskForm} />
        // ...
);

最后,我们连接了所有的位。享受您集中的单一真相来源 Redux 商店。

Alternatively, take a look at the redux-form library. As of writing this book, it is an industry standard for building forms in Redux. The library can be found at https://redux-form.com.

使用 MobX 创建替代方案

在没有强有力的替代方案的情况下依赖 Redux 是愚蠢的。其中一个替代方案是 MobX,一个在突变方面不那么固执己见的状态管理库。MobX 提供尽可能少的样板文件。这与 Redux 相比是巨大的,Redux 非常明确,需要大量的样板文件。

Here I must stop to remind you that the React ecosystem leans towards explicitness, that is, building apps without too many hidden mechanisms. You control the flow and you see all of the bits that are required for the app to go the full cycle of Flux. It's n surprise, then, that mainstream developers prefer Redux. An interesting fact is that Facebook Open Source is backing the MobX project.

MobX 更加隐式,它可以隐藏一些围绕可观察对象构建的逻辑,并提供简洁的注释,以使用 MobX 流快速增强有状态组件。

一些开发人员可能会发现这是一种更好的方法,最有可能的是那些来自面向对象背景的人,他们已经习惯了这些事情。我发现 MobX 是一个更容易启动和开发原型或概念验证应用程序的库。然而,由于逻辑对我来说是隐藏的,我担心一些开发人员将永远不会看到幕后黑幕。这可能会导致性能不佳,以后无法轻松修复。

让我们看看在行动中的感受。

移动到 MobX

在本节中,我们将重构 Tasks 应用程序以使用 MobX 而不是 vanilla Flux。

The Tasks application was developed in the previous chapters. If you have jumped straight to this chapter, please have a look at the application located at src / Chapter 4 / Example 1_ Todo app with Flux, in the GitHub repository.

在开始之前,请使用以下命令安装这两个软件包:

yarn add mobx mobx-react

好的,首先,让我们清理不需要的碎片:

  • AppDispatcher.js:调度由 MobX 在幕后使用可观察物完成。
  • TaskActions.js:Actions 现在将生活在TaskStore中,并在其状态下工作。在 MobX 中,你很可能会有很多商店,所以这不是一个大问题,我们把相关的东西放在一起。
  • TasksActionTypes.js:无需对此进行定义。MobX 将在内部处理它。

正如您所看到的,在开始之前,我们已经删除了这么多的开销。这是 MobX 最大的优点之一,也是图书馆的粉丝们提到的。

是时候以 MobX 的方式重建商店了。这将需要一些新的关键字,因此请仔细阅读以下代码段:

// Chapter 5 / Example 4 / src / data / TaskStore.js
import { configure, observable, action } from 'mobx';
import data from './tasks.json';

// don't allow state modifications outside actions
configure({ enforceActions: true });

export class TaskStore {
    @observable tasks = [...data.tasks]; // default state

    @action addTask(task) {
        this.tasks.push({
            name: task.name,
            description: task.description,
            likes: 0
        });
    }
}

const observableTaskStore = new TaskStore();
export default observableTaskStore;

如您所见,我从 MobX 库导入了三个新关键字:

  • configure:用于设置我们的存储,以便仅通过操作强制执行突变。
  • observable:这是用来丰富财产的,现在可以观察到。如果您有一些关于流或可观察对象的 JavaScript 背景,那么它实际上是由这些内容包装的。
  • action:这就像其他任何动作一样,但它是以装饰的方式使用的。

最后,我们创建一个存储实例,并将其作为默认导出传递。

现在,我们需要向视图展示我们的新商店。为此,我们将使用 MobXProvider,这是一个与 Redux 中类似的实用程序:

// Chapter 5 / Example 4 / src / App.js
// ...
import { Provider as MobXProvider } from 'mobx-react/native';
// ...

const App = () => (
    <MobXProvider store={TaskStore}>
        <AppView />
    </MobXProvider>
);

export default App;

前面代码段的最后一部分涉及重构子视图。

AppView组件提供了下至TaskList组件的任务。现在,让我们使用新创建的存储中的任务:

// Chapter 5 / Example 4 / src / views / AppView.js

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

@inject('store') @observer
class AppView extends React.Component {
 render = () => (
     // ...
     <AddTaskForm />
     <TaskList tasks={this.props.store.tasks} />
     // ...
 );
}

让我们做一些与AddTaskForm类似的事情,但是我们将使用addTask函数而不是tasks

// Chapter 5 / Example 4 / src / views / AddTaskForm.js
// ...

@inject('store') @observer
class AddTaskForm extends React.Component {
    // ...
    handleSubmit = () => {
        this.props.store.addTask({
            name: this.state.name,
            description: this.state.description
        });
        // ...
    };
    // ...
}

就这样!我们的应用程序再次功能齐全。

使用带有注释的 PropType

如果你跟着走,你可能会感到有点失落,因为你的皮棉可能开始抱怨不足或缺失PropTypes。让我们来解决这个问题。

对于AppView,我们缺少tasks存储的PropTypes验证。当用@observer注释类时,需要为wrappedComponentPropTypes有点棘手,如下所示:

AppView.wrappedComponent.propTypes = {
    store: PropTypes.shape({
        tasks: PropTypes.arrayOf(PropTypes.shape({
            name: PropTypes.string.isRequired,
            description: PropTypes.string.isRequired,
            likes: PropTypes.number.isRequired
        })).isRequired
    }).isRequired
};

对于AddTaskForm,我们缺少addTask存储操作的PropTypes验证。现在我们来解决这个问题:

AddTaskForm.wrappedComponent.propTypes = {
    store: PropTypes.shape({
        addTask: PropTypes.func.isRequired
    }).isRequired
};

就这样,皮棉的抱怨都没有了。

比较 Redux 和 MobX

有一天我在想如何比较这两种情况,我想到了以下几点。

This section is highly influenced by Preethi Kasireddy's talk at React Conf 2017. Please spend half an hour and watch it. You can find the talk at https://www.youtube.com/watch?v=76FRrbY18Bs.

MobX 就像汽车的道路系统。创建路线图,让人们驾驶。有些会导致事故,有些会小心驾驶。一些道路可能被限制为单向,以限制交 Flux,甚至以某种方式成形,以便于对车流进行更简单的推理,如在曼哈顿。另一方面,Redux 就像一列火车。一次只能有一列火车在轨道上行驶。如果在同一时刻有几辆车,前面的那辆车被什么东西挡住了,那么每一辆车都会在后面等着,就像在地铁站一样。有时火车需要把人送到大陆的另一边,这也是可能的。所有这些车流都由一个(分布式)机构管理,该机构负责规划车流并对车流进行限制。

记住这个例子,让我们更仔细地看一下这些库:

  • Redux 使用普通对象,而 MobX 将对象包装成可见对象。 你可能希望我再次提到一些魔法——不。残酷的事实是 MobX 是要付出代价的。它需要包装可观察的数据,并为每个对象或集合的每个成员添加一些权重。查找多少数据是相当容易的:使用console.log查看可观察到的集合。
  • Redux 手动跟踪更新,而 MobX 自动跟踪更新。
  • Redux 状态是只读的,可以通过调度操作来更改,而 MobX 状态可以随时更改,有时只能通过使用存储 API 公开的操作来更改。此外,在 MobX 中,不需要执行操作。您可以直接更改状态。
  • 在 Redux 中,状态通常是标准化的,或者至少建议这样做。在 MobX 中,您的状态是非规范化的,计算值是嵌套的。
  • 无状态和有状态组件:在这里可能看起来很困难。Preethi Kasireddy 在前面信息框中链接的讲座中提到,MobX 只能与智能组件一起使用。在某种程度上,这是正确的,但这里与 Redux 没有区别。两者都支持表示组件,因为它们与状态管理库完全解耦!
  • 学习曲线这是非常主观的标准。有些人会觉得 Redux 更容易,有些人会觉得 MobX 更容易。人们普遍认为 MobX 更容易学习。我是个例外。
  • Redux 需要更多的样板文件。更明确地说,这是非常简单的,但是如果您不介意的话,有一些库可以解决这个问题。此处不提供参考资料,我建议使用。
  • Redux 更容易调试。这自然与单一流和轻松重放消息有关。这就是 Redux 的亮点所在。MobX 在这里更老派,更难预测,甚至对有经验的用户来说也不那么明显。
  • 在可伸缩性方面,Redux 获胜。MobX 可能会带来一些可维护性问题,特别是在具有大量连接和大型域的大型项目中。
  • MobX 简洁,在时间有限的小型项目中大放异彩。如果你去黑客大会,考虑使用 MOBX。在大型、长期项目中,除了 MobX 提供的自由外,您还需要一种更加固执己见的方法。
  • MobX 遵循 Flux 架构,并且不像 Redux 那样对其进行修改。Redux 倾向于一个全局存储(尽管可以与多个存储一起使用!),而 MobX 对于存储的数量非常灵活,其示例通常展示了与 Flux 早期想法类似的想法。

在使用 Redux 时,您需要学习如何处理不同的情况以及如何组织事情。特别是在处理副作用时,你需要学习 Redux Thunk 和可能的 Redux Saga,这将在下一章中介绍。在 MobX 中,所有这些都是通过反应流在幕后神奇地处理的。在这方面,MobX 是固执己见的,但它从您身上拿走了一项责任。

在 React Native 中使用系统存储

来自本机环境的人习惯于持久性存储,如数据库或文件。到目前为止,任何时候重新启动我们的应用程序,它都会失去状态。我们可以使用系统存储解决这个问题。

为此,我们将使用 React Native 附带的AsyncStorageAPI:

"On iOS, AsyncStorage is backed by native code that stores small values in a serialized dictionary and larger values in separate files. On Android, AsyncStorage will use either RocksDB or SQLite based on what is available."  - From the React Native official documentation, which can be found at: https://facebook.github.io/react-native/docs/asyncstorage.html.

AsyncStorageAPI 非常容易使用。首先,让我们保存数据:

import { AsyncStorage } from 'react-native';

try {
    await AsyncStorage.setItem('@MyStore:key', 'value');
} catch (error) {
    // Error saving data
}

接下来,我们将介绍如何检索保存的值:

try {
    const value = await AsyncStorage.getItem('@MyStore:key');
} catch (error) {
    // Error retrieving data
}

然而,文档建议我们对AsyncStorage使用一些抽象:

"It is recommended that you use an abstraction on top of AsyncStorage instead of AsyncStorage directly for anything more than light usage since it operates globally."  - From the React Native official documentation, which can be found at: https://facebook.github.io/react-native/docs/asyncstorage.html.

那么,让我们遵循标准库redux-persist。关于存储的话题非常广泛,而且有点超出了本书的范围,所以我不想深入讨论这个话题。

让我们使用以下命令安装库:

yarn add redux-persist redux-persist-transform-immutable

第一步是使用新的持久化中间件增强我们的AppStore定义,如下所示:

// Chapter 5 / Example 5 / src / data / AppStore.js
// ...
import { persistStore, persistReducer } from 'redux-persist';
import immutableTransform from 'redux-persist-transform-immutable';
import storage from 'redux-persist/lib/storage';

const persistConfig = {
    transforms: [immutableTransform()],
 key: 'root',
    storage
};

const rootReducer = combineReducers({
    // ...
});
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = createStore(persistedReducer);
export const persistor = persistStore(store);
export default store;

当我们完成配置时,我们需要使用PersistGate.加载状态。如果您有一个自定义组件,您可以为加载道具提供一个自定义组件:

// Chapter 5 / Example 5 / src / App.js
import store, { persistor } from './data/AppStore';
// ...
const TasksApp = () => (
    <Provider store={store}>
        <PersistGate loading={null} persistor={persistor}>
            <AppContainer />
        </PersistGate>
    </Provider>
);

瞧!无论何时重新启动应用程序,状态都将从持久存储中加载,并且您将看到上次启动应用程序时的所有任务。

效应模式

在处理外部数据时,需要处理外部因素,例如网络或磁盘。这些因素会影响您的代码,因此它需要是异步的。此外,您应该努力将其与可预测的部分分离,因为网络是不可预测的,可能会失败。我们称之为副作用,你们已经了解了一些。

为了理解这一点,我想介绍一个大词:效果。

"We yield plain JavaScript Objects [...]. We call those Objects Effects. An Effect is simply an object that contains some information to be interpreted by the middleware. You can view Effects like instructions to the middleware to perform some operation (e.g., invoke some asynchronous function, dispatch an action to the store, etc.)."

如果在直接作用范围之外使用,则会产生所谓的副作用,因此得名。最常见的情况是,这意味着外部范围变量的突变。

没有副作用是数学证明程序正确性的关键。稍后我们将在第 9 章函数式编程模式的要素中深入探讨这个主题。

处理副作用

第 4 章流量架构中,您了解了什么是副作用,以及可以采用什么策略将其与视图和存储分离。在使用 Redux 时,您应该坚持使用它们。然而,有一些整洁的库已经为 Redux 开发,以解决这个问题。您将在下一章中了解更多关于它们的信息,这一章专门讨论这个问题:

"We're mixing two concepts that are very hard for the human mind to reason about: mutation and asynchronicity. I call them Mentos and Coke. Both can be great in separation, but together they create a mess. Libraries like React attempt to solve this problem in the view layer by removing both asynchrony and direct DOM manipulation. However, managing the state of your data is left up to you. This is where Redux enters."

  • Official Redux documentation

总结

在本章中,我们讨论了存储在体系结构中的重要性。您学习了如何塑造应用程序以满足不同的业务需求,从使用状态和全局状态混合方法的非常脆弱的应用程序,到允许时间旅行和 UI 重构的复杂应用程序。

我们没有专注于一个主流解决方案,即 Redux,而是探索了 MobX 库的完全不同的方法。我们发现它在很多领域都很好,比如快速原型和小项目,现在您知道什么时候、在哪些项目中选择 MobX 而不是 Redux 是明智的。

进一步阅读