#6 异步actions & react-thunk

asynchronous action creator 做数据取回,至少发送3个不同的actions:

  1. an action 查询store, 请求开始(request began)
  2. an action 查询store, 请求成功的完成(request finished successfully)
  3. an action 查询store, 请求失败(request failed)

下面以pro react中的示例作介绍,这个项目主要是查询飞机航班信息和飞机票信息了解具体操作流程:

1.目录结构

  • app/
    • actions/
    • api/
    • components/
    • reducers/
    • store/
    • index.js
    • constants.js

其中 粗体带/ 表示文件夹, 斜体 表示文件, app/ 文件夹在根目录下。

2.定义constants.js

这个常量文件主要是项目中发送请求的类型,通常都是一边开发,一边根据需求来实现的,这里由于是示例,暂且先一次性写出来:

// constants.js
/*
 *  REQUEST_AIRPORTS: 表示应用开始取回航班信息
 *  RECEIVE_AIRPORTS: 通过负载(preloaders)来表示异步取回信息失败或者成功(注意这个动作表示2种状态)
 *  CHOOSE_AIRPORT  : 选择起点和终点的操作,同步操作
 *  REQUEST_TICKETS : 表示应用请求票务信息
 *  RECEIVE_TICKETS : 异步取回票务信息成功或失败(注意这个动作表示2种状态,成功或失败)
 */

export default const REQUEST_AIRPORTS = 'request airports';
export default const RECEIVE_AIRPORTS = 'receive airports';
export default const CHOOSE_AIRPORT   = 'choose airport';
export default const REQUEST_TICKETS  = 'request tickets';
export default const RECEIVE_TICKETS  = 'receive tickets';

3.创建辅助API函数,用来获取服务器中的数据

这些数据APIs现在都由示例提供,通过上面的示例我们知道有2次异步的操作: 取回航班信息和取回票务信息。

本例使用 fetch进行ajax操作,返回一个promise,在 api/文件夹下:

// api/AirCheapAPI.js

import 'whatwg-fetch';

// 用fetch取回数据
// 对数据进行json()格式化
// 整个函数返回一个promise, 等待进一步操作
const AirCheapAPI = {
  // 取回航班信息  
  fetchAirports() {
    return fetch('https://aircheapapi.pro-react.com/airports')
            .then(response => response.json());
  },

  // 取回票务信息
  fetchTickets(origin, destination) {
    return fetch(`https://aircheapapi.pro-react.com/tickets?origin=${origin}&destination=${destination}`)
            .then(response => response.json());
  }
}

export default AirCheapAPI;

4.AirportActionCreators

根据常量中的动作类型和需求来定义actions, 再将其写成函数的形式,方便传入到数据中,在实际的开发中,这些不可能一蹴而就,往往都是按需开发。

// actions/AirportActionCreators.js

import {
  REQUEST_AIRPORTS,
  RECEIVE_AIRPORTS,
  CHOOSE_AIRPORT,
  REQUEST_TICKETS,
  RECEIVE_TICKETS
} from '../constants';
import AirCheapAPI from '../api/AirCheapAPI';

const AirportActionCreators = {
  // 获取航班信息
  // 异步操作, 返回一个函数
  // Thunk action creator
  fetchAirportsInfo(origin, destination) {
    return (dispatch) => {
      // 本地发送请求航班信息
      dispatch({ type: REQUEST_AIRPORTS });
      
      // promise
      // 取回数据成功(success), 则发送RECEIVE_AIRPORTS动作和preload
      // success, airports为负载
      // 取回数据失败(error), 则发送RECEIVE_AIRPORTS动作和preload
      AirCheapAPI.fetchAirports().then(
        (success) => dispatch({ type: RECEIVE_AIRPORTS, success: true, airports }),
        (error) => dispatch({ type: RECEIVE_AIRPORTS, success: false })
      )
    };
  },

  // 选择出发地和目的地
  // 同步操作, 返回一个action 对象
  // target, code 都是发送动作时的负载
  chooseAirport(target, airport) {
    return {
      type: CHOOSE_AIRPORT,
      target,
      code: airport ? airport.value : ''
    }
  },

  // 获取票务信息
  // 异步操作, 返回一个函数
  fetchTicketsInfo(origin, destination) {
    return (dispatch) => {
      dispatch({ type: REQUEST_TICKETS });
      AirCheapAPI.fetchTickets(origin, destination).then(
        (success) => dispatch({ type: RECEIVE_TICKETS, success: true, tickets}),
        (error) => dispatch({ type: RECEIVE_TICKETS, success: false })
      )
    };
  }
}

