使用Node.JS,React,Redux和Redux-Saga Part2:Redux集成构建Retrogames存档。

In the previous tutorial we built the retrogames archive app and successfully made it work.

在上一个教程中,我们构建了retrogames存档应用程序并成功使之运行。

Actually, for a small project like this, adding Redux may increase the overall complexity of the code without real benefits. At most we could do some refactoring, create some utils functions and so on. However we can consider the project as the base for a more complex one so Redux improves the overall development experience as well as the code organization:

实际上,对于像这样的小型项目,添加Redux可能会增加代码的整体复杂性而没有真正的好处。 最多我们可以进行一些重构,创建一些utils函数,等等。 但是,我们可以将项目视为更复杂项目的基础,因此Redux可以改善总体开发经验以及代码组织:

By decoupling the state from the our components we will see immediate benefits in terms of readibilty, moreover we have a precise picture of the current state. No more non-deterministic state means easy debugging as well!

通过将状态从我们的组件中分离出来,我们将在准备就绪性方面看到直接的好处,而且,我们对当前状态有了一个精确的了解。 没有更多的不确定状态也意味着易于调试!

In few words I summarized a few reasons why I like Redux and why I use it in my apps:

用几句话概括了为什么我喜欢Redux以及为什么在应用程序中使用它的一些原因:

  • Decouple the state from the components. By connecting the containers to the redux store I can get the data I need and pass it as props to presentational components. This helps readibility.

    使状态与组件分离。 通过将容器连接到redux存储,我可以获得所需的数据并将其作为道具传递给表示性组件。 这有助于提高可读性。
  • unidirectional data-flow rocks!

    单向数据流的岩石!
  • Having a single state means having a single source of truth.

    有一个单一的状态意味着有一个单一的真理来源。
  • It's developer friendly, with redux-dev-tools debugging my code is easier and faster.

    它对开发人员友好,使用redux-dev-tools调试我的代码更加轻松快捷。
  • I can do time travelling by deleting previously dispatched actions.

    我可以通过删除以前调度的动作来进行时间旅行。
  • Redux comes with great documentation.

    Redux附带了出色的文档

For more, take a look a the documentation on "when should I use Redux?".

有关更多信息,请查看有关“何时应使用Redux?”的文档。 。

先决条件 (Prerequisites)

  • The most obvious is having the code of the part.1 that you can grab on my github.

    最明显的是拥有part.1的代码,您可以在我的github上获取它。
  • The prerequisites of the part.1 are also still valid, plus I assume some basic knowledge of Redux and Immutability.

    part.1的前提条件仍然有效,此外,我还假设了Redux和Immutability的一些基本知识。

Regarding the project, if you want to start from the part1 and update the project incrementally, you can get the code on github and checkout to tutorial/part1 branch to start editing the code. On the other hand you can just checkout to tutorial/part2 branch instead to get the exact code of this tutorial.

关于项目,如果要从part1开始并逐步更新项​​目,则可以在github上获取代码,并签出到tutorial / part1分支以开始编辑代码。 另一方面,您可以直接检出tutorial / part2分支,以获得本教程的确切代码。

目录 ( Table of Contents )

资料夹结构 ( Folder Structure )

That's the folder structure for the Part.2:

那是Part.2的文件夹结构:

--app
 ----models
 ------game.js
 ----routes
 ------game.js
 --client
 ----dist
 ------css
 --------style.css
 ------fonts
 --------PressStart2p.ttf
 ------index.html
 ------bundle.js
 ----src
 ------actions
 --------filestack.js
 --------games.js
 ------components
 --------About.jsx
 --------Archive.jsx
 --------Contact.jsx
 --------Form.jsx
 --------Game.jsx
 --------GamesListManager.jsx
 --------Home.jsx
 --------index.js
 --------Modal.jsx
 --------Welcome.jsx
 ------constants
 --------filestack.js
 --------games.js
 ------containers
 --------AddGameContainer.jsx
 --------GamesContainer.jsx
 --------reducers
 ----------filestack.js
 ----------games.js
 ----------index.js
 --------sagas
 ----------filestack.js
 ----------games.js
 ----------index.js
 ------index.js
 ------routes.js
 ------store.js
 --.babelrc
 --package.json
 --server.js
 --webpack-loaders.js
 --webpack-paths.js
 --webpack.config.js
 --yarn.lock

重写GamesContainer ( Rewrite GamesContainer )

To better understand the process of integrating Redux in our app we can start from the games list view.

为了更好地了解将Redux集成到我们的应用中的过程,我们可以从游戏列表视图开始。

Take a look at GamesContainer function getGames:

看一下GamesContainer函数getGames

/* ... code */
// This is the fetch code directly in the container
getGames () {
    fetch('http://localhost:8080/games', {
      headers: new Headers({
        'Content-Type': 'application/json'
      })
    })
    .then(response => response.json())
    .then(data => this.setState({ games: data }));
  }
  /* ... code */

We are making an asynchronous call to the server to fetch our games and then put them in the state. Thus, this is a perfect candidate for our purpose.

我们正在异步调用服务器以获取游戏,然后将其置于状态。 因此,这是达到我们目的的理想人选。

Let's think about the new state structure, here is an initial draft the games list only:

让我们考虑一下新的状态结构,这只是游戏列表的初稿:

