使用React,Redux和Redux-saga构建媒体库-第2部分

In the first part of this tutorial, we had a running app. We covered basic React setup, project workflow; defined basic components and configured our application's routes.

在本教程的第一部分中,我们有一个正在运行的应用程序。 我们介绍了基本的React设置,项目工作流程; 定义基本组件并配置应用程序的路由。

In Part 2 of this tutorial, which is unarguably the most interesting part of building React/Redux application, we will setup application state management with redux, connect our React components to the store, and then deploy to Heroku. We will walk through this part in eight steps:

在本教程的第2部分(无疑是构建React / Redux应用程序中最有趣的部分)中,我们将使用redux设置应用程序状态管理,将React组件连接到商店,然后部署到Heroku。 我们将分八步完成本部分:

  1. Define Endpoints of interest.

    定义感兴趣的端点。
  2. Create a container component.

    创建一个容器组件。
  3. Define action creators.

    定义动作创建者。
  4. Setup state management system.

    设置状态管理系统。
  5. Define async task handlers.

    定义异步任务处理程序。
  6. Create presentational components.

    创建演示组件。
  7. Connect our React component to Redux store.

    将我们的React组件连接到Redux商店。
  8. Deploy to Heroku.

    部署到Heroku。

第1步(共8步):定义感兴趣的端点 ( Step 1 of 8: Define Endpoints of interest )

Our interest is the media search endpoints of Flickr API and Shutterstock API.

我们感兴趣的是Flickr API和Shutterstock API的媒体搜索端点。

Api/api.js

api / api.js

const FLICKR_API_KEY = 'a46a979f39c49975dbdd23b378e6d3d5';
const SHUTTER_CLIENT_ID = '3434a56d8702085b9226';
const SHUTTER_CLIENT_SECRET = '7698001661a2b347c2017dfd50aebb2519eda578';

// Basic Authentication for accessing Shutterstock API
const basicAuth = () => 'Basic '.concat(window.btoa(`${SHUTTER_CLIENT_ID}:${SHUTTER_CLIENT_SECRET}`));
const authParameters = {
  headers: {
    Authorization: basicAuth()
  }
};

/**
* Description [Access Shutterstock search endpoint for short videos]
* @params { String } searchQuery
* @return { Array } 
*/
export const shutterStockVideos = (searchQuery) => {
  const SHUTTERSTOCK_API_ENDPOINT = `https://api.shutterstock.com/v2/videos/search?
  query=${searchQuery}&page=1&per_page=10`;

  return fetch(SHUTTERSTOCK_API_ENDPOINT, authParameters)
  .then(response => {
    return response.json();
  })
  .then(json => {
      return json.data.map(({ id, assets, description }) => ({
        id,
        mediaUrl: assets.preview_mp4.url,
        description
      }));
  });
};

/**
* Description [Access Flickr search endpoint for photos]
* @params { String } searchQuery
* @return { Array } 
*/
export const flickrImages = (searchQuery) => {
  const FLICKR_API_ENDPOINT = `https://api.flickr.com/services/rest/?method=flickr.photos.search&text=${searchQuery}&api_key=${FLICKR_API_KEY}&format=json&nojsoncallback=1&per_page=10`;

  return fetch(FLICKR_API_ENDPOINT)
    .then(response => {
      return response.json()
    })
    .then(json => {
      return json.photos.photo.map(({ farm, server, id, secret, title }) => ({
        id,
        title,
        mediaUrl: `https://farm${farm}.staticflickr.com/${server}/${id}_${secret}.jpg`
      }));
    });
};

First, head to Flickr and Shutterstock to get your credentials or use mine.

首先,前往FlickrShutterstock获得您的证书或使用我的证书。

We’re using fetch method *from *fetch API for our AJAX request. It returns a promise that resolves to the response of such request. We simply format the response of our call using ES6 destructuring assignment before returning to the store.

我们正在使用* fetch API中的*提取方法来处理我们的AJAX请求。 它返回一个解决此类请求响应的承诺。 在返回商店之前,我们只需使用ES6销毁分配来格式化调用的响应。

We can use jQuery for this task but it’s such a large library with many features, so using it just for AJAX doesn’t make sense.

我们可以使用jQuery来完成此任务,但是它是一个具有许多功能的大型库,因此仅将其用于AJAX是没有意义的。

第2步,共8步:创建一个容器组件 ( Step 2 of 8: Create a container component )

In order to test our application as we walk through the steps, let's define a MediaGalleryPage component which we will update later for a real time sync with our store.

为了在逐步执行过程中测试我们的应用程序,让我们定义一个MediaGalleryPage组件,稍后我们将对其进行更新以与商店实时同步。

container/MediaGalleryPage.js

容器/MediaGalleryPage.js

import React, { Component } from 'react';
import { flickrImages, shutterStockVideos } from '../Api/api';

// MediaGalleryPage Component
class MediaGalleryPage extends Component {

 // We want to get images and videos from the API right after our component renders.
 componentDidMount() {
    flickrImages('rain').then(images => console.log(images, 'Images'));
    shutterStockVideos('rain').then(videos => console.log(videos,'Videos'));
  }

  render() {
  // TODO: Render videos and images here
  return (<div></div>)
  }
}

export default MediaGalleryPage;

We can now add library route and map it to MediaGalleryPage Container.

现在,我们可以添加库路由并将其映射到MediaGalleryPage Container。

Let's update out routes.js for this feature.

