更多分享内容可访问我的个人博客
https://www.niuiic.top/
rtk-query 是进行网络请求的一个高级工具,下面介绍一下使用流程。
首先是新建一个基础 api。
// apis/base.ts
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const baseApi = createApi({
// 根据url自动选择http或者https协议
// 如果是移动端应用,由于模拟器中的网络与本地不一致,即使服务器在本地也不能使用127.0.0.1,必须用局域网地址
baseQuery: fetchBaseQuery({ baseUrl: "http://192.168.31.228:8000/v1/" }), // 发送请求的目的地址的前半部分
reducerPath: "baseApi", // 告诉redux-toolkit我们想把从这个api获取的数据放到store的什么位置
endpoints: () => ({}), // endpoints中放的是各种请求相关的函数,这里暂时为空,后续写具体的Api时再补充。
});
如果你没有现成的服务端程序,可以在 https://pokeapi.co/ 进行测试。
然后是写一个具体的 api,填充 endpoints。
// apis/user.ts
import { baseApi } from "../base";
// 请求返回值的类型
// 这里不需要严格对应,举个例子
// 假设真实返回值的类型比下面还多个time字段,或者说并没有data字段,下面定义的类型也是不会报错的
// 甚至返回的是一个json文件,且定义的返回值类型为string也是可以的,最终可以通过`.`操作符获取其中的字段
interface User {
data: string;
}
// 在baseApi的基础上创建userApi
const userApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
// 根据id去查询
// query用于检索数据,并可以向缓存提供标签
// 第一个参数是返回值的类型,第二个参数是传递给后端的数据类型(这里传递的是id,为number类型)
getUserById: builder.query<User, number>({
// 这里参数的类型必须和上面定义的一致
query: (id: number) => ({
// 请求地址的后半部分
url: `/user/${id}`,
// 请求的方法
method: "get",
}),
}),
// 根据id删除用户
// 对于改变服务器上的数据或可能会使缓存无效的任何内容应当使用mutation
deleteUserById: builder.mutation({
query: (id: number) => ({
url: `/user/${id}`,
method: "delete",
}),
}),
}),
overrideExisting: false,
});
// 导出可在函数式组件使用的hooks,它是基于定义的endpoints自动生成的
// 这个的名称是固定的,就是use加上前面定义的名字再加上query或者mutation
export const { useGetUserByIdQuery, useDeleteUserByIdMutation } = userApi;
然后就是将定义好的 api 与 store 联系起来。
// stores/Welcome.ts
import { configureStore } from "@reduxjs/toolkit";
import loginReducer from "../components/welcome/LoginRedux";
import { baseApi } from "../apis/base";
import { setupListeners } from "@reduxjs/toolkit/dist/query";
// 中间件集合
const middlewareHandler = (getDefaultMiddleware: any) => {
const middlewareList = [...getDefaultMiddleware()];
// 把api自动生成的中间件加进去
middlewareList.push(baseApi.middleware);
return middlewareList;
};
export const welcomeStore = configureStore({
reducer: {
// login是Welcome页面的一个模块
login: loginReducer,
// 把api自动生成的reducer加进入
[baseApi.reducerPath]: baseApi.reducer,
},
middleware: (getDefaultMiddleware) => middlewareHandler(getDefaultMiddleware),
});
// 这里主要是监听焦点、网络通断、组件可视性变化和其他一些事件,如果没有这些情况,就不需要监听
setupListeners(welcomeStore.dispatch);
export type WelcomeState = ReturnType<typeof welcomeStore.getState>;
export type WelcomeDispatch = typeof welcomeStore.dispatch;
现在已经全部配置完成,可以使用了。
// components/Login.tsx
import { TextInput, Button, View, Text } from "react-native";
import { useGetUserByIdQuery } from "../../apis/welcome/user";
export function Login() {
const { data, isLoading, isError } = useGetUserByIdQuery(1);
if (isLoading) {
return (
loading
);
}
if (isError) {
return (
Something went wrong
);
}
return (
The response is {data}
);
}
如果要进行轮询,那也很简单。前有export const { useGetUserByIdQuery, useDeleteUserByIdMutation } = userApi;
。只需在使用时传入轮询间隔即可。比如像下面这样。
const { data, isLoading, isError, refetch } = useGetUserByIdQuery(1, {
pollingInterval: 3000,
});
export function Login() {
const { data, isLoading, isError } = useGetUserByIdQuery(1);
}
从前面的例子中可以看到,发出请求的这些函数只能在函数式组件内部使用,也就是说,请求函数永远在组件被渲染之前调用,想要直接将其作为事件的处理函数是不可能的。如果需求是当按钮按下时发出才请求,就必须要进行进一步处理。
解决该问题的方案有三种,一种是使用lazy
版本的函数,比如useLazyGetUserByIdQuery
,一种是使用选择性调用特性,还有一种是使用mutation
。
第一种可能写的比较多,这里只介绍后两种。下面是代码片段。
import { useState } from "react";
export function Login() {
const [skip, setSkip] = useState(true);
// 下面的useVerifyUserQuery就相当于前面的useGetUserByIdQuery,与上面的定义方式没有区别。
// userState是useVerifyUserQuery定义时的参数,等同于useGetUserByIdQuery的id:number。
// {skip}是传入的第二个参数,该参数不需要定义。
// 这些配置使得该函数不会在一开始就执行。
const { data, error, isLoading, isUninitialized } =
Hooks.user.useVerifyUserQuery(userState, { skip });
// 然后定义一个按钮触发请求函数。
// 至此已经可以实现需求。除了这里的改动之外,其他地方都没有变。
const LoginButton = () => (
<Button title="登陆" onPress={() => setSkip((prev) => !prev)} />
);
return (
……
);
}
那么假设现在有两个按钮,每个按钮对应不同的请求,其余和之前相同,之前的写法还能用吗。显然是不行的,因为skip
就一个,按一个按钮给它改了,两个都触发了。所以,还需要改进。
再稍微看一下上面的代码,skip
就只是一个boolean
值,那么问题已经解决了。用createSlice
再建一个 Slice,其 state 的类型就假设为number
。然后可以在页面中获取该 state 的值。现在用{skip : !(state == 1)}
代替{skip}
,另一个写{skip : !(state == 2)}
。按钮响应事件改成修改state
的值就行,后面的就不用说了。这样不管是想要用一个按钮触发几个请求,不管页面上有多少个按钮想触发请求都是可以实现的。当然1, 2
这种过于低级,应当自行包装一下。
第三种方案是mutation
,以下是代码片段。
export const UserApi = BaseApi.injectEndpoints({
endpoints: (builder) => ({
verifyUser: builder.mutation<boolean, User>({
query: (user: User) => ({
url: `/user/${user.name}:${user.password}`,
method: "get",
}),
// 第一个参数是传入的User
// 第二个参数是MutationLifecycleApi的解构
async onQueryStarted(
arg,
{ dispatch, getState, queryFulfilled, requestId, extra, getCacheEntry }
) {
console.log("onQueryStarted");
},
// 第一个参数同上
// 第二个参数是MutationCacheLifecycleApi的解构
async onCacheEntryAdded(
arg,
{
dispatch,
getState,
extra,
requestId,
cacheEntryRemoved,
cacheDataLoaded,
getCacheEntry,
}
) {
console.log("onCacheEntryAdded");
},
}),
}),
overrideExisting: false,
});
这里定义了一个mutation
函数,其中有两个钩子函数,分别是onQueryStarted
和onCacheEntryAdded
,看名称就知道是怎么回事。
const [verifyUser, result] = Hooks.user.useVerifyUserMutation();
const LoginButton = () => (
<Button
title="登陆"
onPress={() => {
verifyUser(userState);
}}
/>
);
使用的时候也很简单,useVerifyUserMutation()
会返回一个触发函数和一个result
。触发函数可以在生命域内任何地方使用。result
内含status, error, data
。
现在还有一个问题,如何在发出请求并且接收到数据之后再对数据进行处理。比如,请求返回了用户的信息,想把这个信息存到 store 中。在刚才的例子中响应事件时只是给了触发请求的条件。在此之后对返回的数据进行操作显然不能保证数据已经返回来了。使用
mutation
的时候虽然有两个钩子函数,但是暂时没能在里面搞到返回的结果。onCacheEntryAdded
中打印一下它的参数getCacheEntry()
,发现isLoading=true
,但不知道这个东西是怎么判断的,因为此时数据还没入缓存,流程还没走完,isLoading=true
也可以理解,就这个函数的用处来说此时数据应当已经返回。
最终笔者找到一个不是非常理想的方案,来看以下代码。
export const UserApi = BaseApi.injectEndpoints({
endpoints: (builder) => ({
verifyUser: builder.mutation<boolean, User>({
query: (user: User) => ({
url: `/user/${user.name}:${user.password}`,
method: "get",
}),
transformResponse: (response: boolean, meta, arg) => {
console.log("real", arg);
return response;
},
}),
}),
overrideExisting: false,
});
关键在transformResponse
,该属性在query
和mutation
中都存在,在这里对数据进行处理完全不用担心是否返回的问题,只是它的本意应当是整理返回的数据。另外,在这个函数中除了 response
,其他的参数meta
是设备信息,arg
是verifyUser
输入的参数,没有现成的可以将返回值存入 store 或者其他地方的函数。这个 return 的response
就是使用时返回的result
中的data
。
还有要注意的一点是这里的response: boolean
必须和返回类型一致。如果写成response: {data: boolean}
和return response.data
,虽然不会报错,但result
中的data
永远是undefined
。
有兴趣的可以研究一下文档,找出mutation
的正确用法。
之前添加了一个 api 自动生成的中间件,现在可以模仿之前的写法添加更多中间件。
使用redux-logger
。
import logger from "redux-logger";
const middlewareHandler = (getDefaultMiddleware: any) => {
const middlewareList = [...getDefaultMiddleware()];
middlewareList.push(baseApi.middleware);
// 开发环境下加上redux logger中间件
if (process.env.NODE_ENV === "development") {
middlewareList.push(logger);
}
return middlewareList;
};
默认情况下应当就是开发环境,不需要额外设置
import {
configureStore,
MiddlewareAPI,
isRejectedWithValue,
Middleware,
} from "@reduxjs/toolkit";
import loginReducer from "../components/welcome/LoginRedux";
import { baseApi } from "../apis/base";
import { setupListeners } from "@reduxjs/toolkit/dist/query";
import logger from "redux-logger";
// 错误中间件
export const rtkQueryErrorLogger: Middleware =
(api: MiddlewareAPI) => (next) => (action) => {
// 拦截错误,httpstatus不是200的时候
if (isRejectedWithValue(action)) {
console.warn("中间件拦截了");
} else {
console.log(action, "未发生错误", api);
}
return next(action);
};
// 中间件集合
const middlewareHandler = (getDefaultMiddleware: any) => {
const middlewareList = [...getDefaultMiddleware()];
// 加上错误处理中间件
middlewareList.push(rtkQueryErrorLogger);
middlewareList.push(baseApi.middleware);
// 开发环境下加上redux logger中间件
if (process.env.NODE_ENV === "development") {
middlewareList.push(logger);
}
return middlewareList;
};
该中间件是自定义的,功能不强,只能通过 httpstatus 判别错误。更进一步需要拦截器。不过用了拦截器中间件似乎没法用了,不知是哪里没写对,暂未解决。
这里数据持久化使用的是redux-persist
。再来看一个简单的案例,为了简化,就不用 rtk-query 了。
这是一个使用 Expo cli 建立的 react native 项目,目录结构如下。
App.tsx
pages/
Welcome.tsx
components/
welcome/
LoginRedux.ts
Login.tsx
stores/
Welcome.ts
hooks/
Welcome.ts
实现一个简单的登陆功能,输入用户名为user
,密码为password
时显示success
,否则显示fail
。
App.tsx
是入口,内容如下。
import { View } from "react-native";
import Welcome from "./pages/Welcome";
export default function App() {
return (
);
}
这里调用的 Welcome 来自pages/Welcome.tsx
。
import { Provider } from "react-redux";
import { Login } from "../components/welcome/Login";
import { welcomeStore, welcomePersistor } from "../stores/Welcome";
import { PersistGate } from "redux-persist/integration/react";
export default function Welcome() {
return (
);
}
这里出现的PersistGate
就是数据持久化插件的内容。下面来到stores/Welcome.ts
看填入的welcomePersistor
是如何生成的。
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import loginReducer from "../components/welcome/LoginRedux";
import { persistStore, persistReducer } from "redux-persist";
import ExpoFileSystemStorage from "redux-persist-expo-filesystem";
const persistConfig = {
key: "root",
storage: ExpoFileSystemStorage,
};
const persistedReducer = persistReducer(
persistConfig,
combineReducers({
login: loginReducer,
})
);
export const welcomeStore = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
// serializableCheck这个中间件需要关掉,否则会报错
getDefaultMiddleware({ serializableCheck: false }),
});
export const welcomePersistor = persistStore(welcomeStore);
export type WelcomeState = ReturnType<typeof welcomeStore.getState>;
export type WelcomeDispatch = typeof welcomeStore.dispatch;
storage 是存储数据的仓库,可以在这里查看并选择合适的,当前用的只适用于 Expo SDK 环境下
其他的内容与不使用数据持久化并无差别,这里将其补齐。
components/welcome/LoginRedux.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface LoginState {
username: string;
password: string;
authentication: boolean;
}
const initialState = {
username: "",
password: "",
authentication: false,
} as LoginState;
export const loginSlice = createSlice({
name: "login",
initialState,
reducers: {
login: (state) => {
if (state.username == "user" && state.password == "password") {
state.authentication = true;
} else {
state.authentication = false;
}
},
setUsername: (state, action: PayloadAction<string>) => {
state.username = action.payload;
},
setPassword: (state, action: PayloadAction<string>) => {
state.password = action.payload;
},
},
});
export const { login, setUsername, setPassword } = loginSlice.actions;
export default loginSlice.reducer;
components/welcome/Login.tsx
import { TextInput, Button, View, Text } from "react-native";
import { useWelcomeDispatch, useWelcomeSelector } from "../../hooks/Welcome";
import { login, setUsername, setPassword } from "./LoginRedux";
export function Login() {
const loginState = useWelcomeSelector((state) => state.login);
const dispatch = useWelcomeDispatch();
return (
Username:
dispatch(setUsername(value))}
/>
Password:
dispatch(setPassword(value))}
/>
);
}
hooks/welcome.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { WelcomeState, WelcomeDispatch } from "../stores/Welcome";
export const useWelcomeDispatch = () => useDispatch<WelcomeDispatch>();
export const useWelcomeSelector: TypedUseSelectorHook<WelcomeState> =
useSelector;
现在的效果是输入用户名和密码之后刷新 app,可以发现之前输入的内容依旧存在。说明经过配置的这一个 store 存储的内容都被持久化了。
如果使用rtk-qeury
实在有困难,也可以使用axios
作为替代方案,下面是例子。
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
import axios from "axios";
import { User } from "utils/types";
const login = createAsyncThunk("user/login", async (user: User) => {
const { data } = await axios.get(
"http://192.168.1.109:8000/v1/user/user:password"
);
return data;
});
const initialState = {
name: "",
password: "",
} as User;
const UserSlice = createSlice({
name: "user",
initialState,
reducers: {},
extraReducers: {
[login.pending.type]: (state) => {
console.log("is loading");
},
[login.fulfilled.type]: (state, action) => {
// 打印返回的数据
console.log("is fulfilled", action.payload);
},
[login.rejected.type]: (state, action: PayloadAction<string | null>) => {
console.log("is rejected");
},
},
});
const actionCreators = UserSlice.actions;
export const UserActionCreators = { actionCreators, login };
export const UserReducer = UserSlice.reducer;
以上代码应当不需要解释,可以看到异步函数的三种状态pending
, fulfilled
, rejected
已经列出,比起之前的方案显然要简单的多。
注意这个不是请求的三种状态。可以在异步函数中通过请求的返回值来判断有没有出问题,然后触发异步的
fulfilled
和rejected
状态。
使用的时候只要像一般的 action creators 一样使用login
即可。只是需要注意一点。之前使用export const useStoreDispatch = () => useDispatch
和const dispatch = useStoreDispatch();
生成的dispatch
不能再使用。建议直接把类型限定关掉,即将useDispatch
改为useDispatch()
。