Redux 工具箱之数据请求入门

1,208 阅读3分钟

@reduxjs/toolkitredux 封装了很多实用 api,比如 createSlice, 创建一个切片,既可以避免写很多的样板代码(代价:学习/时间)。其中 Toolkit 对数据请求进行了支持。

之前

  • redux-thunk 将函数作为 action 派发,是 redux 中使用作为广泛和简单的异步数据请求方案。(目前, toolkit 中内置 thunk 功能)
  • redux-saga 基于 Generator 生成器,支持复杂的异步请求,学习起来相对 redux-thunk 难度大
  • ...

下面使用 express 为后端提供 api 数据,React/Vite 作为前端,来使用 Redux 的工具箱。

安装 @reduxjs/toolkit

npm install @reduxjs/toolkit

使用 vite 初始化一个 React + TS 项目

pnpm create vite rtk-query # 选择 react/ts

前端依赖

pnpm install redux react-redux @reduxjs/toolkit qs

qs 将对象, post 表单数据数据序列变化

setNoStore: build.mutation({
  query(amount) {
    return {
      url: "set-nostore",
      method: "POST",
      body: qs.stringify(amount),
    };
  },
}),

后端依赖

pnpm install  express nodemon esno cors @types/node
  • cors 处理跨域问题
  • nodemon 监听文件自动重启 express 服务
  • esno 运行 ts 文件

nodemon.json 简单配置

{
  "verbose": false,
  "debug": false,
  "exec": "esno  ./src/index.ts", // 重新执行文件
  "ignore": [
    "node_modules",
    "./test",
    "**/*.d.ts",
    "*.test.ts",
    "*.spec.ts",
    "fixtures/*",
    "test/**/*",

  ],
  "events": {
    "restart": ""
  },
  "watch": ["./server"],
  "ext": "ts tsx js jsx",
  "inspect": true
}

package.json 添加 server 启动

"scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "server": "nodemon"
},

express 服务

import path from "path";
import express from "express";
import cors from "cors";

const app = express();

app.use(
  "/static",
  express.static(path.join(__dirname, "../static"), {
    index: false,
  })
);

app.use(cors());

app.use(express.urlencoded({ extended: false }));

app.listen(9898, () => {
  console.info(`===> api server is running at http://localhost:9898`);
});
  • 设置静态文件 static 目录
  • 设置接受跨域请求
  • 设置 urlencoded 的解析
  • ...

后续需要测试的接口就可以以中间的形式添加在 express 服务内部.

一个简单的测试中间件

app.get("/", (_, res) => {
  res.send({ code: 1, data: { app: "express server " } });
});

如何使用 redux toolkit 的 query

  • 不依赖 store
  • 与 store 同行

在没有 store 情况下是使用 rtk query

mkdir src/server && cd src/server
touch nostore.ts

app.get("/query-nostore", (_, res) => {
  res.send({
      code: 0,
      data: {
        tip: 'test data',
      },
      msg: "success",
    });
})

rtk-query 前置知识

如果熟悉 graphql 的 api, grapgql 将请求主要抽象为:

  • query 查询
  • muation 变化

rtk-query 也采用了类似的方式方式,构建 api

构建 api

api 需要 baseQuery 参数,@reduxjs/toolkit 默认提供基于 fetchfetchBaseQuery(当然也可以自定义的 baseQuery, 官方文档中,提供了 axios/graphql 的两种):

import {
  createApi,
  fetchBaseQuery,
} from "@reduxjs/toolkit/query/react";

import qs from 'qs';

export const api = createApi({
  baseQuery: fetchBaseQuery({ // fetchBaseQuery
    baseUrl: "http://localhost:9899",
    prepareHeaders: (headers, api) => {
      headers.set(
        "Content-Type",
        "application/x-www-form-urlencoded;charset=UTF-8"
      );
      return headers;
    },
    
  }),
  endpoints: () => ({}),
});

// 分离 api
const noStoreApi = api.injectEndpoints({
  endpoints: (build) => ({
    getNoStore: build.query({
      query: () => "query-nostore",
    }),
    setNoStore: build.mutation({
      query(amount) {
        return {
          url: "set-nostore",
          method: "POST",
          body: qs.stringify(amount), // const a = qs.stringify({a: 1, b: 2}); // a=1&b=2
        };
      },
    }),
  }),
});

export const { useGetNoStoreQuery, useSetNoStoreMutation } = noStoreApi;
  • useGetNoStoreQuery 等价于 api.endpoints.getNoStore.useQuery

使用 ApiProvider 让 hook 调用生效

import { api } from "./server/nostore";
import { ApiProvider } from "@reduxjs/toolkit/query/react";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <ApiProvider api={api}>
    <App />
  </ApiProvider>
);

前端 hooks 使用实例

import { useGetNoStoreQuery, useSetNoStoreMutation } from './server/nostore'
function App() {
  const {data, error} = useGetNoStoreQuery("")
  const [setNoStore] = useSetNoStoreMutation(); 

  const onSubmit = (e) => {
    e.preventDefault();
    const { target } = e
    setNoStore({
      a: target[0].value,
      b: target[1].value,
    })
  }

  if(error) {
    return <>error</>
  }

  return (
    <div className="App">
      {
        JSON.stringify(data)
      }
      <div>
        <form name='f' onSubmit={onSubmit} encType="application/x-www-form-urlencoded">
          <input name="username" type="text" />
          <br/>
          <input name="password" type="text" />
          <br />
          <button type="submit">提交</button>
        </form>
      </div>
    </div>
  )
}

export default App

express 后端接口

app.use(express.urlencoded({ extended: false, }));
app.get("/query-nostore", (req, res) => {
  res.send({
      code: 0,
      data: {
        tip: 'test data',
      },
      msg: "success",
    });
})

app.post("/set-nostore", (req, res) => {
  res.send({
      code: 0,
      data: {
        tip: 'test data',
        ...req.body
      },
      msg: "success",
    });
})
  • 总之, rtk-query 解决的是数据请求的问题,与 redux 专门数据管理还是不一样。
  • 到这里,打通了前端后的基本使用。使用 express 作为后端服务(有跨域),提供了 get/post 两种接口的基本解析方案。
  • 前端实例化 redux store 情况下使用 ApiProvider 来使得钩子函数生效, 拆分 api 的方法。
  • rtk-query 默认使用 fetch 来发生请求。
  • ...

与 class 组件配合

与 function 组件有所有不同,class 组件不能直接使用钩子函数获取数据,获取数据需要初始化 store, 使用 connect 函数,state/dispatch, map 为 Props, 核心方法是:

  • initiate 方法
onst mapState = (state: RootState) => ({
    users: api.endpoints.getUsers.select(3)(state)
});

const mapDispatch = {
    getUsers: api.endpoints.getUsers.initiate,
    updateUser: api.endpoints.updateUser.initiate
};

函数式编程更加推荐,因为可以直接使用,不用下载 axios 等请求工具,且能能够方便的管理 api。

后面的探索

  • 与 toolkit 其他的相互配合
  • 使用 store 实例
  • token 存储机制

参考