让我们更新此功能的routes.js

import React from 'react';
import { Route, IndexRoute } from 'react-router';
import App from './containers/App';
import HomePage from './components/HomePage';
import MediaGalleryPage from './containers/MediaGalleryPage';

// Map components to different routes.
// The parent component wraps other components and thus serves as 
// the entrance to other React components.
// IndexRoute maps HomePage component to the default route
export default (
  <Route path="/" component={App}>
    <IndexRoute component={HomePage} />
    <Route path="library" component={MediaGalleryPage} />
  </Route>
);

Let's check it out on the browser console.

让我们在浏览器控制台上检查一下。

Images and Videos from the API

We are now certain that we can access our endpoints of interest to fetch images and short videos. We can render the results to the view but we want to separate our React components from our state management system. Some major advantages of this approach are maintainability, readability, predictability, and testability.

现在我们确定可以访问感兴趣的端点以获取图像和短视频。 我们可以将结果呈现给视图,但是我们想将React组件与状态管理系统分开。 这种方法的一些主要优点是可维护性,可读性,可预测性和可测试性。

We will be wrapping our heads around some vital concepts in a couple of steps.

我们将分两步围绕一些重要概念进行探讨。

第3步,共8步:定义动作创建者 ( Step 3 of 8: Define action creators )

Action creators are functions that return plain Javascript object of action type and an optional payload. So action creators create actions that are dispatched to the store. They are just pure functions.

动作创建者是返回动作类型的纯Javascript对象和可选有效负载的函数。 因此,动作创建者可以创建分配到商店的动作。 它们只是纯函数。

Let’s first define our action types in a file and export them for ease of use in other files. They’re constants and it’s a good practice to define them in a separate file(s).

首先,让我们在文件中定义操作类型,然后将其导出以方便在其他文件中使用。 它们是常量,在一个单独的文件中定义它们是一个好习惯。

constants/actionTypes.js

constants / actionTypes.js

// It's preferable to keep your action types together.
export const SELECTED_IMAGE = 'SELECTED_IMAGE';
export const FLICKR_IMAGES_SUCCESS = 'FLICKR_IMAGES_SUCCESS';
export const SELECTED_VIDEO = 'SELECTED_VIDEO';
export const SHUTTER_VIDEOS_SUCCESS = 'SHUTTER_VIDEOS_SUCCESS';
export const SEARCH_MEDIA_REQUEST = 'SEARCH_MEDIA_REQUEST';
export const SEARCH_MEDIA_SUCCESS = 'SEARCH_MEDIA_SUCCESS';
export const SEARCH_MEDIA_ERROR = 'SEARCH_MEDIA_ERROR';

Now, we can use the action types to define our action creators for different actions we need.

现在,我们可以使用动作类型为所需的不同动作定义动作创建者。

actions/mediaActions.js

actions / mediaActions.js

import * as types from '../constants/actionTypes';

// Returns an action type, SELECTED_IMAGE and the image selected
export const selectImageAction = (image) => ({
  type: types.SELECTED_IMAGE,
  image
});

// Returns an action type, SELECTED_VIDEO and the video selected
export const selectVideoAction = (video) => ({
  type: types.SELECTED_VIDEO,
  video
});

// Returns an action type, SEARCH_MEDIA_REQUEST and the search criteria
export const searchMediaAction = (payload) => ({
  type: types.SEARCH_MEDIA_REQUEST,
  payload
});

The optional arguments in the action creators: payload, image, and video are passed at the site of call/dispatch. Say, a user selects a video clip on our app, selectVideoAction is dispatched which returns SELECTED_VIDEO action type and the selected video as payload. Similarly, when searchMediaAction is dispatched, SEARCH_MEDIA_REQUEST action type and payload are returned.

动作创建者中的可选参数: payloadimagevideo在调用/调度站点传递。 假设用户在我们的应用上选择了一个视频剪辑, 然后调度了selectVideoAction ,该方法返回SELECTED_VIDEO动作类型和所选视频作为有效内容。 同样,当searchMediaAction被分派,SEARCH_MEDIA_REQUEST动作类型和有效载荷返回。

第8步,共4步:设置状态管理系统 ( Step 4 of 8: Setup state management system )

We have defined the action creators we need and it's time to connect them together. We will setup our reducers and configure our store in this step.

我们已经定义了所需的动作创建者,现在是将它们联系在一起的时候了。 我们将在此步骤中设置减速器并配置商店。

There are some wonderful concepts here as shown in the diagram in Part 1.

第1部分中的图所示,这里有一些很棒的概念。

Let's delve into some definitions and implementations.

让我们深入研究一些定义和实现。

The Store holds the whole state tree of our application but more importantly, it does nothing to it. When an action is dispatched from a React component, it delegates the reducer but passing the current state tree alongside the action object. It only updates its state after the reducer returns a new state.

商店拥有我们应用程序的整个状态树,但更重要的是,它没有执行任何操作。 从React组件分派动作时,它委派了reducer,但将当前状态树与动作对象一起传递。 它仅在化简器返回新状态后才更新其状态。

*Reducers, for short are pure functions that accept the state tree and an action object from the store and returns a new state. No state mutation. No API calls. No side effects. It simply calculates the new state and returns it to the store.*

* Reducers简称为纯函数,它们接受存储中的状态树和操作对象并返回新状态。 没有状态突变。 没有API调用。 没有副作用。 它仅计算新状态并将其返回到商店。*

