如何使用Redux?

2021/06/07

「如何」系列第二篇,通过一个案例来介绍Redux的用法。

零、前言

背景

本次需求中,要求实现培训文档功能。核心功能是:每个任务下对应一定数量的培训文档,具有权限的用户可以进行上传、下载、删除文档三种操作。本文只选择了上传功能为案例,展示通过 Redux 处理数据和更新视图的思路以及代码结构,代码省略了业务逻辑相关部分。

分析

Pioneer 项目前端将任务进行了抽象化,定义了一个 Task 类,所有 Task 类的相关操作可以通过其成员方法来实现,同时将修改动作交由 reducer 来实时更新 redux 中的 Task 数据。

由于目前并没有将 Task下的文档共享到其他组件的硬性需求,我这是强行使用 Redux 来熟悉项目,增加自己的工作量,不够闲的朋友请勿尝试。

先复习一下 Redux 的工作流程:image-20210609105516763

根据 Redux 的工作流和业务逻辑,我将要上传培训文档应该会经历以下步骤

  1. ReactComponent 中选择文件(data),点击上传按钮
  2. 触发 ActionCreator ,调用后端接口,上传 data
  3. ActionCreator 根据调用结果,向 Reducer 发送不同 action
  4. action 携带 data 进入 Reducer
  5. Reducer store 获取当前 state,根据 actionType state 进行修改
  6. Reducer store 返回新的 state
  7. store ReactComponent 返回 newState(一般为容器组件(Container))

前置知识:React, TypeScript

新增知识:Redux Flow, react-redux

拓展知识:TypeScriptr-enum, MVVM, MVC

以下是思路展示:

一、 直接调用后端 API

server-proxy

引入axios并对其进行封装

在此文件中定义直接调用接口的方法 async uploadDocs()

async function uploadDocs(id, docsData) {
    const { backendAPI } = config;
    //上传文件时,需要用FormData 来构造post的body部分
    const taskData = new FormData();
    taskData.append(`file`, docsData.file);
    taskData.append(`description`,docsData.description);

    try {
        //此处用到了封装过的 axios,也可以采用其他 HTTP 库
        const response = await Axios.post(`${backendAPI}/tasks/${id}/docs`,taskData, {
            proxy: config.proxy,
            headers: {
                'Content-Type': 'application/json',
            },
        });
        return response
    } catch (errorData) {
        throw generateError(errorData);
    }
}

二、在实例所属类中引入封装好的方法

session

引入server-proxy 中的方法进行封装

  1. 在 Task 原型中定义 uploadDocs 方法
Task.prototype.uploadDocs.implementation = async function (data, taskid) {
    const result = await serverProxy.tasks.uploadDocs(data, taskid);
    return result
}

2. 用apiWrapper来调用原型方法

async uploadDocs(id, docsData){
    const result = await PluginRegistry
    .apiWrapper.call(this, Task.prototype.uploadDocs, id, docsData);
    return result;
}

注意:这一整个步骤属于视具体业务而出现的操作,并非 Redux 必要步骤。

三、ActionCreator - 创建 action

tasks-actions

定义了和 task 相关的 ActionCreator

在此文件中定义一个返回 TunkAction 的函数uploadDocsAsync()(此函数即为 ActionCreator),根据异步操作的执行结果触发相应动作。

//声明了要用到的 actionTypes
export enum TasksActionTypes {
    //培训文档相关动作
    UPLOAD_DOCS = 'UPLOAD_DOCS',
    UPLOAD_DOCS_SUCCESS = 'UPLOAD_DOCS_SUCCESS',
    UPLOAD_DOCS_FAILED = 'UPLOAD_DOCS_FAILED',
	//...
}

//上传任务的培训文档
export function uploadDocsAsync(data:any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
    return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
        //进入函数,dispatch 触发 UPLOAD_DOCS 动作
        dispatch({ type:TasksActionTypes.UPLOAD_DOCS })
        try {
        //尝试调用 2 中实例方法,上传培训文档
        const res = await taskInstance.uploadDocs(data)
        //成功,dispatch 触发 SUCCESS 动作,传递新的培训文档数据
        dispatch({ type:TasksActionTypes.UPLOAD_DOCS_SUCCESS,payload:{docs:data}})
        } catch (error) {
            //失败,dispatch 触发 FAILED 动作,传递 error 
            dispatch({ type:TasksActionTypes.UPLOAD_DOCS_FAILED,payload:{error:error} })
        }
    };
}

四、Reducer - 执行 action 修改 state

task-reducer

响应 task 相关 action 并改变 state

reducer 文件结构:

