从Redux到Redux Toolkit的转变(下)

1,208 阅读7分钟

背景介绍

一个月前发布了关于Redux到Redux Toolkit的转变(),讲述了关于RTK的一些基本用法及其原理。本片内容会围绕RTK关于如何规范化处理异步数据请求作用法以及原理的详细介绍,属于RTK知识的一个进阶。下面我们就开始createAsync和RTK query的详细介绍。

构建目的

我们来回忆下,在自己的项目中是怎么做异步数据处理的呢。无非就是一下三个步骤(这里结合redux来说哈):

  1. 数据请求之前,dispatch一个数据求的action,设置loading为true,表示正在进行数据请求,然后ui层面根据loading状态显示相应的ui组件;
  2. 接着利用数据请求工具,比如axios、fetch等进行真正的数据请求;
  3. 根据数据请求的结果,成功或者失败,在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的逻辑进行了封装。

  1. reducerPath: reducer的名称,直接返回传入createApi的reducerPath参数即可。
  2. 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的使用及其简单原理的介绍就到此为止了,一共上下两篇,希望能给到广大读者一点点帮助,关于数据管理的工具有很多,我们只需熟练的掌握其中一两种即可,其他的可以作为了解,趁手的工具适合自己即可,谢谢大家~