Let’s wire up our reducers by first setting our initial state. We want to initialize images and videos as an empty array in our own case.

让我们首先设置初始状态来连接减速器。 在我们自己的情况下,我们想将图像和视频初始化为一个空数组。

reducers/initialState.js

reducers / initialState.js

export default {
  images: [],
  videos: []
};

Our reducers take the current state tree and an action object and then evaluate and return the outcome.

我们的化简器采用当前状态树和一个动作对象,然后评估并返回结果。

Let’s check it out.

让我们来看看。

reducers/imageReducer.js

reducers / imageReducer.js

import initialState from './initialState';
import * as types from '../constants/actionTypes';

// Handles image related actions
export default function (state = initialState.images, action) {
  switch (action.type) {
    case types.FLICKR_IMAGES_SUCCESS:
      return [...state, action.images];
    case types.SELECTED_IMAGE:
      return { ...state, selectedImage: action.image };
    default:
      return state;
  }
}

reducers/videoReducer.js

reducers / videoReducer.js

import initialState from './initialState';
import * as types from '../constants/actionTypes';

// Handles video related actions
// The idea is to return an updated copy of the state depending on the action type.
export default function (state = initialState.videos, action) {
  switch (action.type) {
    case types.SHUTTER_VIDEOS_SUCCESS:
      return [...state, action.videos];
    case types.SELECTED_VIDEO:
      return { ...state, selectedVideo: action.video };
    default:
      return state;
  }
}

The two reducers look alike and that’s how simple reducers can be. We use a switch statement to evaluate an action type and then return a new state.

这两个减速器看起来很相似,那就是简单的减速器。 我们使用switch语句评估操作类型,然后返回新状态。

create-react-app comes preinstalled with *babel-plugin-transform-object-rest-spread* that lets you use the spread (…) operator to copy enumerable properties from one object to another in a succinct way.

create-react-app预先安装了* babel-plugin-transform-object-rest-spread *,它使您可以使用span(…)运算符以简洁的方式将可枚举的属性从一个对象复制到另一个对象。

For context, { …state, videos: action.videos } evaluates to Object.assign({}, state, action.videos).

对于上下文, {…状态,视频:action.videos}的计算结果为Object.assign({},状态,action.videos)。

Since reducers don’t mutate state, you would always find yourself using spread operator, to make and update the new copy of the current state tree.

由于reducer不会改变状态,因此您总是会发现自己使用散布运算符来创建和更新当前状态树的新副本。

So, When the reducer receives SELECTED_VIDEO action type, it returns a new copy of the state tree by spreading it(…state) and updating the selectedVideo property.

因此,当化SELECTED_VIDEO器接收到SELECTED_VIDEO操作类型时,它通过扩展状态树( …state )并更新selectedVideo属性来返回状态树的新副本。

The next step is to register our reducers to a root reducer before passing to the store.

下一步是在传递到商店之前,将我们的reduces注册到root reducer。

reducers/index.js

reducers / index.js

import { combineReducers } from 'redux';
import images from './imageReducer';
import videos from './videoReducer';

// Combines all reducers to a single reducer function
const rootReducer = combineReducers({
  images, 
  videos
});

export default rootReducer;

We import combineReducers from Redux. CombineReducers is a helper function that combines our images and videos reducers into a single reducer function that we can now pass to the creatorStore function.

我们从Redux导入CombineReducers 。 CombineReducers是一个辅助函数,它将我们的图像视频缩减器组合为单个缩减器函数,我们现在可以将其传递给creatorStore函数。

You might be wondering why we’re not passing in key/value pairs to combineReducers function. Yes, you’re right. ES6 allows us to pass in just the property if the key and value are the same.

您可能想知道为什么我们不将键/值对传递给CombineReducers函数。 你是对的。 如果键和值相同,则ES6允许我们仅传递属性。

Now, we can complete our state management system by creating the store for our app.

现在,我们可以通过为我们的应用创建商店来完善我们的状态管理系统。

store/configureStore.js

store / configureStore.js

import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from '../reducers';
import rootSaga from '../sagas'; // TODO: Next step

//  Returns the store instance
// It can  also take initialState argument when provided
const configureStore = () => {
  const sagaMiddleware = createSagaMiddleware(); 
  return {
    ...createStore(rootReducer,
      applyMiddleware(sagaMiddleware)),
    runSaga: sagaMiddleware.run(rootSaga)
  };
};

export default configureStore;
  • Initialize your SagaMiddleWare. We’d discuss about sagas in the next step.

    初始化您的SagaMiddleWare。 我们将在下一步中讨论有关sagas的问题。
  • Pass rootReducer and sagaMiddleware to the createStore function to create our redux store.

    将rootReducer和sagaMiddleware传递给createStore函数以创建我们的redux存储。
  • Finally, we run our sagas. You can either spread them or wire them up to a rootSaga.

    最后,我们运行我们的sagas。 您可以传播它们或将它们连接到rootSaga。

What are sagas and why use a middleware?

什么是sagas,为什么要使用中间件?

第5步(共8步):定义异步任务处理程序 ( Step 5 of 8: Define async task handlers )

Handling AJAX is a very important aspect of building web applications and React/Redux application is not an exception. We will look at the libraries to leverage for such task and how they neatly fit into the whole idea of having a state management system.

处理AJAX是构建Web应用程序的一个非常重要的方面,React / Redux应用程序也不例外。 我们将研究可用于此类任务的库,以及它们如何巧妙地适应拥有状态管理系统的整个想法。