export default AirportActionCreators;

通过上面可以看出,异步操作一般调用返回一个函数,调用辅助api,而同步操作,返回一个action对象即可

4.reducers

写完actionCreators之后就该写reducers(通常需要知道数据结构),一般将大的reducers工具功能划分成小的单个reducer,最后再合并成一个rootReducer。

更具constants.js可以知道总共有5种类型的action type, 但其中 REQUEST_AIRPORTSREQUEST_TICKETS在异步操作中完成。

airports.js:

// reducers/airports.js

import { RECEVIE_AIRPORTS } from '../constants';

const airports = (state = [], action) => {
  switch (action.type) {
    case RECEVIE_AIRPORTS:
      return action.tickets;
    default:
      return state;
  }
}

export default airports;

route.js:

// reducers/route.js

import update from 'react-addons-update';
import { CHOOSE_AIRPORT } from '../constants';

const initialState = {
  origin: '',
  destination: ''
};

const route = (state = initialState, action) => {
  switch (action.type) {
    case CHOOSE_AIRPORT:
      // 更新状态树中的route状态
      // action.target 表示 'origin'或者'destination'
      return update(state, { [action.target]: { $set: action.code } });
    default:
      return state;
  }
}

export default route;

tickets.js:

// reducers/tickets.js

import { REQUEST_TICKETS, RECEVIE_TICKETS } from '../constants';

const tickets = (state = [], action) => {
  switch (action.type) {
    // 这里的REQUEST_TICKETS主要用于清空状态树中tickets的内容
    case REQUEST_TICKETS:
      return [];
    case RECEVIE_TICKETS:
      return action.tickets;
    default:
      return state;
  }
}

export default tickets;

将多个reducers合并成一个rootReducer, index.js:

// reducers/index.js

import { combineReducers } from 'redux';
import airports from './airports';
import route from './route';
import tickets from './tickets';

const rootReducer = combineReducers({
  airports,
  route,
  tickets
});

export default rootReducer;

5.store

完成reducers之后,下一步就是写store了,因为异步的原因,需要添加中间件react-thunk,另外这里自己写一个logger的中间件。

// store/index.js

import { createStore, applyMiddleware } from 'redux';
import thunk from 'react-thunk';
import rootReducer from '../reducers/index';

// 自定义logger中间件
const logger = (store) => (next) => (action) => {
  if (typeof action !== 'function') {
    console.log('dispatching:', action)
  }
  next(action);
};

const aircheapStore = createStore(
  rootReducer,
  applyMiddleware(logger, thunk)
);

export default aircheapStore;

如果配合redux-devtools chrome插件,可以写为:

import { createStore, applyMiddleware, compose } from 'redux';

// ...

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const aircheapStore = createStore(
  rootReducer,
  composeEnhancers(applyMiddleware(logger, thunk))
);

export default aircheapStore;

到目前为止,所有的基本框架已经搭建好了,剩下的就是去写components/中的组件了,当然这只是示例,实际开发中肯定要先写组件,然后依据需求添加上面的api, constants, actions, reducers, store。

总结

写这个主要是为了熟悉开发流程,以及异步操作的处理当中要注意的一些事项和规范。真实项目目前做的比较少,大致流程可以理解为:

  1. 写设计组件结构,由几部分组件,写出presentation components(视觉组件)
  2. 然后初步列举出所需要的功能,将功能的实现以动作的形式命名,写入到constants.js中
  3. 列举出可能会出项的异步操作,写入到辅助api中
  4. 根据动作类型写出actionCreator, 及对应的reducer
  5. 将routeReducer写入store
  6. 再写container components(容器组件), 然后调用store中的所需要的状态

当然状态树(state tree)这一点上对于我这样的新手来说十分的困难,如果设计数据结构这又是一个大难题了。

2016/12/12 17:17:49

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容