Redux 整合笔记二 异步与中间件

458 阅读6分钟

一 使用API

目前境况是:用户与APP交互,而action被dispatch以反映某些事件。数据直接存在于浏览器中,这意味着如果用户刷新,所有操作进展都会丢失。

目前为止dispatch的action都是同步的。当action被dispatch时,store会立即接收到它们。这种action简单直接。

但现实中,很难找到完全不涉及异步代码的JS APP。

1.1 异步action

在此之前所有的action都是同步的。当dispatch同步action时,没有任何空间处理额外的功能。

异步action提供了一种处理异步操作的方法,通常返回同步action以提供可读性

export function fetchTasks(){
	return dispatch => {
    	api.fetchTasks().then(resp => {
        	dispatch(fetchTasksSucceeded(resp.data))
        })
    }
}

1.2 使用redux-thunk调用异步action

redux-thunk是一个redux中间件。(关于中间件详细后面会讲到)redux-thunk将允许像dispatch标准action对象一样dispatch函数,可安全地添加异步代码。

src/app.js

//...
componentDidMount(){
	this.props.dispatch(fatchTasks())
    // 请求初始数据
}
//...

src/actions/index.js

import axios from 'axios';

export function fetchTasks(){
	return dispatch => {
    	axios.get('http://localhost:3001/tasks')
        	.then(resp => {
            	dispatch(fetchTasksSucceeded(resp.data));
            })
    }
}

export function fetchTasksSucceeded(tasks){
	return {
    	type: FETCH_TASKS_SUCCEEDED,
        paylpad: {
        	tasks
        }
    }
}

优化 API客户端

src/api/index.js

import axios = from 'axios';

const API_BASE_URL='http://localhost:3001';

export const client = axios.create({
	baseURL: API_BASE_URL,
    headers: {
    	'Content-type': 'application/json',
    }
})

src/action/index.js

import * as api from '.../api';

export function fetchTasks(){
	return dispatch => {
    	api.client().get('/tasks').then(resp=>{
        	dispatch(fetchTasksSucceeded(resp.data))
        })
    }
}

view action 和 server action

view action: 由用户发起 如:FETCH_TASKS / CREATE_TASK / EDIT_TASK serve action: 由服务器发起 如:FETCH_TASKS_SUCCEEDED

1.3 将任务保存到服务器

src/action/index.js

import * as api from '.../api';

function createTasksSucceeded(task){
	return {
    	type: CREATE_TASK_SUCCESSDED,
        payload: {
        	task
        }
    }
}

export function createTasks({title, description, status='Unstarted'}){
	return dispatch => {
    	api.client().post('/tasks',{title, description, status}).then(resp=>{
        	dispatch(createTasksSucceeded(resp.data))
        })
    }
}

function editTaskSucceeded(task){
	return {
    	type: EDIT_TASK_SUCCEEDED,
        payload:{
        	task
        }
    }
}

export function editTask(id, params={}) {
	return (dispatch, getState) => {
    	const task = getTaskById(getState().tasks, id);
        const updatedTask = Object.assign({}, task, params)
        
        api.client().post(id, updatedTask).then(resp=>{
        	dispatch(editTaskSucceeded(resp.data))
        })
    }
}

function getTaskById(tasks, id){
	return tasks.find(task => task.id === id)
}

1.4 加载状态

当进入加载状态时,使用redux跟踪请求状态,并在请求进行过程中更新UI以展示合适的反馈

请求生命周期

对网络请求,需要关注两个时刻:何时开始以及何时完成。将成些事件建模为action,最终会得到3种action

  • FETCH_TASKS_STARTED -- 请求发起时dispatch
  • FETCH_TASKS_SUCCEEDED -- 请求成功完成时dispatch
  • FETCH_TASKS_FAILED -- 请求失败时dispatch, payload通常来自服务器的错误信息

src/reducers/index.js

const initialState = {
	tasks: [],
    isLoading: false
}

export default function tasksReducer(sate = initialState, action) {
	switch(action.type){
    	case FETCH_TASKS_STARTED: {
          return {
              ...state,
              isLoading: true,
          }
        }
    	case FETCH_TASKS_SUCCEEDED: {
          return {
              ...state,
              isLoading: false,
              tasks: action.paylpad.tasks
          }
        }
        default: {
        	return state;
        }
    }
}

随着添加更多的功能,需要在redux中跟踪更多的数据,可为store添加新的顶层属性

index.js

import {tasksReducer, projectsReducer} from './reducers';

const rootReducer = (state = {}, action) => {
	return {
    	tasks: tasksReducer(state.tasks, action),
        projects: projectsReducer(state.projects, action),
    }
}

const store = createStore(
	rootReducer,
    composeWithDevTools(applyMiddleware(thunk))
)

这样就允许每个reducer不用关心store的整体形状,而只关心它所操作的数据片段

二 中间件

中间件是什么?中间件是指两个软件组件之间运行的代码,一般作为框架的一部分。

redux中间件是位于action和store之间的代码。与服务器中间件有助于跨多个请求运行代码的方式类似,redux中间件允许跨多个action派发运行代码

2.1 中间件的基础知识

创建中间件包含两个简单的步骤:

  1. 用正确的函数签名定义中间件
  2. 使用redux的applyMiddleware函数将中间件与store一起注册