You would remember reducers are pure functions and don’t handle side effects or async tasks; this is where redux-saga comes in handy.

您会记得,reducers是纯函数,不处理副作用或异步任务; 这是redux-saga派上用场的地方。

*redux-saga is a library that aims to make side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) in React/Redux applications easier and better — documentation.*

* redux-saga是一个库,旨在使React / Redux应用程序中的副作用(例如,异步的事情,例如数据获取和不纯净的事情,例如访问浏览器缓存)变得更加容易和完善- 文档

So, to use redux-saga, we also need to define our own sagas that will handle the necessary async tasks.

因此,要使用redux-saga,我们还需要定义自己的sagas,以处理必要的异步任务。

What the heck are sagas?

萨加斯到底是什么?

Sagas are simply generator functions that abstract the complexities of an asynchronous workflow. It’s a terse way of handling async processes. It’s easy to write, test and reason. Still confused, you might want to revisit the first part of this tutorial if you missed it.

Sagas只是生成器函数,可以抽象出异步工作流程的复杂性。 这是处理异步进程的简洁方法。 这很容易编写,测试和推理。 仍然很困惑,如果您错过了本教程的第一部分,则可能需要重新阅读。

Let’s first define our watcher saga and work down smoothly.

让我们首先定义观察者的传奇故事,并顺利进行工作。

sagas/watcher.js

sagas / watcher.js

import { takeLatest } from 'redux-saga/effects';
import { searchMediaSaga } from './mediaSaga';
import * as types from '../constants/actionTypes';

// Watches for SEARCH_MEDIA_REQUEST action type asynchronously
export default function* watchSearchMedia() {
  yield takeLatest(types.SEARCH_MEDIA_REQUEST, searchMediaSaga);
}

We want a mechanism that ensures any action dispatched to the store which requires making API call is intercepted by the middleware and result of request yielded to the reducer.

我们需要一种机制,以确保任何分发给商店的,需要进行API调用的操作都被中间件拦截,并将请求结果传递给reducer。

To achieve this, Redux-saga API exposes some methods. We need only four of those for our app: call, put, fork and takeLatest.

为此,Redux-saga API公开了一些方法。 我们的应用程序只需要其中四个: callputforktakeLatest

  • takeLatest is a high-level method that merges take and fork effect creators together. It basically takes an action type and runs the function passed to it in a non-blocking manner with the result of the action creator. As the name suggests, takeLatest returns the result of the last call.

    takeLatest是一个高层次的方法,该方法合并采取效果创作者在一起。 它基本上采用一个动作类型,并以动作创建者的结果以非阻塞方式运行传递给它的函数。 顾名思义, takeLatest返回上一次调用的结果。
  • watchSearchMedia watches for SEARCH_MEDIA_REQUEST action type and call searchMediaSaga function(saga) with the action’s payload from the action creator.

    watchSearchMedia监视 SEARCH_MEDIA_REQUEST操作类型,并使用操作创建者提供的操作有效负载调用searchMediaSaga函数(saga)。

Now, we can define searchMediaSaga; it serves as a middleman to call our API. Getting interesting right?

现在,我们可以定义searchMediaSaga ; 它充当调用我们的API的中间人。 变得有趣了吧?

sagas/mediaSaga.js

sagas / mediaSaga.js

import { put, call } from 'redux-saga/effects';
import { flickrImages, shutterStockVideos } from '../Api/api';
import * as types from '../constants/actionTypes';

// Responsible for searching media library, making calls to the API
// and instructing the redux-saga middle ware on the next line of action,
// for success or failure operation.
export function* searchMediaSaga({ payload }) {
  try {
    const videos = yield call(shutterStockVideos, payload);
    const images = yield call(flickrImages, payload);
    yield [
      put({ type: types.SHUTTER_VIDEOS_SUCCESS, videos }),
      put({ type: types.SELECTED_VIDEO, video: videos[0] }),
      put({ type: types.FLICKR_IMAGES_SUCCESS, images }),
      put({ type: types.SELECTED_IMAGE, image: images[0] })
    ];
  } catch (error) {
    yield put({ type: 'SEARCH_MEDIA_ERROR', error });
  }
}

searchMediaSaga is not entirely different from normal functions except the way it handles async tasks.

searchMediaSaga除了处理异步任务的方式外,与正常功能并不完全不同。

call is a redux-saga effect that instructs the middleware to run a specified function with an optional payload.

call是redux-saga效果,它指示中间件运行带有可选有效负载的指定函数。

Let’s do a quick review of some happenings up there.

让我们快速回顾一下那里发生的事情。

  • searchMediaSaga is called by the watcher saga defined earlier on each time SEARCH_MEDIA_REQUEST is dispatched to store.

    searchMediaSaga由早前在每次定义的守望者传奇叫SEARCH_MEDIA_REQUEST被分派到店。
  • It serves as an intermediary between the API and the reducers.

    它充当API和reducer之间的中介。

  • So, when the saga(searchMediaSaga) is called, it makes a call to the API with the payload. Then, the result of the promise(resolved or rejected) and an action object is yielded to the reducer using put effect creator. put instructs Redux-saga middleware on what action to dispatch.

    因此,当调用saga( searchMediaSaga )时,它将使用有效负载来调用 API。 然后,使用放置效果创建器将promise(已解决或拒绝)和操作对象的结果提供给reducer。 put指示Redux-saga中间件调度要执行的操作。
  • Notice, we’re yielding an array of effects. This is because we want them to run concurrently. The default behaviour would be to pause after each yield statement which is not the behaviour we intend.

    注意,我们产生了一系列效果。 这是因为我们希望它们同时运行。 默认行为是在每个yield语句之后暂停,这不是我们想要的行为。
  • Finally, if any of the operations fail, we yield a failure action object to the reducer.

    最后,如果任何操作失败,我们会向减速器产生一个失败动作对象。

