第 12 章 使用 Redux 完善 Flux 应用

第 12 章 使用 Redux 完善 Flux 应用

Flying
2018-12-25 / 0 评论 / 275 阅读 / 正在检测是否收录...

上一章带你实现了一个基于 Flux 架构的完整的 React 应用。在本章中,你将对这个应用进行一些修改,以便它使用 Redux 库来实现 Flux 架构。本章的组织结构如下:

  • Redux 的简要概述
  • 实现控制状态的 reducer 函数
  • 构建 Redux action 创建器
  • 将组件连接到 Redux store
  • 应用状态的 Redux 入口

为什么是 Redux?

在我们开始重构你的应用之前,我们将花几分钟时间从高层次上了解一下 Redux。刚好能勾起你的食欲。准备好了吗?

一个 store 管理所有

传统的 Flux 应用和 Redux 的第一个主要区别是,使用 Redux,你只有一个 store。传统的 Flux 架构可能也只需要一个 store,但是它可能有多个 store。你可能会认为拥有多个 store 实际上可以简化架构,因为你按应用的不同部分分隔状态。的确,这是一个很好的策略,但在实践中不一定有效。创建多个 store 可能会导致混乱。store 是架构中的移动部件。如果你有更多,就有更多的可能性出错。

Redux 通过只允许一个 store 来消除这个可能。你可能会认为这将导致单一的数据结构,使各种应用功能难以使用。事实并非如此,因为你可以自由地以任何你喜欢的方式构建你的 store。

更少的变化部件

通过只允许一个 store, Redux 消除了变化部分。Redux 简化架构的另一个地方是消除了对专用 dispatcher 的需求。在传统的 Flux 架构中,dispatcher 是一个独特的组件,它将消息发送给 stores。由于在 Redux 架构中只有一个 store,你可以直接给 store 调度 actions。换句话说,store 就是 dispatcher。

Redux 减少代码中移动部分数量的最后一个地方是事件监听器。在传统的 Flux 应用中,你必须手动订阅和退订 store 事件,才能正确地连接所有内容。当你可以让一个库为你处理布线工作时,这是一种干扰。Redux 在这方面做得很好。

使用 Flux 最好的部分

Redux 不是传统意义上的 Flux。Flux 有一个规范和一个库来实现它。Redux 不是这样的。如前文所述,Redux 是对 Flux 的简化。它保留了所有导致健全应用架构的 Flux 的概念,同时忽略了那些使 Flux 难以实现并最终难以被采用的乏味部分。

用 Reducer 控制状态

Redux 的核心概念是,状态由 reducer 函数控制。在本节中,我们将介绍什么是 reducer,以及在 Snapterest 应用中如何实现 reducer 函数。

什么是 Reducer?

Reducers 是一种函数,它接受一个数据集合,比如一个对象或一个数组,并返回一个新的集合。返回的集合可以包含与初始集合相同的数据,也可以包含与初始集合完全不同的数据。在 Redux 应用中,reducer 函数接收一个 state 切片,并返回一个新的 state 切片。就是这样!你已经了解了 Redux 架构的关键。现在让我们看看 reducer 函数的实际效果。

Redux 应用中的 Reducer 函数可以被分离成模块,这些模块代表了它们处理的应用状态的一部分。我们将介绍集合 Reducer,然后介绍 Snapterest 应用的推文 Reducers。

集合 Reducer

现在,让我们演练一下更改部分应用状态的集合 Reducer 函数。 首先,让我们看一下整个函数:

const collectionReducer = ( state = initialState, action ) => {
  let tweet;
  let collectionTweets;


  switch (action.type) {
    case 'add_tweet_to_collection':
      tweet = {};
      tweet[action.tweet.id] = action.tweet;


      return {
        ...state, collectionTweets: {
          ...state.collectionTweets,
          ...tweet
        }
      };

        case 'remove_tweet_from_collection':
      collectionTweets = { ...state.collectionTweets };
      delete collectionTweets[action.tweetId];

      return {
        ..state, collectionTweets
      };


    case 'remove_all_tweets_from_collection':
      collectionTweets = {};

      return {
        ...state, collectionTweets
      };


    case 'set_collection_name':
      return {
        ...state,
        collectionName: state.editingName, isEditingName: false
      };


    case 'toggle_is_editing_name':
      return {
        ...state,
        isEditingName: !state.isEditingName
      };


    case 'set_editing_name':
      return {
        ...state,
        editingName: action.editingName
      };


    default:
      return state;
  }
};