首先:中间件的函数签名

const middlewareExample = store => next => action => {...}
  • store -- 需要基于现有状态做出决定时,可使store对象 / tore.getState方法满足所需
  • next -- 将action传递给链中下一个中间件时调用的函数
  • action -- 被派发的action。通常,中间件将对每个action执行一些操作、或通过检查action.type来监视特定的action

组合中间件

中间件一个重要方面在于它的链接能力,所有redux中间件都必须以相同的方式创建,所以将自己的中间件与第三方中间件结合起来使用是完全可行的。

中间件的真正好处是能够将需要跨多个action执行的一些任务集中化。

案例分析:如何不使用中间件

登录重定向

export function login(params){
	return dispatch => {
    	api.client().post('/login', params).then(resp=>{
        	dispatch(loginSucceeded(resp.data))
            
            // 执行重定向
            dispatch(navigate('/dashboard'))
        })
    }
}

另一方面,还可在中间件中添加特定的路由逻辑

export function login(params){
	return dispatch => {
    	api.client().post('/login', params).then(resp=>{
        	dispatch(loginSucceeded(resp.data))
        })
    }
}

// within a routing middleware file
const routing = store => next => action => {
	if(action.type === LOGIN_SUCCEEDED){
    	storeeee.dispatch(navigate('/dashboard'))
    }
}

这里关键是显式与隐式的区别,中间件能帮助减小重复和集中逻辑,但这是有代价的,取决于做出的最佳判断。中间件就是抽象,它的主要目的是帮助开发。

API中间件

回顾下服务器API调用的三大关键时刻:

  • FETCH_TASKS_STARTED
  • FETCH_TASKS_SUCCEEDED
  • FETCH_TASKS_FAILED

对于中间件来说,所有进行API调用的action都需要三项内容:

  • CALL_API字段,在中间件中定义和导出
  • types属性,对应的数组由用于请求开始、成功、失败的3种action组成
  • endpoint属性,用于明确需要请求的资源的相对URL

src/actions/index.js

import {CALL_API} from '../middleware/api';

export function fetchTasks(){
	return {
    	[CALL_API]: {
        	types: [
            	FETCH_TASKS_STARTED, 
                FETCH_TASKS_SUCCEEDED, 
                FETCH_TASKS_FAILED
                ],
            endpoint: '/tasks'
        }
    }
}

src/middle/api.js

export const CALL_API = 'CALL_API';

const apiMiddleware = stpre => next => action => {
	const callApi = action[CALL_API];
    if(typeof callApi === 'undefined'){
    	return next(action);
        // 如果不是这个中间件关心的action,那么不进行任何处理并继续
    }
}

src/index.js

import apiMiddleware from './middleware/api';
cosnt store = createStore(
	rootReducer,
    composeWithDevTools(applyMiddleware(thunk, apiMiddleware))
)

应用中间件的顺序很重要。

现在是实现中间件主要代码时候了

src/middle/api.js

export const CALL_API = 'CALL_API';

const apiMiddleware = store => next => action => {
	const callApi = action[CALL_API];
    if(typeof callApi === 'undefined'){
    	return next(action);
    }
    
    const [requestStartedType, successType, failureType] = callApi.types;
    // 使用数组解构方式创建每个action类型变量
    next({type: requestStartedType})
    // next最终会向store派发action,与store.dispatch相同。
}

接下来添加AJAX请求的函数 src/middle/api.js

import axios = from 'axios';

const API_BASE_URL='http://localhost:3001';

function makeCall(endpoint, method = 'GET', body){
	const url = `${API_BASE_URL}${endpoint}`;
    
    const params = {
    	method,
        url,
        data: body,
        headers: {
        	'Content-Type': 'application/json',
        }
    }
    
    return axios(params).then(resp => resp).catch(err => err)
}

const apiMiddleware = store => next => action => {
	const callApi = action[CALL_API];
    if(typeof callApi === 'undefined'){
    	return next(action);
    }
    
    const [requestStartedType, successType, failureType] = callApi.types;
    next({type: requestStartedType})
    
    return makeCall({
    	method: callApi.method,
        body: callApi.body,
        endpoint: callApi.endpoint,
    }).then(
        respinse => 
            next({
                type: successType,
                paylpad: respinse.data,
            }),
        error => 
            next({
                type: failureType,
                error: error.message,
            })
    )
}

这里主要好处是,如果需要增加额外的用于发起服务器请求的异步action,可极大地减少随之而来的重复工作。可使用API中间件来完成繁重的工作,而不是创建3种新的action类型并手动dispatch它们。

虽然添加了几行代码,但收益是巨大的。 src/actions/index.js

export function createTask({title, description, status = 'Unstarted'}){
	return {
    	[CALL_API]: {
        	types: [
            	FETCH_TASKS_STARTED, 
                FETCH_TASKS_SUCCEEDED, 
                FETCH_TASKS_FAILED
                ],
            endpoint: '/tasks',
            method: 'POST',
            body: {
            	title,
                description,
                status,
            }
        }
    }
}

好极了!总的来说,你更倾向哪种风格?让每个action创建器使用redux-thunk显式地dispatch action? 或者喜欢更强大的API中间件?