Let’s wrap up this section by registering our saga to the rootSaga.

让我们通过将我们的传奇注册到rootSaga来结束本节。

sagas/index.js

sagas / index.js

import { fork } from 'redux-saga/effects';
import watchSearchMedia from './watcher';

// Here, we register our watcher saga(s) and export as a single generator 
// function (startForeman) as our root Saga.
export default function* startForman() {
  yield fork(watchSearchMedia);
}

fork is an effect creator that provisions the middleware to run a non-blocking call on watchSearchMedia saga.

fork是一个效果创建者,提供了中间件以在watchSearchMedia传奇上运行非阻塞调用。

Here, we can bundle our watcher sagas as an array and yield them at once if we have more than one.

在这里,我们可以将观察者sagas捆绑成一个数组,如果我们有多个,则立即产生它们。

Hope by now, you are getting comfortable with the workflow. So far, we’re able to export startForman as our rootSaga.

希望现在,您对工作流程感到满意。 到目前为止,我们可以将startForman导出为我们的rootSaga

How does our React component know what is happening in the state management system?

我们的React组件如何知道状态管理系统中发生了什么?

第8步,共6步:将我们的React组件连接到redux商店 ( Step 6 of 8: Connect our React component to redux store )

I’m super excited that you’re still engaging and we’re about testing our app.

我很高兴您仍然参与其中,我们即将测试我们的应用程序。

First, let’s update our index.js * app’s entry file.*

首先,让我们更新index.js * -应用程序的入口文件。*

import ReactDOM from 'react-dom';
import React from 'react';
import { Router, browserHistory } from 'react-router';
import { Provider } from 'react-redux';  
import configureStore from './store/configureStore';
import routes from './routes';

// Initialize store
const store = configureStore();

ReactDOM.render(
  <Provider store={store}>
    <Router history={browserHistory} routes={routes} />
  </Provider>, document.getElementById('root')
);

Let’s review what’s going on.

让我们回顾发生了什么。

  • Initialize our store.

    初始化我们的商店。
  • Provider component from react-redux makes the store available to the components hierarchy. So, we have to pass the store as props to it. That way, the components below the hierarchy can access the store’s state with connect method call.

    react-redux的 提供程序组件使存储可用于组件层次结构。 因此,我们必须将商店作为道具传递。 这样,层次结构下面的组件可以使用connect方法调用访问商店的状态。

Now, let's update our MediaGalleryPage component to access the store.

现在,让我们更新MediaGalleryPage组件以访问商店。

container/MediaGalleryPage.js

容器/MediaGalleryPage.js

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

// MediaGalleryPage Component
class MediaGalleryPage extends Component {

  // Dispatches *searchMediaAction*  immediately after initial rendering.
  // Note that we are using the dispatch method from the store to execute this task, courtesy of react-redux
 componentDidMount() {
    this.props.dispatch(searchMediaAction('rain'));
  }

  render() {
    console.log(this.props.images, 'Images');
    console.log(this.props.videos, 'Videos');
    console.log(this.props.selecteImage, 'SelectedImage');
    console.log(this.props.selectedVideo, 'SelectedVideo');
    return (<div> </div>)
  }
}

// Define PropTypes
MediaGalleryPage.propTypes = {
// Define your PropTypes here
};

 // Subscribe component to redux store and merge the state into 
 // component's props
const mapStateToProps = ({ images, videos }) => ({
  images: images[0],
  selectedImage: images.selectedImage,
  videos: videos[0],
  selectedVideo: videos.selectedVideo
});

// connect method from react-router connects the component with redux store
export default connect(
  mapStateToProps)(MediaGalleryPage);

MediaGalleryPage component serves two major purposes:

MediaGalleryPage组件主要用于两个目的:

a) Sync React Components with the Redux store.

a)将React Components与Redux存储同步。

b) Pass props to our presentational components: PhotoPage and VideoPage. We will create this later in the tutorial to render our content to the page.

b)将道具传递到我们的演示组件: PhotoPageVideoPage 。 我们将在本教程的后面部分创建此内容,以将我们的内容呈现到页面上。

Let’s summarize what's going on.

让我们总结一下发生了什么。

React-redux exposes two important methods(components) we will use to bind our redux store to our component - connect and Provider.

React-redux暴露了两个重要的方法(组件),我们将使用它们将我们的redux存储绑定到我们的组件: connectProvider。

connect takes three optional functions. If any is not defined, it takes the default implementation. It’s a function that returns a function that takes our React component-MediaGalleryPage as an argument.

connect具有三个可选功能。 如果未定义,则采用默认实现。 这个函数返回一个以我们的React组件MediaGalleryPage作为参数的函数。

mapStateToProps allows us keep in sync with store's updates and to format our state values before passing as props to the React component. We use ES6 *destructuring assignment* to extract images and videos from the store’s state.

mapStateToProps允许我们与商店的更新保持同步并格式化状态值,然后再作为道具传递给React组件。 我们使用ES6 * 解构分配 *从商店状态中提取图像和视频。

