Redux(3):中间件和异步Action【内含练手小项目】

Redux(3):中间件和异步Action【内含练手小项目】

技术杂谈小彩虹2021-08-19 19:00:33360A+A-

这是我参与更文挑战的第5天,活动详情查看: 更文挑战

说在前面

异步 Action 是一个设计模式,即异步 Action 并不是一个特殊的 Action,而是同步 Action 的组合使用。

请带着这句话理解异步 Action 和中间件,这非常重要。

异步 Action

首先,什么是异步 Action?

  • Action 发出以后,Reducer 立即算出 State,是同步;
  • Action 发出以后,过一段时间再执行 Reducer,就是异步。

上一篇 -- Redux(1):入门三件套(Action、Reducer、Store)介绍的都是同步 Action,用户发射(dispatch)一个 Action ,Reducer 会同步计算新的 State,从而引发视图(View)更新。如下图:

redux.jpg

相对比同步 Action,异步 Action 至少需要 dispatch 三种 action:

  • 请求开始的 action
  • 请求成功的 action
  • 请求失败的 action
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }

了解了异步 Action,那么怎么才能实现?又如何能在发送 action 后(store.dispatch())将其截获,并且在一段时间后又自动发出 action 呢?

我们先来看一下异步 Action 的工作流程 。

异步数据流

redux.gif

对比同步数据流,显而易见的差别是 多了一个 Middlewares(中间件)

在异步数据流中,即 Redux 的异步请求,我们发现 store.dispatch() 被重新封装并添加了新功能,Action 不再直接发射给 reducer,而是需要经过 Middlewares 预处理。

Middleware(中间件)

通过使用指定的 Middleware 可以截获某种类型的 Action(比如 redux-thunk 可以截获 ajax 类型),截获后会通过 API 进行预处理,最后再将结果 dispatch 给 Reducer。

简单来说,Middleware 就是一个改造了 store.dispatch 的函数。他的任务就是:

  • 截获 action
  • 处理 action
  • 发出 action

要注意,Actions 发射的是标准 action,Middlewares 返回的也是标准 action。 (回想一下说在前面的话)

开发实践

我们来开发一个异步的应用:使用 Reddit API 来获取 reactjs 相关的文章列表。它就像这样

rekkit.gif

我们会用到的依赖:

准备工作

  • 创建 react 项目
npx create-react-app my_redux-thunk
  • 安装 redux & react-redux
yarn add --dev redux react-redux
  • 安装 redux-thunk & redux-logger
yarn add --dev redux-thunk redux-logger
  • 安装 axios
yarn add axios

入口

index.js

import React from "react";
import ReactDOM from "react-dom";
import Root from "./containers/Root";

import "./index.css";
import reportWebVitals from "./reportWebVitals";

ReactDOM.render(
  <React.StrictMode> <Root /> </React.StrictMode>,
  document.getElementById("root")
);

reportWebVitals();

Action Creators 和 Constants

store/actions.js

import axios from "axios";

export const FETCH_POSTS_REQUEST = "FETCH_POSTS_REQUEST";
export const FETCH_POSTS_SUCCESS = "FETCH_POSTS_SUCCESS";
export const FETCH_POSTS_FAILURE = "FETCH_POSTS_FAILURE";

// 请求开始的 action
function fetchPostsRequest() {
  return {
    type: FETCH_POSTS_REQUEST,
  };
}

// 请求成功的 action
function fetchPostsSuccess(json) {
  return {
    type: FETCH_POSTS_SUCCESS,
    posts: json.data.children.map((child) => child.data),
    receivedAt: Date.now(),
  };
}

// 请求失败的 action
function fetchPostsFailure(error) {
  return {
    type: FETCH_POSTS_FAILURE,
    error,
  };
}

/* ** 异步 Action ** 返回一个函数 */
function fetchPosts() {
  return (dispatch) => {
    // 请求开始 action
    dispatch(fetchPostsRequest());
    return new Promise((resolve, reject) => {
      const doRequest = axios.get("https://www.reddit.com/r/reactjs.json");
      doRequest
        // 请求成功 action
        .then((res) => {
          dispatch(fetchPostsSuccess(res.data));
          resolve(res);
        })
        // 请求失败 action
        .catch((err) => {
          dispatch(fetchPostsFailure(err.message));
          reject(err);
        });
    });
  };
}