games: { 
    list : [
        {//...Game1},
        {//...Game2},
        ...
    ]
}

We added one more level (list) compared to the original one, this is will come handy once our state grows.

与原始级别相比,我们又添加了一个级别(列表),一旦我们的国家发展起来,这将很方便。

We need to install a few packages, let's start from Redux and Immutable (Our state will be an immutable data-structure):

我们需要安装一些软件包,让我们从ReduxImmutable开始(我们的状态将是一个不可变的数据结构):

yarn add redux immutable

动作 (Actions)

First, we write the actions so create the action folder in /client/src and inside create new a file called games.js which contains our action creators. Then, paste the following code:

首先,我们编写动作,因此在/client/src创建动作文件夹,并在内部创建一个名为games.js新文件,其中包含我们的动作创建者。 然后,粘贴以下代码:

// We import the constants from a /constants/games
import {
  GET_GAMES,
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE
} from '../constants/games';

// GET_GAMES function will be dispatched within GamesContainer
function getGames () {
  return {
    type: GET_GAMES
  };
}

/* After fetching form the server this action is intercepted by the reducer and the games added to the state */
function getGamesSuccess (games) {
  return {
    type: GET_GAMES_SUCCESS,
    games
  };
}

// A failure action is sent in case of server errors
function getGamesFailure () {
  return {
    type: GET_GAMES_FAILURE
  };
}

// we export all the function in a single export command
export {
  getGames,
  getGamesSuccess,
  getGamesFailure
};

The three functions are action creators which returns a plain object description of the action which is going to be executed by the store:

这三个函数是动作创建者,它们返回商店将要执行的动作的简单对象描述:

  • getGames will be run in componentDidMount of GamesContainer and requires the HTTP request to the server to fetch the games list. Since we are talking about async requests (the HTTP request), the reducer won't be involved in this, instead we are going to write a specific saga to deal with it.

    getGames将在GamesContainer的 componentDidMount中运行,并且需要服务器的HTTP请求来获取游戏列表。 由于我们正在谈论异步请求(HTTP请求),因此reducer不会涉及到它,相反,我们将编写一个特定的传奇来处理它。
  • getGamesSuccess and getGamesFailure returns actions digested by the reducer function once the HTTP request terminates as we want to handle both cases, success and failure. Besides, the first one carries the games list received from the server as second property.

    一旦HTTP请求终止, getGamesSuccessgetGamesFailure返回由reducer函数消化的操作,因为我们要处理成功和失败两种情况。 此外,第一个携带从服务器接收到的游戏列表作为第二个属性。

常数 (Constants)

At the top of the file we imported three constants to define the actions type, so let's create that file now. Create the /constants folder in /client/src and create a new file games.js inside of it. Then, paste the following code:

在文件的顶部,我们导入了三个常量以定义操作类型,因此,让我们现在创建该文件。 在/client/src创建/constants文件夹,并在其中创建一个新文件games.js 。 然后,粘贴以下代码:

/* the constants are imported in the sagas, reducers and action files so it's very convenient to have a 'centralized' file for them. */
const GET_GAMES = 'GET_GAMES';
const GET_GAMES_SUCCESS = 'GET_GAMES_SUCCESS';
const GET_GAMES_FAILURE = 'GET_GAMES_FAILURE';

export {
  GET_GAMES,
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE
};

While using constants for the actions type is not necessary I recommend a similar code organization for larger apps.

尽管没有必要为操作类型使用常量,但我建议大型应用程序使用类似的代码组织。

减速器 (Reducer)

Now we have some actions but we need the reducer function to receive them and return a new state accordingly. Let's create a file games.js in /clients/src/reducers and paste the following code:

现在我们有了一些动作,但是我们需要reducer函数来接收它们并相应地返回新状态。 让我们在/clients/src/reducers创建一个文件games.js并粘贴以下代码:

import Immutable from 'immutable';
// Here the constants file comes handy
import {
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE
} from '../constants/games';

// The initial state is just an empty Map
const initialState = Immutable.Map();

// That's a very standard reducer function to return a new state given a dispatched action
export default (state = initialState, action) => {
  switch (action.type) {
  // GET_GAMES_SUCCESS case return a new state with the fetched games in the state
    case GET_GAMES_SUCCESS: {
      return state.merge({ list: action.games });
    }
  // In case of failure it simplies returned a new empty state
    case GET_GAMES_FAILURE: {
      return state.clear();
    }
    default:
      return state;
  }
}

Here it is our pure reducer, which receives the current state and an action and returns a new state. In case the state is not passed as parameter, the initialState is considered instead. As a rule, in case the action.type is unhandled (so no case for it), the reducer just the returns the current state.

这是我们的纯归约器,它接收当前状态和操作并返回新状态。 如果没有将状态作为参数传递,则考虑使用initialState 。 作为一项规则,以防action.type是未处理的(所以没有它的情况下 ),减速刚刚返回当前状态。

Now, the entire app must have a single reducer, however we can split the logic in several functions and then combine them all. We created one for now but we can forecast we will have others as well: For instance, the form may have another one!

现在,整个应用程序必须具有一个简化器,但是我们可以将逻辑拆分为多个功能,然后将它们全部组合。 我们现在创建了一个表单,但是我们可以预测还会有其他表单:例如,表单可能还有另一个表单!

The important thing to keep in mind is that the reducer function must be one when we create the store, so we need a mechanism to merge them into a single one. Luckily we can use combineReducers from Redux-immutable to achieve this!

要记住的重要一点是,在创建商店时,reducer函数必须为一个,因此我们需要一种将它们合并为单个函数的机制。 幸运的是,我们可以使用Redux-immutable中的 combineReducers实现这一目标!

NB: We actually need Redux-immutable to have an equivalent function to Redux combineReducers that deals with immutability. If the state wasn't an immutable data-structure we wouldn't need it.

注意 :实际上,我们需要Redux-immutable具有与Redux combineReducers等效的功能,该功能可处理不变性。 如果状态不是一成不变的数据结构,我们将不需要它。

Let's add the package:

让我们添加包:

yarn add redux-immutable

In /client/src/reducers create index.js and paste the following code:

/client/src/reducers创建index.js并粘贴以下代码:

// We import the combineReducers function
import { combineReducers } from 'redux-immutable';
// Import our reducers function from here
import games from './games'; 

// combineReducers merges them all!
export default combineReducers({
  games
});

For now it is just games but we will soon have others.

目前这只是游戏,但我们很快就会推出其他游戏。

萨加斯 (Sagas)

It is time to handle async requests and to do so we are using Redux-saga:

现在该处理异步请求了,为此,我们使用Redux-saga

yarn add redux-saga

Sagas are implemented by generator functions which are transpiled thanks to Babel-polyfill. Let's install it:

Sagas由生成器功能实现,而这些功能由于Babel- polyfill而得以转译。 让我们安装它:

yarn add babel-polyfill --dev

And we have to modify the entry of the common config object in webpack.config.js:

而且我们必须修改webpack.config.js公共配置对象的webpack.config.js

/* ...code */
entry: {
    app: ['babel-polyfill', PATHS.src]
},
/* ...code */

Now, let's create games.js in /client/src/sagas and paste the following code:

现在,让我们在/client/src/sagas创建games.js并粘贴以下代码:

// Import a saga helper
import {
    takeLatest
} from 'redux-saga';
// Saga effects are usesul to interact with the saga middleware
import {
    put,
    call
} from 'redux-saga/effects';
// As predicted a saga will take care of GET_GAMES actions
import {
  GET_GAMES
} from '../constants/games';
// either one is yielded once the fetch is done
import { getGamesSuccess, getGamesFailure } from '../actions/games';

// We moved the fetch from GamesContainer
const fetchGames = () => {
  return fetch('http://localhost:8080/games', {
    // Set the header content-type to application/json
    headers: new Headers({
      'Content-Type': 'application/json'
    })
  })
  .then(response => response.json())
};

// yield call to fetchGames is in a try catch to control the flow even when the promise rejects
function* getGames () {
  try {
    const games = yield call(fetchGames);
    yield put(getGamesSuccess(games));
  } catch (err) {
    yield put(getGamesFailure());
  }
}

// The watcher saga waits for dispatched GET_GAMES actions
function* watchGetGames () {
  yield takeLatest(GET_GAMES, getGames);
}

// Export the watcher to be run in parallel in sagas/index.js
export { 
    watchGetGames
};
  • We have created a watcher saga watchGetGames which spawns getGames on every action dispatched whose action.type is 'GET_GAMES'. We use takeLatest so it will cancel previous running tasks.

    我们创建了一个watcher saga watchGetGames ,它在分派的每个action.type为'GET_GAMES'的动作上生成getGames 我们使用takeLatest这样它将取消以前的运行任务。
  • getGames is gonna yield call(fetchGames) to the saga middleware and wait to the promise to resolve. This suspends the saga till the promise is resolved. We added a try-catch surrounding the instructions to make sure that in case the promise gets rejected we handle the situation and dispatch an failure action to the reducer.

    getGames将让saga中间件yield call(fetchGames)并等待承诺解决。 这将中止传奇,直到诺言得以解决。 我们在指令周围添加了尝试捕获功能,以确保在诺言被拒绝的情况下,我们可以处理这种情况,并将失败操作发送给减速器。

NB: We could have written const games = yield call(fetchGames) in this way:

注意 :我们可以这样编写const games = yield call(fetchGames)

const games = yield fetchGames()

However call creates a plain object describing the effect which makes it easy to test. For a deeper discussion spend some time reading the documentation.

但是, call会创建一个描述效果的简单对象,使测试变得容易。 为了进行更深入的讨论,请花一些时间阅读文档

In our app we want to start all the sagas at once so we need a function rootSaga to start them all. Let's create index.js in /client/src/sagas and paste the following code:

在我们的应用程序中,我们想一次启动所有的sagas,因此我们需要一个函数rootSaga来启动它们。 让我们在/client/src/sagas创建index.js并粘贴以下代码:

// Import the watcher we have just created
import {
  watchGetGames
} from './games';

export default function* rootSaga () {
// We start all the sagas in parallel
  yield [
    watchGetGames()
  ];
}

This yields an array with the results of starting all the sagas inside (just one for now).

这将产生一个数组,其中包含启动所有内部sagas的结果(目前仅一个)。

The final step is to create the Saga middleware and connect it to the redux store but actually we have no store yet.

最后一步是创建Saga中间件并将其连接到redux商店,但实际上我们还没有商店。

App Store商店 (The App Store)

Let's solve this immediately, create store.js in /client/src and paste the following code:

让我们立即解决此问题,在/client/src创建store.js并粘贴以下代码:

// We import Redux and Redux-saga dependencies
import {
  createStore,
  applyMiddleware
} from 'redux';
import createSagaMiddleware from 'redux-saga';
// this comes from our created files
import rootSaga from './sagas';
import reducer from './reducers';

// The function in charge of creating and returning the store of the app
const configureStore = () => {
  const sagaMiddleware = createSagaMiddleware();
  // The store is created with a reducer parameter and the saga middleware
  const store = createStore(
    reducer,
    applyMiddleware(sagaMiddleware)
  );
  // rootSaga starts all the sagas in parallel
  sagaMiddleware.run(rootSaga);

  return store; // Return the state 
}
export default configureStore;

We defined a function configureStore which does the following:

我们定义了一个功能configureStore ,它执行以下操作:

  • Creates the store passing the reducer and the middleware.

    创建通过减速器和中间件的商店。
  • Start the sagaMiddleware by calling the run function.

    通过调用运行功能启动sagaMiddleware
  • Return the state.

    返回状态。

添加提供者组件 (Add the Provider Component)

At this point we need GamesContainer to access the store and subscribe to it. We can do so thanks to the React-redux component Provider.

至此,我们需要GamesContainer来访问商店并进行订阅。 感谢React-redux组件Provider我们可以做到这一点。

First install the package:

首先安装软件包:

yarn add react-redux

Then, in /client/src/routes.js replace the code with the following:

然后,在/client/src/routes.js将代码替换为以下代码:

import React from 'react';
// We import Provider
import { Provider } from 'react-redux';
// We need the store to be passed to Provider
import configureStore from './store';
// All the previous dependencies from Part1
import { Router, Route, hashHistory, IndexRoute } from 'react-router';
import { AddGameContainer, GamesContainer } from './containers';
import { Home, Archive, Welcome, About, Contact } from './components';

// Call the configureStore function previously exported
const store = configureStore();

// Provider wraps our root component
const routes = (
{/* We pass the store to the provider */}
  <Provider store={store}>
    <Router history={hashHistory}>
      <Route path="/" component={Home}>
        <IndexRoute component={Welcome} />
        <Route path="/about" component={About} />
        <Route path="/contact" component={Contact} />
      </Route>
      <Route path="/games" component={Archive}>
        <IndexRoute component={GamesContainer} />
        <Route path="add" component={AddGameContainer} />
      </Route>
    </Router>
  </Provider>
);

export default routes;

By wrapping our root component Provider we make the store available to all the components.

通过包装我们的根组件Provider我们使商店对所有组件都可用。

As final step GamesContainer is required to dispatch actions and read from the state. React-redux also provides a function connect which creates this connection and allow us to export a smart container instead.

作为最后一步,需要GameContainer分发动作并从状态读取信息。 React-redux还提供了一个函数connect ,该连接创建了此连接,并允许我们导出一个智能容器。

连接的游戏容器组件 (Connected GamesContainer Component)

Let's take a look at the updated code for /client/src/containers/GamesContainer.js:

让我们看一下/client/src/containers/GamesContainer.js的更新代码:

import React, { Component } from 'react';
 // We import connect from react-redux
import { connect } from 'react-redux';
// bindActionCreators comes handy to wrap action creators in dispatch calls
import { bindActionCreators } from 'redux';
import Immutable from 'immutable';
import { Modal, GamesListManager } from '../components';
// we import the action-creators to be binde with bindActionCreators
import * as gamesActionCreators from '../actions/games';

// We do not export GamesContainer as it is 'almost' a dumb component
class GamesContainer extends Component {
  constructor (props) {
    super();
    // For now we still initialize the state
    this.state = { selectedGame: {}, searchBar: '' };
    this.toggleModal = this.toggleModal.bind(this);
    this.deleteGame = this.deleteGame.bind(this);
    this.setSearchBar = this.setSearchBar.bind(this);
  }

  componentDidMount () {
    this.getGames();
  }

  toggleModal (index) {
    this.setState({ selectedGame: this.state.games[index] });
    $('#game-modal').modal();
  }
// GET_GAMES is now dispatched and intercepted by the saga watcher 
  getGames () {
    this.props.gamesActions.getGames();
  }

  deleteGame (id) {
    fetch(`http://localhost:8080/games/${id}`, {
      headers: new Headers({
        'Content-Type': 'application/json',
      }),
      method: 'DELETE',
    })
    .then(response => response.json())
    .then(response => {
      this.setState({ games: this.state.games.filter(game => game._id !== id) });
      console.log(response.message);
    });
  }

  setSearchBar (event) {
    this.setState({ searchBar: event.target.value.toLowerCase() });
  }

  render () {
    const { selectedGame, searchBar } = this.state;
    const { games  } = this.props;
    console.log(games);
    return (
      <div>
        <Modal game={selectedGame} />
        <GamesListManager
          games={games}
          searchBar={searchBar}
          setSearchBar={this.setSearchBar}
          toggleModal={this.toggleModal}
          deleteGame={this.deleteGame}
        />
      </div>
    );
  }
}

// We can read values from the state thanks to mapStateToProps
function mapStateToProps (state) {
  return { // We get all the games to list in the page
    games: state.getIn(['games', 'list'], Immutable.List()).toJS()
  }
}
// We can dispatch actions to the reducer and sagas
function mapDispatchToProps (dispatch) {
  return {
    gamesActions: bindActionCreators(gamesActionCreators, dispatch)
  };
}
// Finally we export the connected GamesContainer
export default connect(mapStateToProps, mapDispatchToProps)(GamesContainer);
  • mapStateToProps is a function with the state as parameter: It returns an object that gives our container access to the state information as props. In this case the games list will be available through this.props.games.

    mapStateToProps是一个以状态为参数的函数:它返回一个对象,该对象使我们的容器可以访问作为道具的状态信息。 在这种情况下,可以通过this.props.games获得游戏列表。
  • mapDispatchToProps allows our container to dispatch actions. We also need bindActionCreators which makes our action creators wrapped into a dispatch call. Through the gamesActions object GamesContainer can now call getGames action creator.

    mapDispatchToProps允许我们的容器调度动作。 我们还需要bindActionCreators ,它使我们的动作创建者包装在一个调度调用中。 通过gamesActions对象GamesContainer现在可以调用getGames动作创建者。
  • Take a look at the GamesContainer getGames function: This is now just a single line where we call the action creator function. Our saga will intercept the action and fetch data from the server!

    看一下GamesContainer的 getGames函数:现在这只是一行,我们称之为动作创建者函数。 我们的传奇将拦截动作并从服务器获取数据!
  • In the constructor we still initialize the state (for now) but we get the games array from our state so we deleted it from the initialization.

    在构造函数中,我们仍然初始化状态(现在),但是我们从状态中获取游戏数组,因此从初始化中将其删除。

Let's see if it still works..

让我们看看它是否仍然有效。

To run the server:

要运行服务器:

yarn api

And to run webpack-dev-server:

并运行webpack-dev-server:

yarn start

Once we connect to http://localhost:3000 here is the result:

连接到http:// localhost:3000后 ,结果如下:

Great it works. We went through all the steps to include redux in our app so now we can apply it wherever it is possible.

伟大的作品。 我们完成了所有步骤,将redux包含在我们的应用程序中,因此现在我们可以在任何可能的地方应用它。

The search bar is another good candidate as we save the keyword in the state. Plus, we are not required to use Redux-saga in this case.

搜索栏是另一个很好的候选者,因为我们将关键字保存在状态中。 另外,在这种情况下,我们不需要使用Redux-saga

As we did before, let's picture our state structure to include the search bar keyword:

像以前一样,让我们​​描述一下状态结构,以包括搜索栏关键字:

games: { 
    list : [
        {//...Game1},
        {//...Game2},
        ...
    ],
    searchBar: ''
}

At the same level as the games list makes sense doesn't it?

在与游戏列表相同的级别上有意义吗?

Let's edit /client/src/actions/games.js to include the new action creator:

让我们编辑/client/src/actions/games.js以包括新的动作创建者:

// A new constant SET_SEARCH_BAR
import {
  GET_GAMES,
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE,
  SET_SEARCH_BAR
} from '../constants/games';


function getGames () {
  return {
    type: GET_GAMES
  };
}

function getGamesSuccess (games) {
  return {
    type: GET_GAMES_SUCCESS,
    games
  };
}

function getGamesFailure () {
  return {
    type: GET_GAMES_FAILURE
  };
}

// setSearchBar action-creator has a payload, the keyword typed by the users
function setSearchBar (keyword) {
  return {
    type: SET_SEARCH_BAR,
    keyword
  };
}

export {
  getGames,
  getGamesSuccess,
  getGamesFailure,
  setSearchBar // We export the new action-creators
};
  • The new action has type SET_SEARCH_BAR and carries the keyword to filter the games.

    新动作的类型为SET_SEARCH_BAR,并带有关键字以过滤游戏。
  • As did before, let's create the constant, so edit /client/src/constants/games.js:

    和以前一样,让我们​​创建常量,所以编辑/client/src/constants/games.js

    const GET_GAMES = 'GET_GAMES';
    const GET_GAMES_SUCCESS = 'GET_GAMES_SUCCESS';
    const GET_GAMES_FAILURE = 'GET_GAMES_FAILURE';
    // The new constant
    const SET_SEARCH_BAR = 'SET_SEARCH_BAR';
    
    export {
      GET_GAMES,
      GET_GAMES_SUCCESS,
      GET_GAMES_FAILURE,
      SET_SEARCH_BAR // We export it too
    };
    

    Our reducer switch requires a new case. Let's edit /client/src/reducers/games.js:

    我们的减速机开关需要一个新的情况。 让我们编辑/client/src/reducers/games.js

    import Immutable from 'immutable';
    import {
      GET_GAMES_SUCCESS,
      GET_GAMES_FAILURE,
      SET_SEARCH_BAR
    } from '../constants/games';
    
    const initialState = Immutable.Map();
    
    export default (state = initialState, action) => {
      switch (action.type) {
        case GET_GAMES_SUCCESS: {
          return state.merge({ list: action.games });
        }
        // The reducer can now set the searchBar content into the state
        case SET_SEARCH_BAR: {
          return state.merge({ searchBar: action.keyword });
        }
        case GET_GAMES_FAILURE: {
          return state.clear();
        }
        default:
          return state;
      }
    }
    

    Again, we merge the state with the current searchBar content.

    同样,我们将状态与当前searchBar内容合并。

    Finally, it's time to edit GamesContainer:

    最后,是时候编辑GamesContainer了

    import React, { Component } from 'react';
    import { connect } from 'react-redux';
    import { bindActionCreators } from 'redux';
    import Immutable from 'immutable';
    import { Modal, GamesListManager } from '../components';
    import * as gamesActionCreators from '../actions/games';
    
    class GamesContainer extends Component {
      constructor (props) {
        super(props);
       // We removed the searchBar initialization
        this.state = { selectedGame: {} };
        this.toggleModal = this.toggleModal.bind(this);
        this.deleteGame = this.deleteGame.bind(this);
        this.setSearchBar = this.setSearchBar.bind(this);
      }
    
      componentDidMount () {
        this.getGames();
      }
    
      toggleModal (index) {
        this.setState({ selectedGame: this.state.games[index] });
        $('#game-modal').modal();
      }
    
      getGames () {
        this.props.gamesActions.getGames();
      }
    
      deleteGame (id) {
        fetch(`http://localhost:8080/games/${id}`, {
          headers: new Headers({
            'Content-Type': 'application/json',
          }),
          method: 'DELETE',
        })
        .then(response => response.json())
        .then(response => {
          this.setState({ games: this.state.games.filter(game => game._id !== id) });
          console.log(response.message);
        });
      }
    // It now dispatches the action and pass the search bar content as parameter
      setSearchBar (event) {
        this.props.gamesActions.setSearchBar(event.target.value.toLowerCase());
      }
    
      render () {
      {// we take games and searchBar from props now}
        const { selectedGame } = this.state;
        const { games, searchBar } = this.props;
        console.log(games);
        return (
          <div>
            <Modal game={selectedGame} />
            <GamesListManager
              games={games}
              searchBar={searchBar}
              setSearchBar={this.setSearchBar}
              toggleModal={this.toggleModal}
              deleteGame={this.deleteGame}
            />
          </div>
        );
      }
    }
    
    function mapStateToProps (state) {
      return {
        games: state.getIn(['games', 'list'], Immutable.List()).toJS(),
        searchBar: state.getIn(['games', 'searchBar'], '') // We retrieve the searchBar content too
      }
    }
    
    function mapDispatchToProps (dispatch) {
      return {
        // setSearchBar gets binded too
        gamesActions: bindActionCreators(gamesActionCreators, dispatch)
      };
    }
    export default connect(mapStateToProps, mapDispatchToProps)(GamesContainer);
    
    • in mapStateToProps we retrieve the current value of the search bar from the state which is now accessible within the component at this.props.searchBar.

      mapStateToProps我们从状态中检索搜索栏的当前值,该状态现在可在this.props.searchBar组件内this.props.searchBar
    • mapDispatchToProps doesn't change as it's already an object whose properties are the exported action creators.

      mapDispatchToProps不会更改,因为它已经是一个对象,其属性是导出的动作创建者。
    • The GamesContainer setSearchBar function now dispatches the action to the reducer passing the current search bar value.

      现在,GamesContainer setSearchBar函数会将setSearchBar分派给传递当前搜索栏值的减速器。
    • In the constructor we removed the initiliaziation of the keyword.

      在构造函数中,我们删除了关键字的初始化。

    Let's take a look at the result in the browser:

    让我们看看浏览器中的结果:

    If you try to click "view" on any game you receive an error, we need to modify our modal behavior! The process is very similar to the search bar one.

    如果您尝试在任何游戏上单击“查看”,则会收到错误消息,我们需要修改模态行为! 该过程非常类似于搜索栏之一。

    游戏容器模式 (GamesContainer Modal)

    This is our state including the selectedGame:

    这是我们的状态,包括selectedGame:

    games: { 
        list : [
            {//...Game1},
            {//...Game2},
            ...
        ],
        searchBar: '',
        selectedGame: { //... Game to show in the modal }
    }

    In client/src/actions/games.js let's define an action creator:

    client/src/actions/games.js我们定义一个动作创建者:

    import {
      GET_GAMES,
      GET_GAMES_SUCCESS,
      GET_GAMES_FAILURE,
      SET_SEARCH_BAR,
      SHOW_SELECTED_GAME // Another constant
    } from '../constants/games';
    
    
    function getGames () {
      return {
        type: GET_GAMES
      };
    }
    
    function getGamesSuccess (games) {
      return {
        type: GET_GAMES_SUCCESS,
        games
      };
    }
    
    function getGamesFailure () {
      return {
        type: GET_GAMES_FAILURE
      };
    }
    
    function setSearchBar (keyword) {
      return {
        type: SET_SEARCH_BAR,
        keyword
      };
    }
    
    // We pass the game as payload
    function showSelectedGame (game) {
      return {
        type: SHOW_SELECTED_GAME,
        game
      };
    }
    
    export {
      getGames,
      getGamesSuccess,
      getGamesFailure,
      setSearchBar,
      showSelectedGame // Export the new action-creator
    };
    

    We also must define a new constant SHOW_SELECTED_GAME.

    我们还必须定义一个新的常量SHOW_SELECTED_GAME

    Edit /client/src/constants/games.js:

    编辑/client/src/constants/games.js

    const GET_GAMES = 'GET_GAMES';
    const GET_GAMES_SUCCESS = 'GET_GAMES_SUCCESS';
    const GET_GAMES_FAILURE = 'GET_GAMES_FAILURE';
    const SET_SEARCH_BAR = 'SET_SEARCH_BAR';
    // Define the latest constant
    const SHOW_SELECTED_GAME = 'SHOW_SELECTED_GAME';
    
    export {
      GET_GAMES,
      GET_GAMES_SUCCESS,
      GET_GAMES_FAILURE,
      SET_SEARCH_BAR,
      SHOW_SELECTED_GAME // Export the new constant
    };
    

    Again, let's add the case in the games reducer /client/src/reducers/games.js:

    再次,让我们在游戏reducer /client/src/reducers/games.js添加案例:

    import Immutable from 'immutable';
    import {
      GET_GAMES_SUCCESS,
      GET_GAMES_FAILURE,
      SET_SEARCH_BAR,
      // Import the new constant to be used as new 'case'
      SHOW_SELECTED_GAME
    } from '../constants/games';
    
    const initialState = Immutable.Map();
    
    export default (state = initialState, action) => {
      switch (action.type) {
        case GET_GAMES_SUCCESS: {
          return state.merge({ list: action.games });
        }
        case SET_SEARCH_BAR: {
          return state.merge({ searchBar: action.keyword });
        }
       // We finally moved the selectedGame in the app state
        case SHOW_SELECTED_GAME: {
          return state.merge({ selectedGame: action.game });
        }
        case GET_GAMES_FAILURE: {
          return state.clear();
        }
        default:
          return state;
      }
    }
    

    We can finally edit our GamesContainer:

    我们终于可以编辑我们的GamesContainer了

    import React, { Component } from 'react';
    import { connect } from 'react-redux';
    import { bindActionCreators } from 'redux';
    import Immutable from 'immutable';
    import { Modal, GamesListManager } from '../components';
    import * as gamesActionCreators from '../actions/games';
    
    // GamesContainer does not initialize the state anymore
    class GamesContainer extends Component {
      constructor (props) {
        super(props);
        this.toggleModal = this.toggleModal.bind(this);
        this.deleteGame = this.deleteGame.bind(this);
        this.setSearchBar = this.setSearchBar.bind(this);
      }
    
      componentDidMount () {
        this.getGames();
      }
    // Once the action is dispatched we toggle the modal
      toggleModal (index) {
      // We pass the game given the index parameter passed from the view button
        this.props.gamesActions.showSelectedGame(this.props.games[index]);
        $('#game-modal').modal();
      }
    
      getGames () {
        this.props.gamesActions.getGames();
      }
    
      deleteGame (id) {
        fetch(`http://localhost:8080/games/${id}`, {
          headers: new Headers({
            'Content-Type': 'application/json',
          }),
          method: 'DELETE',
        })
        .then(response => response.json())
        .then(response => {
          this.setState({ games: this.state.games.filter(game => game._id !== id) });
          console.log(response.message);
        });
      }
    
      setSearchBar (event) {
        this.props.gamesActions.setSearchBar(event.target.value.toLowerCase());
      }
    
      render () {
       {/* We get all the info from props */}
        const { games, selectedGame, searchBar } = this.props;
        return (
          <div>
            <Modal game={selectedGame} />
            <GamesListManager
              games={games}
              searchBar={searchBar}
              setSearchBar={this.setSearchBar}
              toggleModal={this.toggleModal}
              deleteGame={this.deleteGame}
            />
          </div>
        );
      }
    }
    
    function mapStateToProps (state) {
      return {
        games: state.getIn(['games', 'list'], Immutable.List()).toJS(),
        searchBar: state.getIn(['games', 'searchBar'], ''),
        // The latest addition to props is the selectedGame
        selectedGame: state.getIn(['games', 'selectedGame'], Immutable.List()).toJS() 
      }
    }
    
    function mapDispatchToProps (dispatch) {
      return {
        gamesActions: bindActionCreators(gamesActionCreators, dispatch)
      };
    }
    export default connect(mapStateToProps, mapDispatchToProps)(GamesContainer);
    
    • We now map selectedGame from the state to the props, so it's available at this.props.selectedGame.

      现在,我们将selectedGame从状态映射到道具,因此可以在this.props.selectedGame上使用它。
    • We can dispatch the new action through gamesActions props. You take a look at the function toggleModal: It dispatches the new action with the selected game and toggle the modal.

      我们可以通过游戏动作道具来派发新动作。 您看一下功能toggleModal :它使用选定的游戏调度新动作并切换模式。

    At this point check the app in the browser:

    此时,请在浏览器中检查该应用程序:

    That's awesome because we already achieve a big result: GamesContainer is now a dumb component as it has no state! Its connected version instead is a smart component because connected to the Redux store.

    太棒了,因为我们已经取得了很大的成就: GamesContainer现在是一个愚蠢的组件,因为它没有状态! 相反,它的连接版本是智能组件,因为已连接到Redux存储。

    We are almost done, we just need to rewrite the logic to delete a game.

    我们差不多完成了,我们只需要重写逻辑来删除游戏。

    删除游戏 (Delete a Game)

    Let's start from the actions, since we are gonna write another HTTP request we can make assumptions based on the getGames logic: Inside a try catch we send a DELETE request to the server and if everything goes well the next action to the reducer will be DELETE_GAME_SUCCESSFUL, otherwise the catch block will send DELETE_GAME_FAILURE.

    让我们从动作开始,因为我们要编写另一个HTTP请求,因此我们可以基于getGames逻辑进行假设:在try catch中,我们将DELETE请求发送到服务器,如果一切顺利,则到reducer的下一个动作将是DELETE_GAME_SUCCESSFUL ,否则catch块将发送DELETE_GAME_FAILURE

    So let's edit /client/src/actions/games.js:

    因此,让我们编辑/client/src/actions/games.js

    import {
      GET_GAMES,
      GET_GAMES_SUCCESS,
      GET_GAMES_FAILURE,
      SET_SEARCH_BAR,
      SHOW_SELECTED_GAME,
      // We import the three constants
      DELETE_GAME,
      DELETE_GAME_SUCCESS,
      DELETE_GAME_FAILURE
    } from '../constants/games';
    
    
    function getGames () {
      return {
        type: GET_GAMES
      };
    }
    
    function getGamesSuccess (games) {
      return {
        type: GET_GAMES_SUCCESS,
        games
      };
    }
    
    function getGamesFailure () {
      return {
        type: GET_GAMES_FAILURE
      };
    }
    
    function setSearchBar (keyword) {
      return {
        type: SET_SEARCH_BAR,
        keyword
      };
    }
    
    function showSelectedGame (game) {
      return {
        type: SHOW_SELECTED_GAME,
        game
      };
    }
    
    // This is called when a user clicks on the delete button
    function deleteGame () {
      return {
        type: DELETE_GAME
      };
    }
    // In case of succesful deletion the action is dispatched to the reducer
    function deleteGamesSuccess (games) {
      return {
        type: DELETE_GAME_SUCCESS,
        games
      };
    }
    // In case of failure the saga dispatches DELETE_GAME_FAILURE instead
    function deleteGameFailure () {
      return {
        type: DELETE_GAME_FAILURE
      };
    }
    
    export {
      getGames,
      getGamesSuccess,
      getGamesFailure,
      setSearchBar,
      showSelectedGame,
      // Export the 3 new functions
      deleteGame,
      deleteGameSuccess,
      deleteGameFailure
    };
    
    • deleteGame returns the action a new saga takes, it will be run from the GameContainer and has the game id as parameter.

      deleteGame返回新传奇的动作,它将从GameContainer运行,并将游戏ID作为参数。
    • The remaining go from the saga to the reducer according to the HTTP request result. In particular, deleteGameSuccess carries the games... Why? That's because once the game is deleted we filter the current games list from the state and delete it from the list as well. Then the reducer will merge the new games list and return a new state. This is the same as what GET_GAMES_SUCCESS does!

      其余部分根据HTTP请求结果从传奇到减速器。 特别是deleteGameSuccess携带游戏...为什么? 这是因为一旦删除游戏,我们就会从状态中过滤当前游戏列表,并将其从列表中删除。 然后,reducer将合并新游戏列表并返回新状态。 这与GET_GAMES_SUCCESS一样!

    We need to edit the constants file as well, open /client/src/constants and paste the following code:

    我们还需要编辑常量文件,打开/client/src/constants并粘贴以下代码:

    const GET_GAMES = 'GET_GAMES';
    const GET_GAMES_SUCCESS = 'GET_GAMES_SUCCESS';
    const GET_GAMES_FAILURE = 'GET_GAMES_FAILURE';
    const SET_SEARCH_BAR = 'SET_SEARCH_BAR';
    const SHOW_SELECTED_GAME = 'SHOW_SELECTED_GAME';
    // Here's the definition for the new 3 constants
    const DELETE_GAME = 'DELETE_GAME';
    const DELETE_GAME_SUCCESS = 'DELETE_GAME_SUCCESS';
    const DELETE_GAME_FAILURE = 'DELETE_GAME_FAILURE';
    
    export {
      GET_GAMES,
      GET_GAMES_SUCCESS,
      GET_GAMES_FAILURE,
      SET_SEARCH_BAR,
      SHOW_SELECTED_GAME,
      // Export the new constants
      DELETE_GAME,
      DELETE_GAME_SUCCESS,
      DELETE_GAME_FAILURE
    };
    

    Now let's create a new saga so let's edit /client/src/sagas/games.js:

    现在让我们创建一个新的传奇,让我们编辑/client/src/sagas/games.js

    import {
      takeLatest,
      delay
    } from 'redux-saga';
    import {
      put,
      select,
      call
    } from 'redux-saga/effects';
    // We import DELETE_GAME constant for the new saga watcher
    import {
      GET_GAMES,
      DELETE_GAME
    } from '../constants/games';
    import {
        getGamesSuccess,
        getGamesFailure ,
        // the last two action creators are imported as well
        deleteGameSuccess,
        deleteGameFailure
    } from '../actions/games';
    
    // Selector function to return the games list from the state
    const selectedGames = (state) => {
      return state.getIn(['games', 'list']).toJS();
    }
    
    const fetchGames = () => {
      return fetch('http://localhost:8080/games', {
        headers: new Headers({
          'Content-Type': 'application/json'
        })
      })
      .then(response => response.json());
    };
    
    const deleteServerGame = (id) => {
      return fetch(`http://localhost:8080/games/${id}`, {
        headers: new Headers({
          'Content-Type': 'application/json',
        }),
        method: 'DELETE',
      })
      .then(response => response.json());
    }
    
    function* getGames () {
      try {
        const games = yield call(fetchGames);
        yield put(getGamesSuccess(games));
      } catch (err) {
        yield put(getGamesFailure());
      }
    }
    
    function* deleteGame (action) {
      const { id } = action;
      // We take the games from the state
      const games = yield select(selectedGames);
      try {
        yield call(deleteServerGame, id);
        // The new state will contain the games except for the deleted one.
        yield put(deleteGameSuccess(games.filter(game => game._id !== id)));
      } catch (e) {
        // In case of error 
        yield put(deleteGameFailure());
      }
    }
    
    function* watchGetGames () {
      yield takeLatest(GET_GAMES, getGames);
    }
    // The new watcher intercepts the action and run deleteGame
    function* watchDeleteGame () {
        yield takeLatest(DELETE_GAME, deleteGame);
    }
    
    export {
        watchGetGames,
        watchDeleteGame
    };
    • We created the watchDeleteGame saga in charge to intercept the action DELETE_GAME.

      我们创建了watchDeleteGame传奇,负责拦截动作DELETE_GAME
    • In deleteGame we first take advantage of the effect select form Redux-saga to retrieve information from the state: the function needs the games list because if everything goes well, it will the deleted game from it and send it along with the action DELETE_GAME_SUCCESS.

      As I mentioned before, the filter function from javascript array comes handy, we can easily build a new games list without the deleted game and pass it as parameter to deleteGameSuccess.

      deleteGame我们首先利用Redux-saga形式的效果选择来从状态中检索信息:该功能需要游戏列表,因为如果一切顺利,它将从中删除游戏并将其与动作DELETE_GAME_SUCCESS一起发送。

      正如我之前提到的,来自javascript数组的过滤器功能非常方便,我们可以轻松构建新游戏列表而无需删除游戏,并将其作为参数传递给deleteGameSuccess

    We also need to edit /client/src/sagas/index.js to run watchDeleteGame in parallel with watchGetGames:

    我们还需要编辑/client/src/sagas/index.js运行watchDeleteGame并联watchGetGames

    import {
      watchGetGames,
      watchDeleteGame
    } from './games';
    
    export default function* rootSaga () {
      yield [
        watchGetGames(),
        watchDeleteGame() // must be run in parallel
      ];
    }

    We almost finished, let's edit the reducer games.js:

    我们快完成了,让我们编辑reducer games.js

    import Immutable from 'immutable';
    import {
      GET_GAMES_SUCCESS,
      GET_GAMES_FAILURE,
      SET_SEARCH_BAR,
      SHOW_SELECTED_GAME,
      DELETE_GAME_SUCCESS,
      DELETE_GAME_FAILURE
    } from '../constants/games';
    
    const initialState = Immutable.Map();
    
    export default (state = initialState, action) => {
      switch (action.type) {
      // Both cases share the same behavior in fact
        case DELETE_GAME_SUCCESS:
        case GET_GAMES_SUCCESS: {
          return state.merge({ list: action.games });
        }
        case SET_SEARCH_BAR: {
          return state.merge({ searchBar: action.keyword });
        }
        case SHOW_SELECTED_GAME: {
          return state.merge({ selectedGame: action.game });
        }
        // We can simply assume all the failures clear the state
        case DELETE_GAME_FAILURE:
        case GET_GAMES_FAILURE: {
          return state.clear();
        }
        default:
          return state;
      }
    }
    • As explained before, the action DELETE_GAME_SUCCESS does the same as GET_GAMES_SUCCESS so the two cases can do the same as well.

      如前所述,动作DELETE_GAME_SUCCESSGET_GAMES_SUCCESS相同,因此两种情况也可以相同。
    • Could be a better idea to separate the behavior of DELETE_GAME_FAILURE and GET_GAMES_FAILURE, however for the purpose of the tutorial we can just assume that whenever the server is down we simply return a new state with empty values.

      分离DELETE_GAME_FAILUREGET_GAMES_FAILURE的行为可能是一个更好的主意,但是出于本教程的目的,我们可以假定,只要服务器关闭,我们就简单地返回一个空值的新状态。

    Finally, we need to dispatch the action DELETE_GAME within our container, let's edit /client/src/containers/GamesContainer.js:

    最后,我们需要在容器内分派动作DELETE_GAME ,让我们编辑/client/src/containers/GamesContainer.js

    // ...Code
      deleteGame (id) { // It simplies dispatches the action including the game id
        this.props.gamesActions.deleteGame(id);
      }
    // ...Code
    
    

    It's not necessary to show the entire code, we just need to modify the deleteGame function to dispatch the action.

    不必显示完整的代码,我们只需要修改deleteGame函数即可分派动作。

    Easy as pie!

    非常简单!

    Let's try to delete a game in the browser, just go to http://localhost:3000:

    让我们尝试在浏览器中删除游戏,只需转到http:// localhost:3000

    重写AddGameContainer ( Rewrite AddGameContainer )

    We are almost done but we have to rewrite the AddGameContainer and Form to use Redux.

    我们差不多完成了,但是我们必须重写AddGameContainerForm才能使用Redux

    表单组件 (Form Component)

    If you take a look at the code of AddGameContainer you can immediately figure out what to do:

    如果查看一下AddGameContainer的代码,您可以立即弄清楚该怎么做:

    • We need a new action to post the game to the server and dispatch it from its function uploadPicture. The procedure involves moving the server POST request in a saga and perhaps dispatch another action to the reducer.

      我们需要一个新动作将游戏发布到服务器并从其功能uploadPicture分发它。 该过程涉及在一个传奇中移动服务器POST请求,并可能将另一个操作分派给reducer。
    • However, setGame touches the app state as it keeps track of the user input while adding the new game. We can easily get rid of it by using Redux-form.

      但是, setGame在添加新游戏时会跟踪用户输入, setGame触摸应用程序状态。 我们可以使用Redux-form轻松摆脱它。
    • What about uploadPicture then? We can move our picture uploader into a saga function too and keep the url in the state.

      uploadPicture呢? 我们也可以将图片上传器移动到Saga函数中,并将网址保持在状态中。

    NB: We are going to touch just the surface of Redux-form, I do suggest you to take a look at its guide for more.

    注意 :我们将仅涉及Redux-form的表面,我建议您仔细阅读其指南

    Let's start by adding Redux-form to our dependencies:

    首先,将Redux-form添加到我们的依赖项中:

    yarn add redux-form --dev

    Then take a look at the state new structure:

    然后看看状态新结构:

    games: { 
        list : [
            {//...Game1},
            {//...Game2},
            ...
        ],
        searchBar: '',
        selectedGame: { //... Game to show in the modal }
    },
    form : {
        game: {//... it will contain several pieces of information as well as the inputs value}
    },
    filestack : {
        url : 'picture_url' //... Trivial, this is where we 'save' the picture url
    }
    • game is the name of our form specified when the component is decorated by reduxForm.

      game是当组件由reduxForm装饰时指定的表单名称。
    • On the other hand the picture url is available at filestack.url

      另一方面,图片URL可从filestack.url

    And as first thing let's rewrite the Form component, edit /client/src/components/Form.js with the following code:

    首先,让我们重写Form组件,使用以下代码编辑/client/src/components/Form.js

    import React, { PureComponent } from 'react';
    import { Link } from 'react-router';
    // We import Field and reduxForm from redux-form immutable version
    import { Field, reduxForm } from 'redux-form/immutable';
    
    class Form extends PureComponent {
      render () {
        const { picture, uploadPicture } = this.props;
        return (
          <div className="row scrollable">
              <div className="col-md-offset-2 col-md-8">
            <div className="text-left">
                <Link to="/games" className="btn btn-info">Back</Link>
            </div>
            <div className="panel panel-default">
            <div className="panel-heading">
                <h2 className="panel-title text-center">
                     Add a Game!
                </h2>
            </div>
            <div className="panel-body">
            <form onSubmit={this.props.handleSubmit}>
                    <div className="form-group text-left">
                      <label htmlFor="name">Name</label>
        {/* All the previous form input become Field components. 
        Notice that Field render the right form input given the value of component */}
                      <Field 
                        name="name" 
                        type="text" 
                        className="form-control" 
                        component="input" 
                        placeholder="Enter the name" 
                      />
                    </div>
                    <div className="form-group text-left">
                      <label htmlFor="description">Description</label>
        {/* The description textarea becomes a Field component too */}
                      <Field 
                        name="description" 
                        component="textarea" 
                        className="form-control" 
                        placeholder="Enter the description" 
                        rows="5" 
                      />
                    </div>
                    <div className="form-group text-left">
                      <label htmlFor="price">Year</label>
        {/* ... And the input number for the year */}
                      <Field 
                        name="year" 
                        component="input" 
                        type="number" 
                        className="form-control" 
                        placeholder="Enter the year" 
                      />
                    </div>
              <div className="form-group text-left">
                   <label htmlFor="picture">Picture</label>
                   <div className="text-center dropup">
                <button 
                  id="button-upload" 
                  type="button" 
                  className="btn btn-danger" 
                  onClick={() => uploadPicture()}
                >
                  Upload <span className="caret" />
                </button>
                  </div>
                </div>
                <div className="form-group text-center">
                <img id="picture" className="img-responsive img-upload" src={picture} />
                </div>
                <button type="submit" className="btn btn-submit btn-block">Submit</button>
            </form>
             </div>
           </div>
        </div>
    </div>
        );
      }
    }
    
    // we named the form game so that in the state we can access it like form.game
    export default reduxForm({ form: 'game' })(Form);
    
    • We included from Field and reduxForm: The first is a component to connect a field to the redux store while the second is also a component but it wraps the Form component in a high order component instead. Once we add the form reducer our state will keep up-to-date with our Field inputs as it listens to actions dispatched from reduxForm.

      我们从FieldreduxForm中包括了:第一个是将字段连接到redux存储的组件,第二个也是一个组件,但是它将Form组件包装在一个高阶组件中。 一旦添加了表单reduxForm器,我们的状态就会随着Field输入的更新而更新,因为它监听从reduxForm派发的reduxForm
    • Also, they are both the immutable version (redux-form/immutable) as our state is an immutable data-structure.

      同样,它们都是不可变的版本(redux-form / immutable),因为我们的状态是不可变的数据结构。

    缩径机 (Form Reducer)

    As last step let's add the form reducer, edit the /client/src/reducer/index.js and paste the following code:

    作为最后一步,我们添加表单化/client/src/reducer/index.js器,编辑/client/src/reducer/index.js并粘贴以下代码:

    import { combineReducers } from 'redux-immutable';
    // Even here we need to include the immutable version
    import { reducer as form } from 'redux-form/immutable';
    import games from './games';
    
    // Now you can see the benefit of using combineReducers!
    export default combineReducers({
      games,
      form
    });
    

    If you now try to play with the app and type anything in the form fields, Redux-form will automatically dispatch special actions to keep the game info in the state. Plus, and this is great, if you go back to the games list the form will automatically remove its information from the state as well.

    如果您现在尝试使用该应用程序并在表单字段中键入任何内容,则Redux-form将自动调度特殊操作以将游戏信息保持在该状态。 另外,这很棒,如果您返回游戏列表,该表格也会自动从状态中删除其信息。

    Still, we can't actually create any object yet, we need the sagas for it, as well as for uploading the picture on Filestack.

    不过,我们实际上还不能创建任何对象,我们需要它的sagas,以及将图片上传到Filestack上。

    These are the next steps.

    这些是下一步。

    行动 (The Actions)

    We can think of the actions for both sagas the same way we did for the previous ones: We have an action dispatched from the component/container and two actions, one for success and one for failure, both yielded by the saga.

    我们可以像对待前一个一样思考两个sagas的动作:我们有一个从组件/容器分派的动作和两个动作,一个是成功的,一个是失败的,都是由传奇产生的。

    First, let's write the actions for adding a new game, so edit /client/src/actions/games.js and paste the following code:

    首先,让我们编写添加新游戏的动作,因此编辑/client/src/actions/games.js并粘贴以下代码:

    import {
      GET_GAMES,
      GET_GAMES_SUCCESS,
      GET_GAMES_FAILURE,
      SET_SEARCH_BAR,
      SHOW_SELECTED_GAME,
      DELETE_GAME,
      DELETE_GAME_SUCCESS,
      DELETE_GAME_FAILURE,
      // Import new constants
      POST_GAME,
      POST_GAME_SUCCESS,
      POST_GAME_FAILURE
    } from '../constants/games';
    
    
    function getGames () {
      return {
        type: GET_GAMES
      };
    }
    
    function getGamesSuccess (games) {
      return {
        type: GET_GAMES_SUCCESS,
        games
      };
    }
    
    function getGamesFailure () {
      return {
        type: GET_GAMES_FAILURE
      };
    }
    
    function setSearchBar (keyword) {
      return {
        type: SET_SEARCH_BAR,
        keyword
      };
    }
    
    function showSelectedGame (game) {
      return {
        type: SHOW_SELECTED_GAME,
        game
      };
    }
    
    function deleteGame (id) {
      return {
        type: DELETE_GAME,
        id
      };
    }
    
    function deleteGameSuccess (games) {
      return {
        type: DELETE_GAME_SUCCESS,
        games
      };
    }
    
    function deleteGameFailure () {
      return {
        type: DELETE_GAME_FAILURE
      };
    }
    
    // POST_GAME is dispatched when users click on submit
    function postGame () {
      return {
        type: POST_GAME
      };
    }
    
    // The action is dispatched when the returned promise from a POST request resolve
    function postGameSuccess () {
      return {
        type: POST_GAME_SUCCESS
      };
    }
    
    // In case of failure
    function postGameFailure () {
      return {
        type: POST_GAME_FAILURE
      };
    }
    
    export {
      getGames,
      getGamesSuccess,
      getGamesFailure,
      setSearchBar,
      showSelectedGame,
      deleteGame,
      deleteGameSuccess,
      deleteGameFailure,
      // Export the new action-creators
      postGame,
      postGameSuccess,
      postGameFailure
    };
    

    Notice that POST_GAME doesn't carry any payload, the saga takes it directly from the state. Next, we create a new file filestack.js in /client/src/actions and paste the following code:

    请注意, POST_GAME不携带任何负载,传奇直接从状态获取它。 接下来,我们在/client/src/actions创建一个新文件filestack.js并粘贴以下代码:

    // Import constants (obviously)
    import {
      UPLOAD_PICTURE,
      UPLOAD_PICTURE_SUCCESS,
      UPLOAD_PICTURE_FAILURE
    } from '../constants/filestack';
    
    // Triggered by the upload button
    function uploadPicture () {
      return {
        type: UPLOAD_PICTURE
      };
    }
    
    // It carries the picture url to be added to the state
    function uploadPictureSuccess (url) {
      return {
        type: UPLOAD_PICTURE_SUCCESS,
        url
      };
    }
    
    // In case of failure
    function uploadPictureFailure () {
      return {
        type: UPLOAD_PICTURE_FAILURE
      };
    }
    
    export {
      uploadPicture,
      uploadPictureSuccess,
      uploadPictureFailure
    };
    

    Nothing exotic here, UPLOAD_PICTURE_SUCCESS has a payload which is the CDN url returned by Filestack.

    UPLOAD_PICTURE_SUCCESS在这里没有什么奇怪的地方, 有一个有效载荷,它是Filestack返回的CDN URL。

    Again, we are adding functionalities to the app while following a similar pattern. Right after the actions creators we need to define the new constants used for the action.type property. Open /client/src/constants/games.js and paste the following code:

    同样,我们在遵循类似模式的同时向应用程序添加功能。 在动作创建者之后,我们需要定义用于action.type属性的新常量。 打开/client/src/constants/games.js并粘贴以下代码:

    const GET_GAMES = 'GET_GAMES';
    const GET_GAMES_SUCCESS = 'GET_GAMES_SUCCESS';
    const GET_GAMES_FAILURE = 'GET_GAMES_FAILURE';
    const SET_SEARCH_BAR = 'SET_SEARCH_BAR';
    const SHOW_SELECTED_GAME = 'SHOW_SELECTED_GAME';
    const DELETE_GAME = 'DELETE_GAME';
    const DELETE_GAME_SUCCESS = 'DELETE_GAME_SUCCESS';
    const DELETE_GAME_FAILURE = 'DELETE_GAME_FAILURE';
    // The new constants definition
    const POST_GAME = 'POST_GAME';
    const POST_GAME_SUCCESS = 'POST_GAME_SUCCESS';
    const POST_GAME_FAILURE = 'POST_GAME_FAILURE';
    
    export {
      GET_GAMES,
      GET_GAMES_SUCCESS,
      GET_GAMES_FAILURE,
      SET_SEARCH_BAR,
      SHOW_SELECTED_GAME,
      DELETE_GAME,
      DELETE_GAME_SUCCESS,
      DELETE_GAME_FAILURE,
      // Export the constants
      POST_GAME,
      POST_GAME_SUCCESS,
      POST_GAME_FAILURE
    };
    

    Then create a new constants file called filestack.js and paste the following code:

    然后创建一个名为filestack.js的新常量文件,并粘贴以下代码:

    // A very simple file but we want to keep the constants for filestack separated to another file
    const UPLOAD_PICTURE = 'UPLOAD_PICTURE';
    const UPLOAD_PICTURE_SUCCESS = 'UPLOAD_PICTURE_SUCCESS';
    const UPLOAD_PICTURE_FAILURE = 'UPLOAD_PICTURE_FAILURE';
    
    export {
      UPLOAD_PICTURE,
      UPLOAD_PICTURE_SUCCESS,
      UPLOAD_PICTURE_FAILURE
    };
    

    Filestack Reducer (Filestack Reducer)

    We obviously need a new reducer for Filestack related actions, let's create filestack.js in /client/src/reducers and paste the following code:

    显然,我们需要一个用于Filestack相关操作的新的reducer,让我们在/client/src/reducers创建filestack.js并粘贴以下代码:

    import Immutable from 'immutable';
    // import the constants
    import {
      UPLOAD_PICTURE_SUCCESS,
      UPLOAD_PICTURE_FAILURE
    } from '../constants/filestack';
    // Also import the constants for the post game actions
    import {
      POST_GAME_SUCCESS,
      POST_GAME_FAILURE
    } from '../constants/games';
    
    // The initial state is just a Map
    const initialState = Immutable.Map();
    
    export default (state = initialState, action) => {
      switch (action.type) {
      // The url is saved in filestack.url
        case UPLOAD_PICTURE_SUCCESS: {
          return state.merge({ url: action.url });
        }
       // After a game was posted we want to clear the state from the picture url as well
        case POST_GAME_SUCCESS:
        case POST_GAME_FAILURE:
        case UPLOAD_PICTURE_FAILURE: {
          return state.clear();
        }
        default:
          return state;
      }
    }
    

    Notice that also the actions after the game submission are intercepted by the reducer: We want to clear the state which means delete the picture url from it. As said before, once we submit we change the view with hashHistory, so the Redux-form game will be automatically deleted from the state while filestack.url will persist.

    注意,提交游戏后的动作也被reducer拦截:我们要清除状态,这意味着从中删除图片url。 如前所述,一旦提交,我们将使用hashHistory更改视图,因此Redux形式的游戏将自动从状态中删除,而filestack.url将保持不变。

    Let's now combine it with the others in /client/src/reducers/index.js:

    现在让我们将其与/client/src/reducers/index.js的其他对象结合起来:

    import { combineReducers } from 'redux-immutable';
    import { reducer as form } from 'redux-form/immutable';
    import games from './games';
    import filestack from './filestack';
    
    export default combineReducers({
      games,
      form,
      filestack // Include the filestack reducer to be combined into a single one
    });
    

    AddGameContainer Sagas (AddGameContainer Sagas)

    Now let's talk about sagas, we need to write a few, let's start from adding the game: Open /client/src/sagas/games.js and paste the following code:

    现在让我们谈谈sagas,我们需要编写一些内容,让我们从添加游戏开始:打开/client/src/sagas/games.js并粘贴以下代码:

    import { takeLatest } from 'redux-saga';
    import {
        put,
        select,
        call
    } from 'redux-saga/effects';
    import {
      GET_GAMES,
      DELETE_GAME,
      POST_GAME // import the constant to be used by the watcher
    } from '../constants/games';
    import {
      getGamesSuccess,
      getGamesFailure ,
      deleteGameSuccess,
      deleteGameFailure,
      // Import the action creators to handle the server POST request outcome
      postGameSuccess,
      postGameFailure
    } from '../actions/games';
    
    const selectedGames = (state) => {
      return state.getIn(['games', 'list']).toJS();
    }
    
    // selector to get the picture from the state
    const selectedPicture = (state) => {
      return state.getIn(['filestack', 'url'], '');
    }
    
    const fetchGames = () => {
      return fetch('http://localhost:8080/games', {
        headers: new Headers({
          'Content-Type': 'application/json'
        })
      })
      .then(response => response.json());
    };
    
    const deleteServerGame = (id) => {
      return fetch(`http://localhost:8080/games/${id}`, {
        headers: new Headers({
          'Content-Type': 'application/json',
        }),
        method: 'DELETE',
      })
      .then(response => response.json());
    }
    
    // the function contains the fetch logic to add a game
    const postServerGame = (game) => {
      return fetch('http://localhost:8080/games', {
        headers: new Headers({
          'Content-Type': 'application/json'
        }),
        method: 'POST',
        body: JSON.stringify(game)
      })
      .then(response => response.json());
    }
    
    function* getGames () {
      try {
        const games = yield call(fetchGames);
        yield put(getGamesSuccess(games));
      } catch (err) {
        yield put(getGamesFailure());
      }
    }
    
    function* deleteGame (action) {
      const { id } = action;
      const games = yield select(selectedGames);
      try {
        yield call(deleteServerGame, id); // API call
        yield put(deleteGameSuccess(games.filter(game => game._id !== id)));
      } catch (e) {
        // In case of error
        yield put(deleteGameFailure());
      }
    }
    
    const getGameForm = (state) => {
      return state.getIn(['form', 'game']).toJS();
    }
    
    function* postGame () {
      // Access the state to retrieve the new game information
      const picture = yield select(selectedPicture);
      const game = yield select(getGameForm);
      // Create the newGame object to be sent to the server
      const newGame = Object.assign({}, { picture }, game.values);
      try {
        // yield call postServerGame to post to the server
        yield call(postServerGame, newGame);
        yield put(postGameSuccess());
      } catch (e) {
        yield put(postGameFailure());
      }
    }
    
    function* watchGetGames () {
      yield takeLatest(GET_GAMES, getGames);
    }
    
    function* watchDeleteGame () {
        yield takeLatest(DELETE_GAME, deleteGame);
    }
    
    // The new watcher saga to intercept POST_GAME actions
    function* watchPostGame () {
      yield takeLatest(POST_GAME, postGame);
    }
    
    export {
        watchGetGames,
        watchDeleteGame,
        watchPostGame // Export the new watcher to be run in parallel
    };
    

    The postGame function by yielding select twice with a selector function as parameter is able to get the games information and picture. Then, we run the fetch function and post to the game to the server.

    postGame函数通过使用选择器函数作为参数产生两次select来获取游戏信息和图片。 然后,我们运行获取功能并将游戏发布到服务器。

    Regarding Filestack, we have to rethink about the pick function: while the first parameter is an object of options the others are all function, we have onSuccess, onFailure and in fact onProgress too (to learn more about it just take a look a the documentation). Unfortunately pick doesn't not return any promise but sagas requires that, so we can take advantage of the onSuccess and onFailure function parameters to resolve or reject a promise.

    关于Filestack,我们必须重新考虑pick函数:虽然第一个参数是选项的对象,而其他参数都是函数,但我们还有onSuccessonFailure以及实际上是onProgress (要了解更多信息,请查看文档 )。 不幸的是, pick不会返回任何承诺,但是sagas要求这样做,因此我们可以利用onSuccess和onFailure函数参数来解决或拒绝承诺。

    At Filestack they tried their best to provide very flexible functions that users can customize for their needs, this is a perfect example.

    在Filestack,他们尽力提供非常灵活的功能,用户可以根据自己的需求进行自定义,这是一个完美的例子。

    Let's create filestack.js in /client/src/sagas and paste the following code:

    让我们在/client/src/sagas创建filestack.js并粘贴以下代码:

    import { takeLatest } from 'redux-saga';
    import { put, call } from 'redux-saga/effects';
    import { UPLOAD_PICTURE } from '../constants/filestack';
    import {
      uploadPictureSuccess,
      uploadPictureFailure
    } from '../actions/filestack';
    
    const pick = () => {
       return new Promise((resolve, reject) => {
        filepicker.pick (
          {
          // The options are the same as in part1
            mimetype: 'image/*',
            container: 'modal',
            services: ['COMPUTER', 'FACEBOOK', 'INSTAGRAM', 'URL', 'IMGUR', 'PICASA'],
            openTo: 'COMPUTER'
          },
          function (Blob) {
            console.log(JSON.stringify(Blob));
            const handler = Blob.url;
            resolve(handler); // The promise resolves
          },
          function (FPError) {
            console.log(FPError.toString());
            reject(FPError.toString()); // the promise rejects
          }
        );
      });
    }
    
    function* uploadPicture () {
      try {
        const url = yield call(pick); // call the pick function
        yield put(uploadPictureSuccess(url));
      } catch (error) {
        yield put(uploadPictureFailure());
      }
    }
    
    export function* watchUploadPicture () {
      yield takeLatest(UPLOAD_PICTURE, uploadPicture);
    }
    

    The function pick yielded by uploadPicture return a promise which either resolves in onSuccess or rejects in onFailure.

    uploadPicture产生的函数pick返回一个promise,该promise在onSuccess中解析,或者在onFailure中拒绝。

    Let's update /client/src/sagas/index.js to run the new sagas:

    让我们更新/client/src/sagas/index.js来运行新的sagas:

    import {
      watchGetGames,
      watchDeleteGame,
      watchPostGame
    } from './games';
    import { watchUploadPicture } from './filestack';
    
    export default function* rootSaga () {
      yield [
        watchGetGames(),
        watchDeleteGame(),
        watchPostGame(),
        watchUploadPicture() // Run the last saga in parallel with the others
      ];
    }
    

    The last thing we need to do is to edit addGameContainer:

    我们需要做的最后一件事是编辑addGameContainer

    import React, { Component } from 'react';
    import { connect } from 'react-redux';
    import { bindActionCreators } from 'redux';
    import { hashHistory } from 'react-router';
    import { Form } from '../components';
    import * as gamesActionCreators from '../actions/games';
    import * as filestackActionCreators from '../actions/filestack';
    
    class AddGameContainer extends Component {
      constructor (props) {
        super(props);
        this.submit = this.submit.bind(this);
        this.uploadPicture = this.uploadPicture.bind(this);
      }
      // Dispatch POST_GAME to the saga and change the view
      submit (event) {
        event.preventDefault();
        this.props.gamesActions.postGame();
        hashHistory.push('/games');
      }
      // Dispatch UPLOAD_PICTURE to the filestack saga
      uploadPicture () {
        this.props.filestackActions.uploadPicture();
      }
      render () {
        const { picture } = this.props;
        return (
          <Form
            handleSubmit={this.submit}
            picture={picture}
            uploadPicture={this.uploadPicture}
          />
        );
      }
    }
    
    function mapStateToProps (state) {
      return {
      // We access the state to retrieve the url and show the preview of the image in the form
        picture: state.getIn(['filestack', 'url'], '')
      }
    }
    
    function mapDispatchToProps (dispatch) {
      return {
      // We get the actions to dispatch POST_GAME actions and UPLOAD_PICTURE too
        gamesActions: bindActionCreators(gamesActionCreators, dispatch),
        filestackActions: bindActionCreators(filestackActionCreators, dispatch)
      };
    }
    export default connect(mapStateToProps, mapDispatchToProps)(AddGameContainer);
    

    Now try to add a game in http://localhost:3000, or first run yarn build and serve the page from Node.js at http://localhost:8080!

    现在,尝试在http:// localhost:3000中添加游戏,或者首先run yarn build并从http:// localhost:8080的 Node.js提供页面!

    结论 ( Conclusions )

    In this second part of the tutorial we defined a single state and decoupled from the containers/components logic.

    在本教程的第二部分中,我们定义了一个状态,并与容器/组件逻辑分离。

    To do so we use Redux so that we have a reducer to intercept actions and always provide a new state to the app. We also included Redux-saga to control all the async behavior of our app (HTTP requests and Filestack uploader).

    为此,我们使用Redux,以便我们有一个reducer来拦截操作并始终为应用提供新状态。 我们还包括Redux-saga,以控制应用程序的所有异步行为(HTTP请求和Filestack上传器)。

    To facilitate this process we covered each step required to integrate Redux: We started to define actions, reducers and sagas to intercept them, created the store and connected to react through Provider component. Finally, we exported connected versions of the containers which are able to read from the state and dispatch actions.

    为了简化此过程,我们介绍了集成Redux所需的每个步骤:我们开始定义操作,reduce和sagas拦截它们,创建商店,并通过Provider组件进行响应。 最后,我们导出了容器的连接版本,这些版本能够从状态读取并分派操作。

    Stay tuned for the third part, the bonus part where we will improve the UI and add basic authentication!

    请继续关注第三部分,奖金部分,我们将在其中改进用户界面并添加基本身份验证!

    PS: The store I created in my github I compose the saga middleware with redux-dev-tools middleware. You can download the browser extension and checkout the state, all the dispatched actions and much more!

    PS :我在github中创建的商店将saga中间件与redux-dev-tools中间件组成。 您可以下载浏览器扩展并签出状态,所有已调度的动作等等!

    翻译自: https://scotch.io/tutorials/build-a-retrogames-archive-with-node-js-react-redux-and-redux-saga-part2-redux-integration

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值