如你所见,返回的新状态基于已调度的 action。action 名称作为参数提供给此函数。现在让我们来看看这个 reducer 的不同场景。

将推文添加到集合

让我们来看看 add_tweet_to_collection action:

case 'add_tweet_to_collection':
  tweet = {};
  tweet[action.tweet.id] = action.tweet;

  return {
    ...state,
    collectionTweets: {
    ...state.collectionTweets,
    ...tweet
  }
};

switch 语句检测到 action typeadd_tweet_to_collection。该 action 还有一个 tweet 属性,其中包含要添加的实际消息。这里使用 tweet 变量来构建一个以 tweet ID 为键、tweet 为值的对象。这是 collectionTweets 对象所期望的格式。

然后我们返回新的状态。重要的是要记住,这应该始终是一个新对象,而不是对其他对象的引用。这就是你如何避免 Redux 应用中意想不到的副作用。幸运的是,我们可以使用对象展开运算符来简化这项任务。

从集合中删除推文

collectionTweets 对象中删除一条 tweet 意味着我们必须删除包含 tweet ID 键。让我们看看这是如何实现的:

case 'remove_tweet_from_collection':
  collectionTweets = { ...state.collectionTweets };

  delete collectionTweets[action.tweetId];
  return {
    ...state,
    collectionTweets
  };
注意到我们如何为 collectionTweets 变量分配一个新对象了吗?扩展运算符在这里再次派上用场,以避免额外的语法。我们这样做的原因是,reducer 总是返回一个新的引用。从 collectionTweets 对象中删除推文后,我们可以返回包含 collectionTweets 作为属性的新状态对象。

另一个推文删除 action 是 remove_all_tweets_from_collection。代码如下:

case 'remove_all_tweets_from_collection':
  collectionTweets = {};
  return {
    ...state,
    collectionTweets
  };

删除所有推文意味着我们可以将 collectionTweets 值替换为新的空对象。

设置集合名称

当重命名推文集合时,我们必须更新 Redux store。这是通过调度 set_collection_name action 时从状态获取 editName 来完成的:

case 'set_collection_name':
  return {
    ...state,
    collectionName: state.editingName,
    isEditingName: false
  };

可以看到 collectionName 值被设置为 editingName, isEditingName 被设置为 false。这意味着由于设置了该值,我们知道用户不能再编辑名称。

编辑集合名称

你刚刚看到了如何在用户保存更改后设置集合名称。然而,当涉及到在 Redux stores 中跟踪状态时,还有更多的内容需要编辑文本。首先,我们必须让文本能够被编辑,这为用户提供了某种视觉提示:

case 'toggle_is_editing_name':
  return {
    ...state,
    isEditingName: !state.isEditingName
  };

然后用户在文本输入中主动键入的文本。这也必须在 store 的某个地方:

case 'set_editing_name':
  return {
    ...state,
    editingName: action.editingName
  };

这不仅会导致相应的 React 组件重新渲染,而且还意味着我们将文本存储在状态中,当用户完成编辑时就可以使用了。

推文 Reducer

推文 reducer 只需要处理一个 action ,但这并不意味着我们不应该将推文 reducer 放在自己的模块中,以预测将来推文的 actions。现在,让我们只关注我们的应用当前的功能。

接收推文

让我们看一下处理 receive_tweet action 的推文 reducer 代码:

const tweetReducer = (state = null, action) => {
  switch (action.type) {
    case 'receive_tweet':
      return action.tweet;
    default:
      return state;
  }
};

这个 reducer 非常简单。当调度 receive_tweet action 时,action.tweet 值作为新状态返回。因为这是一个小的 reducer 函数,所以可以在这里指出所有 reducer 函数的共同点。

传递给 reducer 函数的第一个参数是旧状态。这个参数有一个默认值,因为 reducer 第一次被调用时没有状态,这个值用来初始化它。在本例中,默认状态为 null。

关于 reducers 需要指出的第二件事是,它们在被调用时总是返回一个新的状态。即使它不产生任何新状态,reducer 函数也需要返回旧状态。Redux 会将新的 state 设置为 reducer 返回的任何值,即使返回的值是 undefined。这就是为什么在 switch 语句中设置一个默认标签是个好主意。

简化 action 创建器

在 Redux 中,action 创建器函数比传统的 Flux 函数简单。主要的区别是 Redux action 创建器函数只返回 action 数据。在传统的 Flux 中,action 创建器还要负责调用 dispatcher。让我们看一下 Snapterest 的 Redux action 创建器函数:

export const addTweetToCollection = (tweet) => ({
  type: 'add_tweet_to_collection',
  tweet,
});
export const removeTweetFromCollection = (tweetId) => ({
  type: 'remove_tweet_from_collection',
  tweetId,
});
export const removeAllTweetsFromCollection = () => ({
  type: 'remove_all_tweets_from_collection',
});
export const setCollectionName = (collectionName) => ({
  type: 'set_collection_name',
  collectionName,
});
export const toggleIsEditingName = () => ({
  type: 'toggle_is_editing_name',
});
export const setEditingName = (editingName) => ({
  type: 'set_editing_name',
  editingName,
});
export const receiveTweet = (tweet) => ({
  type: 'receive_tweet',
  tweet,
});

如你所见,这些函数返回的 action 对象可以被调度——它们实际上并不调用 dispatcher。当我们开始将 React 组件连接到 Redux store 时,你就会看到为什么会出现这种情况。在 Redux 应用中,action 创建器函数的主要职责是确保返回具有正确 type 属性的对象,以及与 action 相关的属性。例如,addTweetToCollection() action 创建器接受一个 tweet 参数,然后将其作为返回对象的属性返回给 action。

将组件连接到 Application 状态

到目前为止,我们已经有了处理创建新应用状态的 reducer 函数,以及触发 reducer 函数的 action 创建器函数。我们仍然需要将我们的 React 组件连接到 Redux store。在本节中,你将学习如何使用 connect() 函数来创建一个新版本的组件,并连接到 Redux store。

将状态和 Action 创建器映射到 Props

将 Redux 和 React 集成的想法是,你告诉 Redux 用一个有状态组件包装你的组件,当 Redux store 更改时,该组件会设置其 state。我们所要做的就是写一个函数来告诉 Redux 我们希望如何将状态值作为 props 传递给我们的组件。此外,我们必须告诉组件它可能想要调度的任何 action。

以下是我们在连接组件时将遵循的通用模式:

connect(mapStateToProps, mapDispatchToProps)(Component);

下面详细说明了它的工作原理:

  • React- redux 包中的 connect() 函数返回一个新的 React 组件。
  • mapStateToProps() 函数接受一个状态参数,并根据该状态返回一个带有属性值的对象。
  • mapDispatchToProps() 函数接受一个 dispatch() 参数,该参数用于调度 action,并返回一个包含可以调度 action 的函数对象。它们被添加到组件的 props 中。
  • Component 是一个你想要连接到 Redux store 的 React 组件。

当你开始连接组件时,你很快就会意识到 Redux 正在为你处理许多 React 组件生命周期的琐事。通常需要实现 componentDidMount() 功能的地方,突然间你不需要了。React 组件变得干净简洁。

链接 Stream 组件

让我们来看看 Stream 组件:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import StreamTweet from './StreamTweet';
import Header from './Header';
import TweetStore from '../stores/TweetStore';

class `Stream` extends Component {
  render() {
    const { tweet } = this.props;
    const { onAddTweetToCollection } = this.props;
    const headerText = 'Waiting for public photos from Twitter...';
    if (tweet) {
      return <StreamTweet tweet={tweet} />;
    }
    return <Header text={headerText} />;
  }
}

const mapStateToProps = ({ tweet }) => ({ tweet });
const mapDispatchToProps = (dispatch) => ({});

export default connect(mapStateToProps, mapDispatchToProps)(`Stream`);

与之前的实现相比,Stream 并没有太多改变。主要的区别是我们删除了一些生命周期方法。所有的 Redux 连接代码都在组件声明之后。函数 mapStateToProps() 从状态返回 tweet 属性。现在我们的组件有了一个 tweet 属性。mapDispatchToProps() 函数返回一个空对象,因为 Stream 不调度任何动作。当你没有操作时,你实际上不必提供这个函数。然而,这在将来可能会改变,如果函数已经存在,你只需要向对象添加属性。

连接 StreamTweet 组件

Stream 组件渲染 StreamTweet 组件,让我们接下来看一下:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import ReactDOM from 'react-dom';
import Header from './Header';
import Tweet from './Tweet';
import store from '../stores';
import { addTweetToCollection } from '../actions';