Now, everthing is good and we can test our application.

现在,一切都很好,我们可以测试我们的应用程序。

$npm start

You can grab a cup of coffee and be proud of yourself.

您可以喝杯咖啡,为自己感到骄傲。

Wouldn't it be nice if we can render our result on the webpage as supposed to viewing them on the browser console?

如果我们可以按照在浏览器控制台上查看结果的方式在网页上呈现结果,那不是很好吗?

第7步,共8步:创建演示组件 ( Step 7 of 8: Create presentational components )

What are presentational components?

什么是演示组件?

They are basically components that are concerned with presentation - how things look. Early in this tutorial, we created a container component - MediaGalleryPage which will be concerned with passing data to these presentational components. This is a design decision which has helped large applications scale efficiently. However, it's at your discretion to choose what works for your application.

它们基本上是与表示有关的组件- 外观。 在本教程的开头 ,我们创建了一个容器组件-MediaGalleryPage ,该组件将与将数据传递给这些演示组件有关。 这是一项设计决定,已帮助大型应用程序有效地扩展。 但是,您可以选择适合自己的应用程序。

Our task is now easier. Let's create the two React components to handle images and the videos.

现在,我们的任务更加轻松。 让我们创建两个React组件来处理图像和视频。

components/PhotoPage.js

组件/PhotoPage.js

import React, { PropTypes } from 'react';

// First, we extract images, onHandleSelectImage, and selectedImage from 
// props using ES6 destructuring assignment and then render.
const PhotosPage = ({ images, onHandleSelectImage, selectedImage }) => (
  <div className="col-md-6">
    <h2> Images </h2>
    <div className="selected-image">
      <div key={selectedImage.id}>
        <h6>{selectedImage.title}</h6>
        <img src={selectedImage.mediaUrl} alt={selectedImage.title} />
      </div>
    </div>
    <div className="image-thumbnail">
      {images.map((image, i) => (
        <div key={i} onClick={onHandleSelectImage.bind(this, image)}>
          <img src={image.mediaUrl} alt={image.title} />
        </div>
      ))}
    </div>
  </div>
);

// Define PropTypes
PhotosPage.propTypes = {
  images: PropTypes.array.isRequired,
  selectedImage: PropTypes.object,
  onHandleSelectImage: PropTypes.func.isRequired
};

export default PhotosPage;

components/VideoPage.js

组件/VideoPage.js

import React, { PropTypes } from 'react';

// First, we extract videos, onHandleSelectVideo, and selectedVideo 
// from props using destructuring assignment and then render.
const VideosPage = ({ videos, onHandleSelectVideo, selectedVideo }) => (
  <div className="col-md-6">
    <h2> Videos </h2>
    <div className="select-video">
      <div key={selectedVideo.id}>
        <h6 className="title">{selectedVideo.description}</h6>
        <video controls src={selectedVideo.mediaUrl} alt={selectedVideo.title} />
      </div>
    </div>
    <div className="video-thumbnail">
      {videos.map((video, i) => (
        <div key={i} onClick={onHandleSelectVideo.bind(this, video)}>
          <video controls src={video.mediaUrl} alt={video.description} />
        </div>
      ))}
    </div>
  </div>
);

// Define PropTypes
VideosPage.propTypes = {
  videos: PropTypes.array.isRequired,
  selectedVideo: PropTypes.object.isRequired,
  onHandleSelectVideo: PropTypes.func.isRequired
};

export default VideosPage;

The two React components are basically the same except that one handles images and the other is responsible for rendering our videos.

这两个React组件基本相同,除了一个组件负责处理图像,另一个负责渲染视频。

Let's now update our MediaGalleryPage to pass props to this components.

现在,让我们更新MediaGalleryPage,以将道具传递给此组件。

container/MediaGalleryPage.js

容器/MediaGalleryPage.js

import React, { PropTypes, Component } from 'react';
import { connect } from 'react-redux';
import {
  selectImageAction, searchMediaAction,
  selectVideoAction } from '../actions/mediaActions';
import PhotoPage from '../components/PhotoPage';
import VideoPage from '../components/VideoPage';
import '../styles/style.css';

// MediaGalleryPage Component
class MediaGalleryPage extends Component {
  constructor() {
    super();
    this.handleSearch = this.handleSearch.bind(this);
    this.handleSelectImage = this.handleSelectImage.bind(this);
    this.handleSelectVideo = this.handleSelectVideo.bind(this);
  }

  // Dispatches *searchMediaAction*  immediately after initial rendering
 componentDidMount() {
    this.props.dispatch(searchMediaAction('rain'));
  }

  // Dispatches *selectImageAction* when any image is clicked
  handleSelectImage(selectedImage) {
    this.props.dispatch(selectImageAction(selectedImage));
  }

  // Dispatches *selectvideoAction* when any video is clicked
  handleSelectVideo(selectedVideo) {
    this.props.dispatch(selectVideoAction(selectedVideo));
  }

  // Dispatches *searchMediaAction* with query param.
  // We ensure action is dispatched to the store only if query param is provided.
  handleSearch(event) {
    event.preventDefault();
    if (this.query !== null) {
      this.props.dispatch(searchMediaAction(this.query.value));
      this.query.value = '';
    }
  }

