背景介绍
一个月前发布了关于Redux到Redux Toolkit的转变(上),讲述了关于RTK的一些基本用法及其原理。本片内容会围绕RTK关于如何规范化处理异步数据请求作用法以及原理的详细介绍,属于RTK知识的一个进阶。下面我们就开始createAsync和RTK query的详细介绍。
构建目的
我们来回忆下,在自己的项目中是怎么做异步数据处理的呢。无非就是一下三个步骤(这里结合redux来说哈):
- 数据请求之前,dispatch一个数据求的action,设置loading为true,表示正在进行数据请求,然后ui层面根据loading状态显示相应的ui组件;
- 接着利用数据请求工具,比如axios、fetch等进行真正的数据请求;
- 根据数据请求的结果,成功或者失败,在dispatch相应的action,并设置loading为false,表示请求结束。成功了,则显示相应的数据成果。失败了,则显示相应的失败ui。
好了,整个异步数据的处理到此结束。我们在这样做的时候,需要自己去写相应的reducer处理,需要构建一定的数据结构,比如{data:xx,loading:xx,error:xxx}, 需要在各个阶段进行action的派发等等。RTK则帮助我们将这整个过程做了封装,简化了我们手动处理的这整个过程,甚至还做了一些关于数据缓存的逻辑(类似React Query)
两个关键
createAsync
- 基本用法: createAsync接受两个参数:redux action类型字符串和一个必须返回promise的回调函数。它会以你传入的redux action类型字符串为前缀,生成promise生命周期(pending、fulfilled、rejected)action types。它的返回值是一个thunk action creator(熟悉redux的同学应该知道thunk简单来说就是一个类型为function的action),而且会根据其传入的回调函数返回的promise的状态自动的dispatch 前面生成的promise生命周期action types。下面直接上代码:
import { createSlice, configureStore, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
const getTodoList = createAsyncThunk("todos/list", async (id, {rejectWithValue}) => {
const result = await axios.get(
"http://127.0.0.1:4523/m1/1340137-0-default/api/todos/list"
);
if (result.data.status >= 80) {
return rejectWithValue(result.statusText)
}
return result
});
const initialState = {
todos: [],
loading: false,
error: null,
};
const todoSlice = createSlice({
name: "todos",
initialState,
reducers: {},
extraReducers: {
[getTodoList.pending.type]: (state, action) => {
state.loading = true;
console.log(action, "pending");
},
[getTodoList.fulfilled.type]: (state, action) => {
state.loading = false;
state.todos = action.payload.data.data;
console.log(action.payload, "fulfilled");
},
[getTodoList.rejected.type]: (state, action) => {
state.loading = false;
state.error = action.error;
console.log(action, "rejected");
}
},
});
const { reducer, actions } = todoSlice;
const store = configureStore({
reducer,
});
const promise = store.dispatch(getTodoList(2));
// promise.abort('自动取消的')
promise.then(
(data) => {
console.log("success", data, store.getState());
},
(error) => {
console.log("error", error);
}
);
其中getTodoList是createAsyncThunk的返回值,根据传入的字符串前缀todos/list,其生成了getTodoList.pending、getTodoList.fulfilled、getTodoList.rejected三个action creator,通过打印可以看到其对应的action type分别是todos/list/pending、todos/list/fulfilled、todos/list/rejected。
然后在createSlice中我们简单的根据三个生命周期action type做了相应的reducer逻辑处理,即在pending的时候将loading设置为true,在成功fulfilled的时候,处理对应的数据(不同的数据格式怎么去接洽),以及在失败rejected的时候,我们需要怎么去处理error的数据。到此,可以看到createAsyncThunk帮我们做了很大一部分的简化,我们不再关注什么时候去派发何种类型的action,仅仅是关注各个reducer里面关于数据处理的逻辑。那么其内部到底做了何种封装呢,接着往下看。
- 原理探究
createAsyncThunk返回了一个thunk action creator,即就是一个返回值为function的action creator,而且该function的返回值是一个promise(从上面const promise = store.dispatch(getTodoList(2))可以看出)。这个function内部呢,我们大概也能猜到,就是帮我们封装好了异步数据处理的那三个流程即请求前,请求中,请求后该做的事情。其核心代码实现如下:
function createAsyncThunk(typePrefix, payloadCreator) {
const actionCreator = (args) => {
return (dispatch, getState) => {
const promise = (async () => {
let finalPromise;
try {
//发起请求前
dispatch(pending());
//发起请求
finalPromise = await payloadCreator(args)
.then(
(result) => {
//请求后的处理,成功状态
dispatch(fulfilled(result));
},
(err) => {
//请求后的处理,出错状态
dispatch(rejected(err));
}
)
} catch (err) {
//出错状态
dispatch(rejected(err));
}
return finalPromise;
})();
return promise;
};
};
return actionCreator;
}
接下来,补充下里面pending、fulfilled、rejected三个生命周期相关的action creator的实现,而且这也是createAsyncThunk返回值向外暴露的三个属性值,借助于(上篇)中的createAction很容易实现:
import createAction from "./createAction";
function createAsyncThunk(typePrefix, payloadCreator) {
const pending = createAction(`${typePrefix}/pending`, () => ({
payload: undefined,
}));
const fulfilled = createAction(`${typePrefix}/fulfilled`, (payload) => ({
payload,
}));
const rejected = createAction(`${typePrefix}/rejected`, (error, payload) => ({
payload,
error,
}));
const actionCreator = (args) => {
return (dispatch, getState) => {
const promise = (async () => {
let finalPromise;
try {
dispatch(pending());
finalPromise = await payloadCreator(args)
.then(
(result) => {
dispatch(fulfilled(result));
},
(err) => {
dispatch(rejected(err));
}
)
} catch (err) {
dispatch(rejected(err));
}
return finalPromise;
})();
return promise;
};
};
return Object.assign(actionCreator, {
pending,
rejected,
fulfilled,
});
}
export default createAsyncThunk;
这样,一个基本的createAsyncThunk功能就算完成了。这个留下一个小问题,在源码里面,返回的promise上还挂载了“取消”的功能,即在请求发出之后,假如在规定的时间内没有得到响应,我想要提前结束这个promise,该怎么操作呢,小伙伴儿们快动动你们聪明的大脑吧。
RTK Query
createAsyncThunk里面,我们还是需要去写各个生命周期阶段的reducer逻辑,去处理数据为我们所用。有没有一种可能,我们也不需要写各种reducer逻辑了,数据的结构也帮我们封装好了,我就按照给定的某种结构去获取数据呢。答案就是RTK Query里面的createApi,它是RTK Query里面最为核心的功能。
- 基本用法
createApi允许你定义一些列的endpoints(终端),用于处理数据获取,但是这仅仅需要简单的机械配置即可(即不需要你写太多的逻辑)。大多数情况下,在你的项目中,你紧紧需要使用一次createApi即可,以baseUrl为基础开展你的业务逻辑。此外RTK Query还提供了一个api fetchBaseQuery。它是对fetch的一个封装,你可以看成是数据获取的工具,当然你也可以对axios进行自己的封装,取代fetchBaseQuery,作为你自己项目里面的数据获取工具。看下基础使用的代码:
//index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { store } from "./store";
import { Provider } from "react-redux";
ReactDOM.render(
<Provider store={store}><App /></Provider>,
document.getElementById('app')
)
//store.js
import { configureStore } from '@reduxjs/toolkit'
import { todoApi } from './todos'
export const store = configureStore({
reducer: {
// Add the generated reducer as a specific top-level slice
[todoApi.reducerPath]: todoApi.reducer,
},
// Adding the api middleware enables caching, invalidation, polling,
// and other useful features of `rtk-query`.
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(todoApi.middleware),
})
//todos.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
// Define a service using a base URL and expected endpoints
export const todoApi = createApi({
reducerPath: 'todoApi',
baseQuery: fetchBaseQuery({ baseUrl: 'http://127.0.0.1:4523/m1/1340137-0-default'}),
endpoints: (builder) => ({
getTodoDetailById: builder.query({
query: (id) => `/api/todos/detail/${id}`,
}),
getTodoList: builder.query({
query: () => `/api/todos/list`
})
})
})
// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const { useGetTodoDetailByIdQuery } = todoApi
//app.js
import {todoApi} from './todos';
const {useGetTodoDetailByIdQuery} = todoApi;
export default props => {
const {data, error, isLoading} = todoApi.endpoints.getTodoDetailById.useQuery(111) || useGetTodoDetailByIdQuery(123);
if (isLoading) return <div>loading...</div>;
if (error) return <div>error...</div>;
return <div>success: {data?.text}</div>
}
关键在store.js和todos.js两个js里面。store里面引入todo里调用createApi后返回的todoApi,可以看到todoApi自动包含了reducer和相应的middleware。而且todoApi还暴露出useGetTodoDetailByIdQuery这样一个自定义hook供开发人员使用。这样我们就只需要在app里面调用自定义hook,关心ui的渲染逻辑即可,不得不说,整个流程一气呵成,简直不要太爽!接下来又到了核心功能解析。
- 原理探究
从store.js和app.js里面可以看出createApi返回的几个核心属性:reducerPath、reducer、middleware、endpoints,说白了,就是帮我们把创建reducer,middleware的逻辑进行了封装。
- reducerPath: reducer的名称,直接返回传入createApi的reducerPath参数即可。
- reducer:
const apiSlice = createSlice({
name: reducerPath,
initialState: {data: undefined, error: undefined, isLoading: false},
reducers: {
setValue(state, action) {
const {payload} = action;
for (let key in payload) {
state[key] = payload[key]
}
}
}
})
const {reducer, actions: {setValue}} = apiSlice;
直接调用createSlice,传入reducer的名字以及数据结构,以及相应的reducer即可。 3. middleware:
const fetchData = createAction('fetch_data');
const middleware = ({dispatch}) => (next) => (action) => {
//处理数据请求
if (action.type === fetchData.type) {
try {
dispatch(setValue({isLoading: true}))
const {url} = action.payload;
(async () => {
const data = await baseQuery(url);
dispatch(setValue({isLoading: false, data}))
})()
} catch (error) {
dispatch(setValue({isLoading: false, error}))
}
} else {
//到下一个中间件
next(action)
}
}
熟悉redux的小伙伴肯定对middleware的原理不陌生,里面就是对数据请求前,请求中,请求后的一个简单封装。 4. endpoints createApi传入的endpoints参数是一个函数,其形参包含一个builder对象,结合app.js里面的调用todoApi.endpoints.getTodoDetailById.useQuery(111),实现的时候就按照以上标准去构建。
import { ReactReduxContext } from 'react-redux';
const builder = {
query: (options) => {
const useQuery = (id) => {
const {store} = useContext(ReactReduxContext);
const [,forceUpdate] = useState({});
useEffect(() => {
let unsubscribe = store.subscribe(() => forceUpdate({}));
store.dispatch({
type: fetchData.type,
payload: {
url: options.query(id)
}
})
return unsubscribe;
}, [])
// console.log(store.getState().todoApi, 'state')
return store.getState()[reducerPath];
}
return {useQuery}
}
}
const endPointsObj = endpoints(builder);
通过useContext拿到了对应的store,然后在useEffct里面主动dispatch了一个fetch-data的action,这个时候中间件里面就会对此action做相应的处理。最后通过store.getState,以及对应的reducer name返回了最终的状态。值得一提的是,在useEffct里面我们除了发起action之外,还对订阅了store的变化,在变化之后通过state的改变去触发ui的重新渲染。
还有一个小小的优化,就是根据endpoints里面函数返回的key,createApi会构建起对应的自定义hook,实现如下:
const otherObj = {};
for (let key in endPointsObj) {
let newKey = `use${key[0]?.toLocaleUpperCase()}${key.slice(1)}Query`;
otherObj[newKey] = (id) => endPointsObj[key].useQuery(id)
}
const api = {
reducerPath,
reducer,
middleware,
endpoints: endPointsObj,
...otherObj
}
这样,我们就能像上面app.js里面那样使用诸如useGetTodoDetailByIdQuery各种类型的自定义hook以此获取到相应的数据。
整个createApi的基础功能就到此结束了,当然createApi还远不止这些。它强大的数据缓存功能,以及一些功能性的api,怎样结合GraphQL等等优秀功能,需要我们更好的熟悉文档,并且在项目中实践起来。
写在最后
关于react toolkit的使用及其简单原理的介绍就到此为止了,一共上下两篇,希望能给到广大读者一点点帮助,关于数据管理的工具有很多,我们只需熟练的掌握其中一两种即可,其他的可以作为了解,趁手的工具适合自己即可,谢谢大家~