class StreamTweet extends Component {
  render() {
    const { tweet, onImageClick } = this.props;
    return (
      <section>
        <Header text="Latest public photo from Twitter" />
        <Tweet tweet={tweet} onImageClick={onImageClick} />
      </section>
    );
  }
}
const mapStateToProps = (state) => ({});
const mapDispatchToProps = (dispatch, ownProps) => ({
  onImageClick: () => {
    dispatch(addTweetToCollection(ownProps.tweet));
  },
});

export default connect(mapStateToProps, mapDispatchToProps)(StreamTweet);

StreamTweet 组件实际上并不使用 Redux store 中的任何状态。那么,为什么有必要连接它吗?答案有必要,这样我们就可以将 action dispatcher 函数映射到组件 props。记住,Redux 应用中的 action 创建器函数只返回 action 对象,而不是调度 action。

在这里的 mapDispatchToProps()函数中,我们通过将 addTweetToCollection() 的返回值传递给 dispatch() 来调度它。Redux 为我们提供了一个简单的 dispatch 函数,绑定到 Redux store。任何时候我们想要 dispatch 一个 action,我们都可以调用 dispatch()。现在 StreamTweet 组件将有一个 onImageClick() 函数 prop,可以用作处理点击事件的事件处理程序。

连接`Collection 组件

现在我们只需要连接 Collection 组件和它的子组件。Collection 组件如下所示:

import React, { Component } from 'react';
import ReactDOMServer from 'react-dom/server';
import { connect } from 'react-redux';
import CollectionControls from './CollectionControls';
import TweetList from './TweetList';
import Header from './Header';
import CollectionUtils from '../utils/CollectionUtils';

class Collection extends Component {
  createHtmlMarkupStringOfTweetList() {
    const { collectionTweets } = this.props;
    const htmlString = ReactDOMServer.renderToStaticMarkup(
      <TweetList tweets={collectionTweets} />
    );
    const htmlMarkup = {
      html: htmlString,
    };
    return JSON.stringify(htmlMarkup);
  }
  render() {
    const { collectionTweets } = this.props;
    const numberOfTweetsInCollection =
      CollectionUtils.getNumberOfTweetsInCollection(collectionTweets);
    let htmlMarkup;
    if (numberOfTweetsInCollection > 0) {
      htmlMarkup = this.createHtmlMarkupStringOfTweetList();
      return (
        <div>
          <CollectionControls
            numberOfTweetsInCollection={numberOfTweetsInCollection}
            htmlMarkup={htmlMarkup}
          />
          <TweetList tweets={collectionTweets} />
        </div>
      );
    }
    return <Header text="Your collection is empty" />;
  }
}
const mapStateToProps = (state) => state.collection;
const mapDispatchToProps = (dispatch) => ({});

export default connect(mapStateToProps, mapDispatchToProps)(Collection);

Collection 组件不调度任何 action,所以我们的 mapDispatchToProps() 函数返回一个空对象。它使用了 Redux store 中的 state,所以我们的 mapStateToProps() 实现返回 state.collection。这就是我们如何将整个应用的状态切分为组件关心的部分。例如,如果我们的组件需要访问 collection 之外的其他状态,我们将返回一个由整个状态的不同切片组成的新对象。

连接集合控件

Collection 组件中,我们有 CollectionControls 组件。让我们看看它连接到 Redux store 后是什么样子:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import Header from './Header';
import Button from './Button';
import CollectionRenameForm from './CollectionRenameForm';
import CollectionExportForm from './CollectionExportForm';
import { toggleIsEditingName, removeAllTweetsFromCollection } from '../actions';

class CollectionControls extends Component {
  getHeaderText = () => {
    const { numberOfTweetsInCollection } = this.props;
    const { collectionName } = this.props;
    let text = numberOfTweetsInCollection;
    if (numberOfTweetsInCollection === 1) {
      text = `${text} tweet in your`;
    } else {
      text = `${text} tweets in your`;
    }
    return (
      <span>
        {text} <strong>{collectionName}</strong> collection
      </span>
    );
  };
  render() {
    const {
      collectionName,
      isEditingName,
      htmlMarkup,
      onRenameCollection,
      onEmptyCollection,
    } = this.props;
    if (isEditingName) {
      return <CollectionRenameForm name={collectionName} />;
    }
    return (
      <div>
        <Header text={this.getHeaderText()} />
        <Button label="Rename collection" handleClick={onRenameCollection} />
        <Button label="Empty collection" handleClick={onEmptyCollection} />
        <CollectionExportForm html={htmlMarkup} title={collectionName} />
      </div>
    );
  }
}
const mapStateToProps = (state) => state.collection;

const mapDispatchToProps = (dispatch) => ({
  onRenameCollection: () => {
    dispatch(toggleIsEditingName());
  },
  onEmptyCollection: () => {
    dispatch(removeAllTweetsFromCollection());
  },
});

export default connect(mapStateToProps, mapDispatchToProps)(CollectionControls);

这次我们有一个组件需要来自 mapStateToProps()mapDispatchToProps() 的对象。我们需要再次将集合状态作为 props 传递给这个组件。onRenameCollection() 事件处理程序调度 toggleIsEditingName() action,而 onEmptyCollection()事件处理程序调度 removeAllTweetsFromCollection() action。

连接 TweetList 组件

最后让我们来看一看 TweetList 组件:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import Tweet from './Tweet';
import { removeTweetFromCollection } from '../actions';

const listStyle = {
  padding: '0',
};
const listItemStyle = {
  display: 'inline-block',
  listStyle: 'none',
};
class TweetList extends Component {
  getListOfTweetIds = () => Object.keys(this.props.tweets);
  getTweetElement = (tweetId) => {
    const { tweets, onRemoveTweetFromCollection } = this.props;
    const tweet = tweets[tweetId];
    return (
      <li style={listItemStyle} key={tweet.id}>
        <Tweet tweet={tweet} onImageClick={onRemoveTweetFromCollection} />
      </li>
    );
  };
  render() {
    const tweetElements = this.getListOfTweetIds().map(this.getTweetElement);
    return <ul style={listStyle}>{tweetElements}</ul>;
  }
}
const mapStateToProps = () => ({});
const mapDispatchToProps = (dispatch) => ({
  onRemoveTweetFromCollection: ({ id }) => {
    dispatch(removeTweetFromCollection(id));
  },
});

export default connect(mapStateToProps, mapDispatchToProps)(TweetList);

这个组件的任何状态都不依赖于 Redux store。但它确实将 action dispatcher 函数映射到它的 props。我们不一定需要在这里连接 dispatcher。例如,如果此组件的父组件将函数连接到 dispatcher,则可以在那里声明该函数并作为 prop 传递到此组件。这样做的好处是 TweetList 完全不再需要 Redux。这样做的缺点是在一个组件中声明了太多的 dispatch 函数。幸运的是,你可以使用任何你认为合适的方法来实现组件。

创建 Store 并连接你的 App

我们几乎完成了我们的 Snapterest 应用的重构,从传统的 Flux 架构,到基于 Redux 的架构。但还有两件事要做。

首先,为了创建一个 store,我们必须将 reducer 函数组合成一个函数:

import { combineReducers } from 'redux';
import collection from './collection';
import tweet from './tweet';

const reducers = combineReducers({
  collection,
  tweet,
});
export default reducers;

使用 combineReducers() 函数来获取两个存在于各自模块中的 reducer 函数,并生成一个 reducer,我们可以使用它来创建 Redux store:

import { createStore } from 'redux';
import reducers from '../reducers';

export default createStore(reducers);

好了,Redux store 创建好了,初始状态默认由 reducer 函数提供。现在我们只需要将这个 store 传递给我们的顶级 React 组件:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import Application from './components/Application';
import { initializeStreamOfTweets } from './utils/WebAPIUtils';
import store from './stores';

initializeStreamOfTweets(store);
ReactDOM.render(
  <Provider store={store}>
    <Application />
  </Provider>,
  document.getElementById('react-application')
);

Provider 组件包装了我们的顶级 Application 组件,并将其与任何依赖于应用状态的子组件一起提供状态更新。

总结

在本章中,你学习了如何使用 Redux 库优化你的 Flux 架构。Redux 应用应该只有一个 store, action 创建器可以很简单,reducer 函数控制不可变状态的转换。简而言之,使用 Redux 的目的是在保留单向数据流的同时,减少传统 Flux 架构中典型的移动部件的数量。

然后使用 Redux 实现了 Snapterest 应用。从 reducers 开始,只要触发一个有效的 action,你就会为 Redux store 返回一个新的 state。然后你构建了 action 创建器函数,它返回一个具有正确类型属性的对象。最后,你重构了组件,使它们连接到 Redux。你确保了组件可以读取存储数据以及调度 action。

这是这本书的结尾。我希望你已经学习了足够的 React 开发要领,以便通过学习更高级的 React 主题来继续你的探索之旅。更重要的是,我希望你通过构建出色的 React 应用来更好学习更多关于 React 的知识。

11

评论 (0)

取消