  render() {
    const { images, selectedImage, videos, selectedVideo } = this.props;
    return (
      <div className="container-fluid">
        {images && selectedImage? <div>
          <input
            type="text"
            ref={ref => (this.query = ref)}
          />
          <input
            type="submit"
       className="btn btn-primary"
            value="Search Library"
            onClick={this.handleSearch}
          />
          <div className="row">
            <PhotoPage
              images={images}
              selectedImage={selectedImage}
              onHandleSelectImage={this.handleSelectImage}
            />
            <VideoPage
              videos={videos}
              selectedVideo={selectedVideo}
              onHandleSelectVideo={this.handleSelectVideo}
            />
          </div>
        </div> : 'loading ....'}
      </div>
    );
  }
}

// Define PropTypes
MediaGalleryPage.propTypes = {
  images: PropTypes.array,
  selectedImage: PropTypes.object,
  videos: PropTypes.array,
  selectedVideo: PropTypes.object,
  dispatch: PropTypes.func.isRequired
};

 // Subscribe component to redux store and merge the state into component's props
const mapStateToProps = ({ images, videos }) => ({
  images: images[0],
  selectedImage: images.selectedImage,
  videos: videos[0],
  selectedVideo: videos.selectedVideo
});

// connect method from react-router connects the component with redux store
export default connect(
  mapStateToProps)(MediaGalleryPage);

This is pretty much how our final MediaGalleryPage component looks like. We can now render our images and videos to the webpage.

这几乎就是我们最终的MediaGalleryPage组件的样子。 现在,我们可以将图像和视频渲染到网页上。

Let's recap on the latest update on MediaGalleryPage component.

让我们回顾一下MediaGalleryPage组件的最新更新。

render method of our component is very interesting. Here we’re passing down props from the store and the component’s custom functions(handleSearch, handleSelectVideo, handleSelectImage) to the presentational components — PhotoPage and VideoPage.

我们组件的render方法非常有趣。 在这里,我们将道具从商店和组件的自定义功能( handleSearchhandleSelectVideohandleSelectImage )传递给演示组件— PhotoPageVideoPage

This way, our presentational components are not aware of the store. They simply take their behaviour from MediaGalleryPage component and render accordingly.

这样,我们的演示组件就不会知道商店。 他们只是从MediaGalleryPage组件获取其行为并进行相应渲染。

Each of the custom functions dispatches an action to the store when called. We use ref to save a callback that would be executed each time a user wants to search the library.

每个自定义函数在调用时都会将操作分派给商店。 我们使用ref保存一个回调,该回调将在每次用户想要搜索库时执行。

componentDidMount Lifecycle method is meant to allow dynamic behaviour, side effects, AJAX, etc. We want to render search result for rain once a user navigates to library route.

componentDidMount生命周期方法旨在允许动态行为,副作用,AJAX等。一旦用户导航到图书馆路线,我们希望呈现搜索结果以防雨淋

One more thing. We bound all the custom functions in the component’s constructor function with .bind() ** method. It’s simply Javascript. **bind() allows you to create a function out of regular functions. The first argument to it is the context(this, in our own case) to which you want to bind your function. Any other argument will be passed to such function that’s bounded.

还有一件事。 我们使用.bind()**方法绑定了组件构造函数中的所有自定义函数 它只是Javascript。 ** bind()允许您从常规函数之外创建函数。 它的第一个参数是您要将函数绑定到的上下文在我们自己的情况下为此 上下文 )。 任何其他参数都将传递给有界的此类函数。

We’re done building our app. Surprised?

我们已经完成了我们的应用程序的构建。 惊讶吗

Let’s test it out…

让我们测试一下...

$npm start

第8步(共8步):部署到Heroku ( Step 8 of 8: Deploy to Heroku )

Now that our app works locally, let’s deploy to a remote server for our friends to see and give us feedback. Heroku’s free plan will suffice.

现在,我们的应用程序可以在本地运行,现在让我们部署到远程服务器上,供我们的朋友查看并向我们提供反馈。 Heroku的免费计划就足够了。

We want to use create-react-app’s build script to bundle our app for production.

我们想使用create-react-app的构建脚本来捆绑我们的应用以进行生产。

$npm run build

A build/ folder is now in our project directory. That’s the minified static files we will deploy.

现在,我们的项目目录中有一个build /文件夹。 那就是我们将部署的最小化的静态文件。

Next step is to add another script command to our package.json to help us serve our build files with a static file server. We will use pushstate-server(a static file server) to serve our files.

下一步是向我们的package.json添加另一个脚本命令,以帮助我们通过静态文件服务器提供构建文件。 我们将使用pushstate-server (静态文件服务器)来提供文件。

"deploy": "npm install -g pushstate-server && pushstate-server build"

Let's create Procfile in our project's root directory and add: web: npm run deploy

让我们在项目的根目录中创建Procfile并添加: web: npm run deploy

Procfile instructs Heroku on how to run your application.

Procfile指导Heroku如何运行您的应用程序。

Now, let’s create our app on Heroku. You need to register first on their platform. Then, download and install *Heroku toolbelt * if it’s not installed on your system. This will allow us to deploy our app from our terminal.

现在,让我们在Heroku上创建我们的应用程序。 您需要先在他们的平台上注册。 然后,下载并安装* Heroku工具带 *(如果您的系统上未安装)。 这将使我们能够从终端部署应用程序。

$ heroku login# Enter your credentials as it prompts you
$ heroku create <app name> # If  you don't specify a name, Heroku creates one for you.
$ git add --all && git commit -m "Add comments"  # Add and commit your changes.
$ git push heroku master # Deploy to Heroku
$ heroku ps:scale web=1 # Initialize one instance of your app
$ heroku open # Open your app in your default browser

