Skip to content

Latest commit

 

History

History
665 lines (493 loc) · 23.3 KB

File metadata and controls

665 lines (493 loc) · 23.3 KB

十二、用 Redux 完善你的流量应用

上一章介绍了构建在 Flux 体系结构之上的成熟 React 应用的实现。在本章中,您将对此应用进行一些修改,以便它使用 Redux 库来实现 Flux 体系结构。以下是本章的组织方式:

  • Redux 简介
  • 实现控制状态的 reducer 函数
  • 构建 Redux 操作创建者
  • 将组件连接到 Redux 存储
  • 将 Redux 入口点复制到应用的状态

为什么要重复?

在我们开始重构应用之前,我们将花几分钟的时间从高层次上研究 Redux。刚好能刺激你的食欲。准备好的

一家店统统

传统 Flux 应用和 Redux 之间的第一个主要区别是,使用 Redux,您只有一个存储。传统的 Flux 架构可能也只需要一个商店,但它可能有几个商店。您可能认为拥有多个存储实际上可以简化体系结构,因为您可以通过应用的不同部分分离状态。事实上,这是一个很好的策略,但在实践中并不一定有效。创建多个商店可能会导致混乱。商店是您的体系结构中移动的部分;如果你有更多的问题,就更有可能出问题。

Redux 只允许一个存储,从而消除了这个因素。您可能会认为这将导致一个单一的数据结构,这对于各种应用功能来说都很难使用。事实并非如此,因为你可以随心所欲地构建你的店铺。

运动部件少

通过只允许一个商店,Redux 将移动部件从图片中移除。Redux 简化体系结构的另一个地方是消除对专用调度器的需求。在传统的 Flux 体系结构中,dispatcher 是一个独特的组件,它向存储发送消息。由于 Redux 体系结构中只有一个存储,因此您可以直接将操作分派到该存储。换句话说,存储是调度器。

Redux 减少代码中移动部分数量的最后一个地方是事件侦听器。在传统的 Flux 应用中,您必须手动订阅和取消订阅 store 事件,以便正确连接所有内容。这是一个分心时,你可以有一个图书馆处理布线工作为你。这是 Redux 做得很好的事情。

使用最好的焊剂

在传统意义上,Redux 不是 Flux。Flux 有一个规范和一个实现它的库。Redux 不是这个。如前一节所述,Redux 是 Flux 的简化。它保留了所有导致良好应用体系结构的流量概念,同时忽略了可能使流量难以实现并最终难以采用的繁琐内容。

减速器控制状态

Redux 的旗舰概念是,状态由 reducer 函数控制。在本节中,我们将让您了解减速机是什么,然后在您的 Snapterest 应用中实现减速机功能。

什么是减速器?

减缩器是接受数据收集(如对象或数组)并返回新收集的函数。返回的集合中可以包含相同的数据,也可以包含与初始集合完全不同的数据。在 Redux 应用中,reducer 函数获取一个状态片,并返回一个新的状态片。就这样!您刚刚了解了 Redux 体系结构的关键。现在让我们看看减速机函数的作用。

Redux 应用中的 Reducer 函数可以分为表示应用状态部分的模块。我们将看看收集缩减器,然后是 Snapterest 应用的推特缩减器。

收集异径管

现在,让我们通过改变应用状态的部分的 collection 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;
  }
};

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

向收藏添加推文

让我们看一看,在动作中:

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

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

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

然后我们返回新状态。重要的是要记住,这应该始终是一个新对象,而不是对其他对象的引用。这就是在 Redux 应用中避免意外副作用的方法。谢天谢地,我们可以使用对象扩展操作符来简化此任务。

从收藏中删除推文

collectionTweets对象中删除 tweet 意味着我们必须删除具有要删除的tweetID 的密钥。让我们看看这是如何做到的:

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

  return {
    ...state,
    collectionTweets
  };

注意我们是如何为collectionTweets变量分配一个新对象的?同样,为了避免额外的语法,spread 操作符在这里很方便。我们这样做的原因是,减速器总是返回一个新的引用。一旦我们从collectionTweets对象中删除 tweet,我们就可以返回包含collectionTweets作为属性的新状态对象。

另一个 tweet 删除操作是remove_all_tweets_from_collection。下面是它的样子:

case 'remove_all_tweets_from_collection':
  collectionTweets = {};

  return {
    ...state,
    collectionTweets
  };

删除所有 tweet 意味着我们可以用一个新的空对象替换collectionTweets值。

设置集合名称

当 tweets 集合被重命名时,我们必须更新 Redux 存储。这是通过从set_collection_name动作被调度时的状态获取editingName来完成的:

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

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

编辑收藏名称

您刚才看到了用户保存更改后如何设置集合名称。然而,当涉及到 Redux 商店中的跟踪状态时,编辑文本还有更多的工作要做。首先,我们必须首先使文本能够被编辑;这为用户提供了某种视觉提示:

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

然后是用户在文本输入中主动输入的文本。这也必须放在商店的某个地方:

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

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

高音减音器

tweet reducer 只需要处理一个操作,但这并不意味着我们不应该在自己的模块中使用 tweet reducer 来预测 tweet 的未来操作。现在,让我们只关注我们的应用目前的功能。

接收推文

让我们看一下 Twitter 减法代码中的处理 Tyt T0 的动作:

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

这个减速机非常简单。当receive_tweet动作被调度时,action.tweet值作为新状态返回。因为这是一个小的 reducer 函数,所以它可能是指出所有 reducer 函数通用的一些东西的好地方。