function shouldFetchPosts(state) {
  if (state.isFetching) {
    return false;
  } else {
    return true;
  }
}

export function fetchPostsIfNeeded() {
  return (dispatch, getState) => {
    // 判断是否需要 fetch
    if (shouldFetchPosts(getState())) {
      return dispatch(fetchPosts());
    }
  };
}

Reducers

store/reducers.js

import {
  FETCH_POSTS_REQUEST,
  FETCH_POSTS_SUCCESS,
  FETCH_POSTS_FAILURE,
} from "./actions";

export default function postsReducer( state = { isFetching: false, items: [], }, action ) {
  switch (action.type) {
    case FETCH_POSTS_REQUEST:
      return {
        ...state,
        isFetching: true,
        error: null,
      };
    case FETCH_POSTS_SUCCESS:
      return {
        ...state,
        isFetching: false,
        items: action.posts,
        lastUpdated: action.receivedAt,
        error: null,
      };
    case FETCH_POSTS_FAILURE:
      return {
        ...state,
        isFetching: false,
        error: action.error,
      };
    default:
      return state;
  }
}

Store

store/store.js

import { createStore, applyMiddleware } from "redux";
import thunkMiddleware from "redux-thunk";
import logger from "redux-logger";
import postsReducer from "./reducers";

export default function configureStore(preloadedState) {
  return createStore(
    postsReducer,
    preloadedState,
    applyMiddleware(thunkMiddleware, logger)
  );
}

容器组件

containers/AsyncApp.jsx

import React, { Component } from "react";
import { connect } from "react-redux";
import { fetchPostsIfNeeded } from "../store/actions";
import Picker from "../components/Picker";
import Posts from "../components/Posts";

class AsyncApp extends Component {
  constructor(props) {
    super(props);
    this.handleFetch = this.handleFetch.bind(this);
  }

  componentDidMount() {
    this.handleFetch();
  }

  handleFetch() {
    this.props.fetchPostsIfNeeded();
  }

  render() {
    const { items, isFetching, lastUpdated, error } = this.props;
    return (
      <div> <Picker onFetch={this.handleFetch} /> <p> {error && <span style={{ color: "red" }}>Fail To Load: {error}</span>} </p> <p> {lastUpdated && ( <span> Last updated at {new Date(lastUpdated).toLocaleTimeString()}.{" "} </span> )} </p> {} {!isFetching && items.length === 0 && <h2>Empty.</h2>} {items.length > 0 && ( <div style={{ opacity: isFetching ? 0.5 : 1 }}> <Posts items={items} /> </div> )} </div>
    );
  }
}

function mapStateToProps(state) {
  const { isFetching, lastUpdated, items, error } = state || {
    isFetching: true,
    items: [],
  };

  return {
    items,
    isFetching,
    lastUpdated,
    error,
  };
}

const mapDispatchToProps = { fetchPostsIfNeeded };

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

contianers/Root.jsx

import React, { Component } from "react";
import { Provider } from "react-redux";
import configureStore from "../store/store";
import AsyncApp from "./AsyncApp";

const store = configureStore();

export default class Root extends Component {
  render() {
    return (
      <Provider store={store}> <AsyncApp /> </Provider>
    );
  }
}

展示组件

components/Picker.js

import React, { Component } from "react";

export default class Picker extends Component {
  render() {
    const { onFetch } = this.props;

    return (
      <div> <h1>获取 Reddit 上 Reactjs 相关文章列表</h1> <button onClick={() => onFetch("reactjs")}>点击我 fetch 列表</button> </div>
    );
  }
}

components/Posts.js

import React, { Component } from "react";

export default class Posts extends Component {
  render() {
    return (
      <ul> {this.props.items.map((item, i) => ( <li key={i}>{item.title}</li> ))} </ul>
    );
  }
}

Redux 系列

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们