本文共 62524 字,大约阅读时间需要 208 分钟。
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以及为什么在应用程序中使用它的一些原因:
For more, take a look a the documentation on .
有关更多信息,请查看有关的文档。 。
Regarding the project, if you want to start from the part1 and update the project incrementally, you can get the code on 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开始并逐步更新项目,则可以在上获取代码,并签出到tutorial / part1分支以开始编辑代码。 另一方面,您可以直接检出tutorial / part2分支,以获得本教程的确切代码。
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
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 containergetGames () { 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):
我们需要安装一些软件包,让我们从Redux和Immutable开始(我们的状态将是一个不可变的数据结构):
yarn add redux immutable
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/gamesimport { GET_GAMES, GET_GAMES_SUCCESS, GET_GAMES_FAILURE} from '../constants/games';// GET_GAMES function will be dispatched within GamesContainerfunction 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 errorsfunction getGamesFailure () { return { type: GET_GAMES_FAILURE };}// we export all the function in a single export commandexport { 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请求终止, getGamesSuccess
和getGamesFailure
返回由reducer函数消化的操作,因为我们要处理成功和失败两种情况。 此外,第一个携带从服务器接收到的游戏列表作为第二个属性。 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.
尽管没有必要为操作类型使用常量,但我建议大型应用程序使用类似的代码组织。
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 handyimport { GET_GAMES_SUCCESS, GET_GAMES_FAILURE} from '../constants/games';// The initial state is just an empty Mapconst initialState = Immutable.Map();// That's a very standard reducer function to return a new state given a dispatched actionexport 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 functionimport { combineReducers } from 'redux-immutable';// Import our reducers function from hereimport games from './games'; // combineReducers merges them all!export default combineReducers({ games});
For now it is just games but we will soon have others.
目前这只是游戏,但我们很快就会推出其他游戏。
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 helperimport { takeLatest} from 'redux-saga';// Saga effects are usesul to interact with the saga middlewareimport { put, call} from 'redux-saga/effects';// As predicted a saga will take care of GET_GAMES actionsimport { GET_GAMES} from '../constants/games';// either one is yielded once the fetch is doneimport { getGamesSuccess, getGamesFailure } from '../actions/games';// We moved the fetch from GamesContainerconst 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 rejectsfunction* getGames () { try { const games = yield call(fetchGames); yield put(getGamesSuccess(games)); } catch (err) { yield put(getGamesFailure()); }}// The watcher saga waits for dispatched GET_GAMES actionsfunction* watchGetGames () { yield takeLatest(GET_GAMES, getGames);}// Export the watcher to be run in parallel in sagas/index.jsexport { watchGetGames};
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 .
但是, 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 createdimport { 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商店,但实际上我们还没有商店。
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 dependenciesimport { createStore, applyMiddleware} from 'redux';import createSagaMiddleware from 'redux-saga';// this comes from our created filesimport rootSaga from './sagas';import reducer from './reducers';// The function in charge of creating and returning the store of the appconst 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
,它执行以下操作:
sagaMiddleware
by calling the run function. 通过调用运行功能启动sagaMiddleware
。 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 Providerimport { Provider } from 'react-redux';// We need the store to be passed to Providerimport configureStore from './store';// All the previous dependencies from Part1import { 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 exportedconst store = configureStore();// Provider wraps our root componentconst routes = ({ /* We pass the store to the 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
,该连接创建了此连接,并允许我们导出一个智能容器。
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-reduximport { connect } from 'react-redux';// bindActionCreators comes handy to wrap action creators in dispatch callsimport { bindActionCreators } from 'redux';import Immutable from 'immutable';import { Modal, GamesListManager } from '../components';// we import the action-creators to be binde with bindActionCreatorsimport * as gamesActionCreators from '../actions/games';// We do not export GamesContainer as it is 'almost' a dumb componentclass 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 (); }}// We can read values from the state thanks to mapStateToPropsfunction 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 sagasfunction mapDispatchToProps (dispatch) { return { gamesActions: bindActionCreators(gamesActionCreators, dispatch) };}// Finally we export the connected GamesContainerexport 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
动作创建者。 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
函数:现在这只是一行,我们称之为动作创建者函数。 我们的传奇将拦截动作并从服务器获取数据! 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 here is the result:
连接到 ,结果如下:
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_BARimport { 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 usersfunction setSearchBar (keyword) { return { type: SET_SEARCH_BAR, keyword };}export { getGames, getGamesSuccess, getGamesFailure, setSearchBar // We export the new action-creators};
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 constantconst 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 (); }}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);
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
不会更改,因为它已经是一个对象,其属性是导出的动作创建者。 setSearchBar
function now dispatches the action to the reducer passing the current search bar value. 现在,GamesContainer setSearchBar
函数会将setSearchBar
分派给传递当前搜索栏值的减速器。 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.
如果您尝试在任何游戏上单击“查看”,则会收到错误消息,我们需要修改模态行为! 该过程非常类似于搜索栏之一。
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 payloadfunction 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 constantconst 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 anymoreclass 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 (); }}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);
selectedGame
from the state to the props, so it's available at this.props.selectedGame
. 现在,我们将selectedGame
从状态映射到道具,因此可以在this.props.selectedGame
上使用它。 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.
我们差不多完成了,我们只需要重写逻辑来删除游戏。
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 buttonfunction deleteGame () { return { type: DELETE_GAME };}// In case of succesful deletion the action is dispatched to the reducerfunction deleteGamesSuccess (games) { return { type: DELETE_GAME_SUCCESS, games };}// In case of failure the saga dispatches DELETE_GAME_FAILURE insteadfunction 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作为参数。 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 constantsconst 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 watcherimport { 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 stateconst 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 deleteGamefunction* watchDeleteGame () { yield takeLatest(DELETE_GAME, deleteGame);}export { watchGetGames, watchDeleteGame};
watchDeleteGame
saga in charge to intercept the action DELETE_GAME. 我们创建了watchDeleteGame
传奇,负责拦截动作DELETE_GAME 。 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; }}
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 :
让我们尝试在浏览器中删除游戏,只需转到 :
We are almost done but we have to rewrite the AddGameContainer
and Form to use Redux.
我们差不多完成了,但是我们必须重写AddGameContainer
和Form才能使用Redux 。
If you take a look at the code of AddGameContainer
you can immediately figure out what to do:
如果查看一下AddGameContainer
的代码,您可以立即弄清楚该怎么做:
uploadPicture
. The procedure involves moving the server POST request in a saga and perhaps dispatch another action to the reducer. 我们需要一个新动作将游戏发布到服务器并从其功能uploadPicture
分发它。 该过程涉及在一个传奇中移动服务器POST请求,并可能将另一个操作分派给reducer。 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轻松摆脱它。 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 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}
reduxForm
. game是当组件由reduxForm
装饰时指定的表单名称。 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 versionimport { Field, reduxForm } from 'redux-form/immutable';class Form extends PureComponent { render () { const { picture, uploadPicture } = this.props; return (); }}// we named the form game so that in the state we can access it like form.gameexport default reduxForm({ form: 'game' })(Form);BackAdd a Game!
Field
inputs as it listens to actions dispatched from reduxForm
. 我们从Field和reduxForm中包括了:第一个是将字段连接到redux存储的组件,第二个也是一个组件,但是它将Form组件包装在一个高阶组件中。 一旦添加了表单reduxForm
器,我们的状态就会随着Field
输入的更新而更新,因为它监听从reduxForm
派发的reduxForm
。 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 versionimport { 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.
这些是下一步。
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 submitfunction postGame () { return { type: POST_GAME };}// The action is dispatched when the returned promise from a POST request resolvefunction postGameSuccess () { return { type: POST_GAME_SUCCESS };}// In case of failurefunction 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 buttonfunction uploadPicture () { return { type: UPLOAD_PICTURE };}// It carries the picture url to be added to the statefunction uploadPictureSuccess (url) { return { type: UPLOAD_PICTURE_SUCCESS, url };}// In case of failurefunction 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 definitionconst 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 fileconst 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};
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 constantsimport { UPLOAD_PICTURE_SUCCESS, UPLOAD_PICTURE_FAILURE} from '../constants/filestack';// Also import the constants for the post game actionsimport { POST_GAME_SUCCESS, POST_GAME_FAILURE} from '../constants/games';// The initial state is just a Mapconst 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});
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 stateconst 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 gameconst 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 actionsfunction* 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 ). 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
函数:虽然第一个参数是选项的对象,而其他参数都是函数,但我们还有onSuccess , onFailure以及实际上是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 (
Now try to add a game in , or first run yarn build
and serve the page from Node.js at !
现在,尝试在添加游戏,或者首先run yarn build
并从 Node.js提供页面!
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 middleware. You can download the browser extension and checkout the state, all the dispatched actions and much more!
PS :我在github中创建的商店将saga中间件与中间件组成。 您可以下载浏览器扩展并签出状态,所有已调度的动作等等!
翻译自:
转载地址:http://wruwd.baihongyu.com/