We did it. Congrats. You’ve built and deployed a React/Redux application elegantly. It can be that simple.

我们做到了。 恭喜。 您已经优雅地构建和部署了React / Redux应用程序。 可能就这么简单。

Now some recommended tasks.

现在,一些建议的任务。

  1. Add tests for your application.

    为您的应用程序添加测试。
  2. Add error handling functionality.

    添加错误处理功能。
  3. Add a loading spinner for API calls for good user experience.

    为API调用添加加载微调器,以获得良好的用户体验。
  4. Add sharing functionality to allow users share on their social media walls

    添加共享功能以允许用户在其社交媒体墙上共享
  5. Allow users to like an image.

    允许用户喜欢图片。

结论 ( Conclusion )

It’s apparent that some things were left out, some intentionally. However, you can improve the app for your learning. The key takeaway is to separate your application state from your React components. Use redux-saga to handle any AJAX requests and don’t put AJAX in your React components. Good luck!!!

显然,有些事情是故意遗漏的。 但是,您可以改进应用程序以进行学习。 关键要点是将应用程序状态与React组件分开。 使用redux-saga处理任何AJAX请求,不要将AJAX放入React组件中。 祝好运!!!

翻译自: https://scotch.io/tutorials/build-a-media-library-with-react-redux-and-redux-saga-part-2

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: React-saga和React-thunk都是用于处理异步操作的中间件。 React-thunk是Redux官方推荐的中间件之一,它允许我们在Redux中编写异步操作,使得我们可以在Redux中处理异步操作,而不需要在组件中处理异步操作。Thunk是一个函数,它接收dispatch和getState作为参数,并返回一个函数,这个函数接收dispatch作为参数,并在异步操作完成后调用dispatch。 React-saga是另一个处理异步操作的中间件,它使用了ES6的Generator函数来处理异步操作。Saga使用了一种称为Effect的概念,它是一个简单的JavaScript对象,用于描述异步操作。Saga使用了一种称为yield的语法,它允许我们在Generator函数中暂停异步操作,并在异步操作完成后继续执行。 总的来说,React-thunk和React-saga都是用于处理异步操作的中间件,它们的实现方式不同,但都可以让我们在Redux中处理异步操作。选择哪种中间件取决于个人的喜好和项目的需求。 ### 回答2: React-Saga和React-Thunk都是React应用中用于处理异步操作的中间件。它们的主要目的是在Redux应用中,帮助我们管理异步操作。这两个中间件都可以让React应用更加的灵活、健壮和易于维护。 React-Saga的核心理念是利用生成器函数来处理异步操作,Saga通过使用生成器来让异步操作变得像同步操作一样,其中每个异步操作都会被转化为一个迭代器函数,这些函数可以被Saga调用和暂停。 Saga主要有以下几个特点: 1. Saga可以使异步操作更加同步和简单,让异步调用变得更容易。Saga使用了轻量级、高效的生成器函数,从而有效地减少了异步调用中的代码复杂度。 2. Saga可以很好地管理和协调多个异步操作。Saga可以在任意阶段暂停异步操作,等待其他异步操作完成之后再继续执行。 3. Saga可以捕获和控制异步操作的错误、超时和状态。当出现问题时,Saga可以修复错误或者更改异步操作的状态,保证应用程序的稳定性和可靠性。 React-Thunk的核心概念是利用闭包函数来处理异步操作,Thunk将异步操作转化为一个闭包函数,然后通过回调函数将其传递到Redux的异步流中。 Thunk的主要特点有以下几个: 1. Thunk可以轻松处理异步操作,没有复杂的代码逻辑或者概念。 2. Thunk主要使用了闭包函数来捕捉当前异步操作的上下文,使得处理异步操作更加的简单、方便和自然。 3. Thunk可以轻松控制异步操作的状态、结果和错误处理,保证应用程序的稳定性和可靠性。 总之,React-Saga和React-Thunk都是帮助我们管理和处理应用程序的异步操作的中间件。它们都有自己独特的实现方式和特点。我们可以根据自己的项目需求和开发团队的技能水平来选择适合我们的中间件。 ### 回答3: React-saga 和 React-thunk 都是针对 React 应用中异步操作的中间件。它们两个都可以用来控制异步流程,使得我们可以更好的管理 React 应用程序中异步操作的数据和状态。 相较于 react-thunk, react-saga 是一个更加强大的中间件,它基于 generator 函数的概念,可以用来控制非常复杂的异步流程,使得我们可以在操作时更加精细地掌控多个异步操作的执行顺序和状态。 如果说 react-thunk 的核心概念是将异步操作封装进一个函数里,而在需要时调用这个函数即可,那么 redux-saga 的核心概念则是分离出一个独立的 Generator 函数来代表所有的异步业务逻辑。 redux-saga 可以让你从另一个角度处理异步流程,使你能够同步处理异步操作,不同的 Saga 可以用一种集中且易于理解的方式组合起来,组成它们自己的执行序列。 总而言之,React-saga和React-thunk 都是 React 应用程序开发中非常实用的工具,对于管理异步操作和数据状态非常有帮助。但是针对不同的开发需求,我们需要选择相应的中间件,来实现我们最好的业务逻辑。所以我们在使用的时候需要根据实际情况选择适合的中间件进行操作,以达到最好的效果。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值