传递给 reducer 函数的第一个参数是旧状态。此参数有一个默认值,因为第一次调用 reducer 时没有状态,此值用于初始化它。在这种情况下,默认状态为 null。

关于 reducer,需要指出的第二点是,它们在被调用时总是返回一个新的状态。即使它不产生任何新状态,reducer 函数也需要返回旧状态。Redux 将把新状态设置为 reducer 返回的任何状态,即使您返回 undefined。这就是为什么在你的switch陈述中有default标签是个好主意。

简化动作创造者

在 Redux 中,action creator 函数比传统的 Flux 函数简单。主要区别在于 Redux action creator 函数只返回动作数据。在传统的 Flux 中,动作创建者还负责调用调度程序。让我们看看 Snapterest 的 ReDux 动作创建者函数:

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
});

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

将组件连接到应用状态

到目前为止,我们有处理创建新应用状态的 reducer 函数,以及触发 reducer 函数的 action creator 函数。我们仍然需要将 React 组件连接到 Redux 商店。在本节中,您将学习如何使用connect()函数创建连接到 Redux 应用商店的组件的新版本。

将状态和动作创建者映射到道具

Redux 和 React 集成的思想是,您告诉 Redux 使用有状态组件包装您的组件,该组件在 Redux 存储更改时设置了状态。我们所要做的就是编写一个函数,告诉 Redux 我们希望如何将状态值作为道具传递给我们的组件。此外,我们必须告诉组件它可能要分派的任何操作。

以下是连接组件时我们将遵循的一般模式:

connect(
  mapStateToProps,
  mapDispatchToProps
)(Component);

下面是这项工作原理的分解:

  • React-Redux 包中的connect()函数返回一个新的 React 组件。
  • mapStateToProps()函数接受一个状态参数,并返回一个具有基于此状态的属性值的对象。
  • mapDispatchToProps()函数接受一个dispatch()参数,该参数用于分派操作,并返回一个包含可以分派操作的函数的对象。这些将添加到组件的道具中。
  • Component是要连接到 Redux 存储的 React 组件。

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

连接流组件

让我们先看一下:

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 组件

Apple T0.组件 Type T2A.呈现了 Tyl T1 个组件,所以让我们来看看下一个:

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 存储中的任何状态。那么为什么要费心连接它呢?答案是,这样我们就可以将动作调度器功能映射到组件道具。记住,Redux 应用中的 action creator 函数只返回 action 对象,而不是调度 action。

在这里的mapDispatchToProps()函数中,我们通过将其返回值传递给dispatch()来调度addTweetToCollection()操作。Redux 为我们提供了一个绑定到 Redux 存储的简单分派函数。任何时候我们想发送一个动作,我们可以直接呼叫dispatch()。现在,StreamTweet组件将有一个onImageClick()函数 prop,可以用作事件处理程序来处理点击事件。

连接采集组件

现在我们只有来连接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组件不分派任何操作,因此我们的mapDispatchToProps()函数返回一个空对象。它确实使用 Redux 存储中的状态,因此我们的mapStateToProps()实现返回state.collection。这就是我们如何将整个应用的状态分割成组件关心的部分。例如,如果我们的组件需要访问Collection之外的其他状态,我们将返回一个由整体状态的不同部分组成的新对象。

连接采集控制

Collection组件中,我们有CollectionControls组件。让我们看看连接到 Redux 商店后的样子:

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()的对象。再一次,我们需要将集合状态作为道具传递给这个组件。onRenameCollection()事件处理程序分派toggleIsEditingName()操作,onEmptyCollection()事件处理程序分派removeAllTweetsFromCollection()操作。

连接 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 存储。不过,它确实将动作调度器功能映射到了它的道具上。我们不需要在这里连接调度员。例如,如果该组件的父组件正在将函数连接到 dispatcher,则可以在那里声明该函数并将其作为 prop 传递到该组件中。好处是TweetList不再需要 Redux。缺点是在一个组件中声明的分派函数太多。幸运的是,您可以使用您认为合适的方法来实现您的组件。

创建店铺并连接应用

我们几乎已经完成了对 Snapterest 应用的重构,从一个传统的 Flux 架构,到一个基于 Redux 的架构。只剩下两件事要做了。

首先,我们必须将我们的 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 存储:

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

export default createStore(reducers);

在这里,我们创建了 Redux 存储,默认情况下,reducer 函数提供了它的初始状态。现在,我们只需将此存储传递给顶级 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组件包装我们的顶级应用组件,并提供它以及依赖于应用状态的任何子组件,以及状态更新。

总结

在本章中,您学习了如何使用 Redux 库优化 Flux 体系结构。Redux 应用应该只有一个存储区,操作创建者可以很简单,并且 reducer 函数控制不变状态的转换。简而言之,Redux 的目标是减少传统 Flux 体系结构中通常存在的移动部件的数量,同时保留单向数据流。

然后使用 Redux 实现 Snapterest 应用。从 reducer 开始,每当调度有效操作时,您都会返回 Redux 存储的新状态。然后,您构建了返回具有正确类型属性的对象的 action creator 函数。最后,重构组件,以便将它们连接到 Redux。您确保组件可以读取存储数据以及分派操作。

这是这本书的包装纸。我希望您已经充分了解了 React 开发的要点,可以通过学习更高级的 React 主题继续您的探索之旅。更重要的是,我希望您通过构建很棒的 React 应用,然后改进它们,从而更多地了解 React。