如何使用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产生变化。