如何使用Redux?
「如何」系列第二篇,通过一个案例来介绍Redux的用法。
零、前言
背景
本次需求中,要求实现培训文档功能。核心功能是:每个任务下对应一定数量的培训文档,具有权限的用户可以进行上传、下载、删除文档三种操作。本文只选择了上传功能为案例,展示通过 Redux 处理数据和更新视图的思路以及代码结构,代码省略了业务逻辑相关部分。
分析
Pioneer 项目前端将任务进行了抽象化,定义了一个 Task 类,所有 Task 类的相关操作可以通过其成员方法来实现,同时将修改动作交由 reducer 来实时更新 redux 中的 Task 数据。
由于目前并没有将 Task下的文档共享到其他组件的硬性需求,我这是强行使用 Redux 来熟悉项目,增加自己的工作量,不够闲的朋友请勿尝试。
先复习一下 Redux 的工作流程:
根据 Redux 的工作流和业务逻辑,我将要上传培训文档应该会经历以下步骤
- 在
ReactComponent中选择文件(data),点击上传按钮 - 触发
ActionCreator,调用后端接口,上传data ActionCreator根据调用结果,向Reducer发送不同actionaction携带data进入ReducerReducer从store获取当前state,根据actionType对state进行修改Reducer向store返回新的statestore向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 中的方法进行封装
- 在 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);
效果展示

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