关于本章节的所有示例代码和练习内容,均在chapter_four中,每一部分的内容请结合代码仓库中对应的section来学习和理解。
截止到目前的章节,我们所有的练习采用的都是派发同步action到store里面来直接处理。action派发时,store会立即接收它们,这种同步代码容易使用和理解,但是实际的前端开发中,经常会伴随着异步代码的产生,异步代码意味着不确切的执行顺序。并且,之前的交互操作所产生的数据是直接保存在浏览器当中的,这意味着如果user刷新页面,所有的当前操作和数据都会丢失。
所以,这个章节就需要通过与服务器的通信来保存user当前的操作和数据。
复习一下redux的理念:
- action是描述事件的对象,比如:
{ type: 'CREATE_TASK', payload: { ..... } }; - 要应用任何的状态更新,都必须派发action。
1、异步action
在前面的章节中,派发的所有action都是同步的。但如果要在应用程序启动之初就获取到相关的数据以及页面刷新前后保持user的操作和数据一致,就需要服务器的支持(异步操作),比如:在程序加载之初通过ajax请求来获取数据,在用户触发交互后,将变化的数据发送到服务器保存起来,这样页面刷新前后呈现的内容将保持不变。
【下图】概括了到目前为止程序的调度情况,从视图派发一个action,而该action会立即被store接收,所有的这些代码都是同步的,这意味着每个操作只有在前一个操作执行完后才会运行。
相比之下,异步action是添加异步代码(比如ajax请求)的地方。当派发同步action时,没有任何空间处理额外的功能(因为要dispatch一个object)。异步action提供了一种处理异步操作的方法,并在结果可用时(拿到response)派发同步action。
异步action通常包含以下内容,然后直接在应用程序中派发:
- 一个或多个副作用,如:ajax请求;
- 一个或多个同步调用,如:在ajax请求处理完毕后派发action。
(以chapter_four为例)假设在应用程序第一次加载时,想要从服务器获取任务列表并渲染到页面上。我们需要发起请求,等待服务器做出响应,然后派发包含任意结果的action。
【下图】使用了fetchTasks异步action,注意从派发初始的异步action到最终store接收到要处理的action之间的延迟,稍等我们就开始实现它(请继续往下看):
和异步action相关的大部分困惑都来源于术语,在这里将尽力具体说明用到的术语(来源于作者的原话),以下是基本的概念和示例:
- action,描述事件的对象。在书本内容的学习中,短语“同步action”总是指这些action对象,如果dispatch一个同步action,则store会立即接收它。action拥有一个必须的type属性,并且可以选择性地具有payload属性来存储处理action所需的数据:
{
type: 'GET_INIT_TASKS',
payload: {
tasks: [ ... ]
}
}
- action创建器,返回action的函数;
- 同步action创建器,返回action的所有action创建器都被视为同步action创建器:
function fetchTasksSucceeded(tasks){
return {
type: 'GET_INIT_TASKS',
payload: {
tasks: [ ... ]
}
}
}
- 异步action创建器,包含异步代码(最常见的例子就是网络请求)的action创建器。正如在本章下面将要看到的,它们通常会进行一个或多个API调用,并在请求生命周期中的特定点(请求前、请求获取数据后)派发一个或多个action。通常,它们可能会返回同步action创建器以提高可读性,而不是直接返回action。
fundtion fetchTasks() {
return dispatch => {
api.fetchTasks().then(resp => {
dispatch(fetchTasksSucceeded(resp))
})
}
}
// 或许你已经发现,异步action创建器返回的不是一个action,而是一个函数,这里就涉及到了redux-thunk这个npm包,它除了能派发标准的action之外,还允许派发函数
2、深入异步action前的前置操作
在深入探究异步action创建器之前,我们需要安装一个简单的服务器json-server,两个依赖包axios和redux-thunk,以及调整redux devTools。
1)安装和配置json-server服务器
json-server包号称能在30秒内让你得到一个伪REST的API,作者还特地在这段话后面加了一个【真】的强调,让我不禁想起了 => 真男人?
安装 json-server
npm install -g json-server
按照json-server的guide line在根目录中创建db.json,,然后将之前在练习中添加的mockTasks数据复制一份到该文件中,如下所示:
{
"tasks": [
{
id: 1,
title: "Learn Redux",
description: "The store, actions, and reducers, oh my!",
status: "Unstarted",
},
{
id: 2,
title: "Peace on Earth",
description: "No big deal",
status: "Unstarted",
},
{
id: 3,
title: "Create Facebook for dogs",
description: "The hottest new social network",
status: "Completed"
}
]
}
完成上面步骤之后,就可以启动服务器:
json-server --watch db.json --port 3001
2)安装axios
npm install axios
关于axios的使用指南,请点击axios官网详细了解。
3)安装redux-thunk
npm install redux-thunk
安装完之后,通过redux的applyMiddleware函数给应用程序添加中间件,如下代码所示:
...
import { tasks } from './reducers'
import { applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(tasks, applyMiddleware(thunk));
...
4)更新redux devTools
在redux中加了中间件之后,旧的devToolsEnhancer函数将不再发挥作用,我们需要导入另外一个可以容纳中间件的方法 => composeWithDevtools,如下代码所示:
...
import { composeWithDevTools } from 'redux-devtools-extension';
const store = createStore(tasks, composeWithDevTools(applyMiddleware(thunk)));
...
3、使用redux-thunk调用异步action
以chapter_four为例,当你刚开始进入应用程序时,想使用GET请求来向服务器获取初始的任务列表,然后使用response data来派发action,我们要怎么做呢?
从流程来看,将执行以下操作:
- 从视图中派发异步action来获取任务;
- 执行ajax请求 GET/tasks;
- 当请求完成时,派发带有response data的异步action。
通过处理异步action的方式之一:redux-thunk,可以让我们知道异步action的实现机制。redux-thunk是一个redux中间件(关于中间件的详细内容,请看第5章:中间件),它将允许像派发action对象一样派发函数,在这个函数里,我们可以安全地进行网络请求(ajax请求),并派发带有response data的同步action。
1)从服务器获取任务
我们上面安装了HTTP api、axios(ajax库)以及redux-thunk中间件,当需要执行异步操作(ajax请求)时,将会派发函数而不是action对象。
之前,我们是使用tasks reducer中定义的静态任务列表来渲染页面,现在需要删除模拟任务列表,然后调整reducer的初始状态从mockTasks变成空数组,如下代码所示:
src/reducers/index.js
...
// 删除模拟任务列表
const mockTasks = [
{
id: uniqueId(),
title: "Learn Redux",
description: "The store, actions, and reducers, oh my!",
status: "Unstarted",
},
{
id: uniqueId(),
title: "Peace on Earth",
description: "No big deal",
status: "Unstarted",
},
];
// 初始状态从mockTasks改成[]
export default function tasks(state = { tasks: [] }, action) {
...
}
从顶层来看,为了通过ajax获取任务列表,需要添加如下逻辑:
- 当应用程序加载时,派发异步action fetchTasks以获取任务列表;
- 向 http://localhost:3001/tasks 发送ajax请求;
- 请求完成后,派发带有response data的同步action GET_INIT_TASKS。
下图展示了将要创建的异步action创建器fetchTasks的详情。和同步action一样,把fetchTasks作为异步action派发的目的是将任务加载到store中,以便在页面上呈现。这里唯一的区别就是,任务列表是从服务器获取的,而不是依赖于直接在代码中定义的mock data。
按照图示的流程,让我们先创建异步action创建器fetchTasks和同步action创建器fetchTasksSucceeded:
src/actions/index.js
import axios from "axios";
...
export const GET_INIT_TASKS = "GET_INIT_TASKS";
export function fetchTasksSucceededAct(tasks) {
// 将从服务器获取的response data派发到store中
return {
type: GET_INIT_TASKS,
payload: {
tasks,
},
};
}
export function fetchTasksAct() {
return (dispatch) => {
// 执行ajax调用,从服务器获取初次的tasks data
axios.get("http://localhost:3001/tasks").then((res) => {
dispatch(fetchTasksSucceededAct(res.data));
});
};
}
从上面的代码中可以看到,与目前使用过的任何action相比,最大的转变是fetchTasks返回函数而非action对象。这多亏了redux-thunk,如果尝试不使用redux-thunk中间件来派发函数,redux将抛出错误,因为redux期望向dispatch传递对象。在派发的函数中,我们可以自由执行以下操作:
- 发送ajax请求以获取数据;
- 访问store状态;
- 执行其他的异步逻辑;
- 派发带有response result的同步action。
创建了两个action创建器之后,我们就可以将其导入使用了。
注意,我们什么时候需要发起ajax请求以获取初次渲染的数据?在组件首次被挂载到DOM时,可以发送ajax请求获取数据然后填充DOM。在react函数组件中,我们可以使用useEffect空数组依赖的模式来发起ajax请求,这代表只在组件初始化的时候来获取首次渲染需要的tasks数据:
src/App.js
import React, { useEffect } from "react";
...
function App(props) {
const { tasks, dispatch } = props;
useEffect(() => {
dispatch(fetchTasksAct());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
...
}
2)api客户端
在开始深入异步action之前,还需要做一步工作,你可能也发现了,在上面的代码中,我们直接在fetchTasksAct函数里面hard code了axios请求的 http://localhost:3001/tasks 服务器接口,这暂时是没有问题的,但是随着应用程序的不断增大,将会遇到一些问题,比如:端口号从3001变成3002、如果要使用其他的ajax库,该怎么办?现在只是需要在一个地方更新代码,想想如果有10个地方呢?那就要在10个地方更改代码。
要解决这些问题,我们可以将这些请求抽出来到一个专属的api文件,并暴露友好的接口,这将有利于以后的可维护性和可读性,也提升了封装性,这对于团队合作来说是很友好的,其他的开发伙伴将不必担心端口、请求头信息以及使用的ajax库等等这些细节问题。
现在,让我们来实现下,封装一个基础的api调用,并用基础的头信息和跟URL配置axios:
src/api/index.js
import axios from "axios";
// 将根url定义为一个常量
const BASE_URL = "http://localhost:3001";
const client = axios.create({
baseURL: BASE_URL,
headers: {
// 对于PUT请求,json-server需要Content-type头
"Content-type": "application/json",
},
});
export function fetchTasks() {
return client.get("/tasks");
}
温馨提示:在这里对BASE_URL进行了hard code,但在实际的开发中,这些url都是通过一些config文件来配置的。
通过fetchTasks函数,我们对请求方法和接口的url进行了封装,如果两者之一有任何变化,只需要在同一个地方更新代码即可。
axios.get方法返回一个promise,我们可以通过then、catch方法来获取数据和捕获错误,或者通过async、await、try...catch来处理数据和错误也是可以的。现在让我们使用fetchTasks来替换现有的api调用:
src/action/index.js
import * as api from "../api";
...
export function fetchTasksAct() {
return (dispatch) => {
api.fetchTasks().then((res) => {
dispatch(fetchTasksSucceededAct(res.data));
});
};
}
3)视图action和服务器action
上面的内容让我们了解了同步action和异步action的相关内容,但还有一个概念,能帮助我们更清楚地了解应用程序中的更新是如何发生的。通常能修改应用程序状态的实体有两个:用户和服务器。因此,action也可以分为两组:视图action和服务器action。
- 视图action是由用户发起的,比如:CREATE_TASK和CHANGE_STATE就是用户单击按钮之后触发的action;
- 服务器action是由服务器发起的。比如:GET_INIT_TASKS就是ajax请求成功后,使用response data派发的服务器(也是同步)action。
现在让我们完成最后一步,更新tasks reducer来完善初始tasks的获取逻辑:
src/reducers/index.js
import { CREATE_TASK, CHANGE_STATE, GET_INIT_TASKS } from "../actions";
export default function tasks(state = { tasks: [] }, action) {
switch (action.type) {
case GET_INIT_TASKS:
return { tasks: action.payload.tasks };
...
}
总结
- 使用异步action和redux-thunk。redux-thunk包允许派发函数而不是对象,在这些函数的内部,可以进行网络请求,并在任何请求完成时派发其他的aciton;
- 同步action的作用。派发带有type和payload的action对象被视为同步action,因为在派发后store会立即接收并处理这种action;
- 用户和服务器是可以更改应用程序状态的两个角色。因此,可以将action分组为视图action和服务器action。
4、将任务保存到服务器
现在我们的应用程序加载时会从服务器来获取任务了。另外,我们还需要将创建的任务和编辑状态后的任务保存到服务器,实现的过程和任务获取的过程类似。
先从保存新任务开始,下图是chapter_four运行之后渲染出来的UI界面:
当用户点击"New task",填写表单提交后,将触发action创建器createTaskAct,然后store接收 CREATE_TASK action,reducer处理更新状态,从而将更改广播回UI。
现在我们需要让createTaskAct返回函数而不是对象,在返回的函数中,可以进行api调用,并在response data回来后派发action,下面是步骤概览:
- 将createTaskAct从同步action创建器改成异步action创建器;
- 添加一个新的api调用方法,该方法将向服务器发送POST请求;
- 创建新的服务器action CREATE_TASK_SUCCEEDED,它的payload将会是一个单独的task object;
- 在action创建器createTask中发出请求,并在请求完成后派发CREATE_TASK_SUCCEEDED(此时,让我们假设请求总是成功的,因为实际开发时会遇到请求出错的情况,这时就需要我们做api error的处理,在本章节的练习中也会有这方面的处理)。
在之前create new task时,我们构建了一个uniqueId函数用来生成id,现在可以去掉了,服务器会负责添加id。现在让我们先添加一个将新创建的任务添加到服务器的api调用:
src/api/index.js
...
export function createTask(params) {
return client.post(params);
}
接着修改action创建器createTaskAct来返回一个函数,代码如下:
src/actions/index.js
...
// 创建一个新的服务器action创建器
export function createTaskSucceededAct(task) {
return {
type: CREATE_TASK_SUCCEEDED,
payload: {
task,
},
};
}
export function createTaskAct({ title, description, status = "Unstarted" }) {
return (dispatch) => {
api.createTask({ title, description, status }).then((res) => {
// POST请求成功后,会返回当前设置的task object
dispatch(createTaskSucceededAct(res.data));
});
};
}
接着再修改tasks reducer来处理CREATE_TASK_SUCCEEDED action:
src/reducers/index.js
...
export default function tasks(state = { tasks: [] }, action) {
switch (action.type) {
...
case CREATE_TASK_SUCCEEDED:
return { tasks: state.tasks.concat(action.payload.task) };
default:
return state;
}
}
这样,当刷新页面后,上一次新创建的任务不会丢失,而是存储于服务器中,然后通过初始的tasks请求后,将数据渲染到UI上。
5、本章总结
- 派发异步action和同步action的不同;
- 利用redux-thunk实现函数派发,借此可以执行一些副作用,如网络请求;
- api调用如何减少重复并提高可复用性;
- 两个action的分组概念:视图action和服务器action;
- 远程api调用生命周期中的三个重要时刻:发出请求、成功拿到请求数据、请求失败;
- 渲染error信息以提升整体的用户体验。
6、练习
还有修改任务状态的的功能未完成,将此功能的实现放在练习中,要求如下:
- 添加新的api条用以更新服务器上的任务;
- 将action创建器changeStateAct从同步改为异步;
- 在changeStateAct中进行ajax请求;
- 请求完成后,使用response data派发服务器action。
chapter_four包含此章节的所有实现,可以直接点击查看相关代码。