import { TasksActionTypes } from 'actions/tasks-actions'; //引入 action 文件
import { TasksState, Task } from './interfaces';//描述 TaskSate 的 ts 文件

const defaultState: TasksState = {
    //...
};

export default (state: TasksState = defaultState, action: AnyAction): TasksState => {
    switch (action.type) {
        case TasksActionTypes.UPLOAD_DOCS:{
            //...
        }
        case TasksActionTypes.UPLOAD_DOCS_SUCCESS:{
			//...
        }
        case TasksActionTypes.UPLOAD_DOCS_FAILED:{
			//...
        }
        default:
            return state;
    }
};

tasks-reducer 中新增对应三个action 的 case,更新 TasksState 中的 docs 数据和上传状态

  • UPLOAD_DOCS --- 修改 uploadDocs 的 Status

  • UPLOAD_DOCS_SUCCESS --- 修改 uploadDocs 的 Status & 修改 docs

  • UPLOAD_DOCS_FAILED --- 修改 uploadDocs 的 Status

    case TasksActionTypes.UPLOAD_DOCS:{ //… } case TasksActionTypes.UPLOAD_DOCS_SUCCESS:{ const { doc } = action.payload; const { uploadDocs } = state.activities;

     return {
         ...state,
         docs:[...state.docs,doc],//添加新 doc
         activities: {
             ...state.activities,
             uploadDocs: {
                 ...uploadDocs,
                 status:'UPLOADED'// 修改 status
             },
         },
     };

    } case TasksActionTypes.UPLOAD_DOCS_FAILED:{ //… }

interfaces.ts

TypeScript 文件,描述 reducer 中的数据类型

在此文件中定义 uploadDocs 与相关的状态成员

export interface TasksState {
	//...
    activities: {
        //...
        uploadDocs:{
            status:string;
            error:string;
        };
    };
    //...
    docs:TrainDoc[]
}

五、容器组件(Container)和展示组件

经过以上几个步骤,终于可以将上传方法和状态值引入组件了,通过容器组件传递到展示组件,完成渲染。

<TrainingDocuments/>

展示组件:只负责页面的渲染和数据绑定,只被其容器组件引用

<TrainingDocumentsContainer/>

容器组件:负责页面的逻辑和状态获取

创建一个 train-doc 组件的 container <TrainingDocumentsContainer />,在 Container 中将 dispatch 的操作映射到 Props。

import { uploadDocsAsync } from 'actions/tasks-actions';//引入第三步中的 action

//定义接口
interface DispatchToProps {
    onUploadDocs(data:object):void
    onGetDocs(taskId:number):void
    onDeleteDocs(taskId:number,docId:number):void
}

//将 dispatch 映射到 Props
function mapDispatchToProps(dispatch: any): DispatchToProps {
    return {
        onUploadDocs: (data: object):void=> dispatch(uploadDocsAsync(data))
    };
}

//这里也可以简写,react-redux 会自动处理
const mapDispatchToProps = {
        onUploadDocs:uploadDocsAsync,
        onGetDocs: getDocsAsync,
        onDeleteDocs: deleteDocsAsync
}

function TrainingDocumentsContainer(props: StateToProps & DispatchToProps & OwnProps): JSX.Element {
    const {
        tasks,//tasks 是在前端 redux 中保存的 TaskState 实例,存储着 task 的状态值,参见第四步中的 interface.ts
        onUploadDocs,
    } = props;
    
    // 上传状态
    const upLoading = (tasks.activities.uploadDocs.status == 'UPLOADING')
    
    // 当前 task 文档列表
	const taskDocs = (tasks.docs)

    // 第一次渲染页面时,先获取一次 task 的状态值
    useEffect(()=>{
        onGetDocs(taskId)
    },[])

    return (
        <TrainingDocuments 
            taskDocs={taskDocs}
            upLoading = {upLoading}
            onUploadDocs = {onUploadDocs}
        />
    );
}

// 将容器组件与状态连接起来
export default connect(
    mapStateToProps,
    mapDispatchToProps,
)(TrainingDocumentsContainer);

效果展示

image-20210322115645319

总结

Redux 不关心数据在视图层面的具体作用,它将所有数据的当前值保存为一个 state,可以理解为数据“快照”。数据的一切变化,即是从旧的state到新的state的变化过程,同时运用中间件 redux-logger,对这个变化过程做一次记录。概括来说,Redux的思想是把有共享需要的「数据和数据逻辑」与「展示逻辑」进行了分离,将修改数据的操作视为为不同的action,在每一个 action 前后,数据快照 state产生变化。