二十四、案例:权限管理

郁子大约 13 分钟约 3883 字笔记React18李立超

(一)创建案例框架

1. src/index.js

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter as Router } from "react-router-dom";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Router>
    <App />
  </Router>,
);

2. src/App.js

import React from "react";
import { Route, Routes } from "react-router-dom";
import Layout from "./components/Layout";
import HomePage from "./pages/HomePage";
import ProfilePage from "./pages/ProfilePage";

const App = () => {
  return (
    <Layout>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/profile" element={<ProfilePage />} />
      </Routes>
    </Layout>
  );
};

export default App;

3. src/components/Home.jsx

import React from "react";

const Home = () => {
  return (
    <div>
      <h2>这是主页</h2>
      <p>此页面无需权限</p>
    </div>
  );
};

export default Home;

4. src/components/Layout.jsx

import React from "react";
import MainMenu from "./MainMenu";

const Layout = (props) => {
  return (
    <div>
      <MainMenu />
      <hr />
      {props.children}
    </div>
  );
};

export default Layout;

5. src/components/MainMenu.jsx

import React from "react";
import { Link } from "react-router-dom";

const MainMenu = () => {
  return (
    <div>
      <ul>
        <li>
          <Link to={"/"}>首页</Link>
        </li>
        <li>
          <Link to={"/profile"}>用户信息</Link>
        </li>
      </ul>
    </div>
  );
};

export default MainMenu;

6. src/components/Profile.jsx

import React from "react";

const Profile = () => {
  return (
    <div>
      <h2>用户信息</h2>
      <p>此页面只有在登录后才能查看</p>
    </div>
  );
};

export default Profile;

7. src/pages/HomePage.jsx

import React from "react";
import Home from "../components/Home";

const HomePage = () => {
  return (
    <div>
      <Home />
    </div>
  );
};

export default HomePage;

8. src/pages/ProfilePage.jsx

import React from "react";
import Profile from "../components/Profile";

const ProfilePage = () => {
  return (
    <div>
      <Profile />
    </div>
  );
};

export default ProfilePage;

(二)创建表单

1. src/App.js

import React from "react";
import { Route, Routes } from "react-router-dom";
import Layout from "./components/Layout";
import AuthFormPage from "./pages/AuthFormPage";
import HomePage from "./pages/HomePage";
import ProfilePage from "./pages/ProfilePage";

const App = () => {
  return (
    <Layout>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/profile" element={<ProfilePage />} />
        <Route path="/auth-form" element={<AuthFormPage />} />
      </Routes>
    </Layout>
  );
};

export default App;

2. src/pages/AuthFormPage.jsx

import React from "react";
import AuthForm from "../components/AuthForm";

const AuthFormPage = () => {
  return (
    <div>
      <AuthForm />
    </div>
  );
};

export default AuthFormPage;

3. src/components/AuthForm.jsx

import React, { useState } from "react";
import { useRef } from "react";

const AuthForm = () => {
  const [isLoginForm, setIsLoginForm] = useState(true);

  const usernameInp = useRef();
  const passwordInp = useRef();
  const emailInp = useRef();

  const submitHandler = (e) => {
    e.preventDefault();
    const username = usernameInp.current.value;
    const password = passwordInp.current.value;
    if (!isLoginForm) {
      const email = emailInp.current.value;
      console.log("注册", username, password, email);
    } else {
      console.log("登录", username, password);
    }
  };

  return (
    <div>
      <h2>{isLoginForm ? "登录" : "注册"}</h2>
      <form onSubmit={submitHandler}>
        <div>
          <input type="text" placeholder="用户名" ref={usernameInp} />
        </div>
        {!isLoginForm && (
          <div>
            <input type="email" placeholder="邮箱" ref={emailInp} />
          </div>
        )}
        <div>
          <input type="text" placeholder="密码" ref={passwordInp} />
        </div>
        <div>
          <button>{isLoginForm ? "登录" : "注册"}</button>
          <div>
            <a
              href="#"
              onClick={(e) => {
                e.preventDefault();
                setIsLoginForm((pre) => !pre);
              }}
            >
              {isLoginForm ? "没有账号?点击注册" : "已有账号?点击登录"}
            </a>
          </div>
        </div>
      </form>
    </div>
  );
};

export default AuthForm;

(三)完成注册功能

1. src/index.js

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter as Router } from "react-router-dom";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <Router>
      <App />
    </Router>
  </Provider>,
);

2. src/components/AuthForm.jsx

import React, { useState } from "react";
import { useRef } from "react";
import { useRegisterMutation } from "../store/api/authApi";

const AuthForm = () => {
  const [isLoginForm, setIsLoginForm] = useState(true);

  const [regFn, { error: regError }] = useRegisterMutation();

  const usernameInp = useRef();
  const passwordInp = useRef();
  const emailInp = useRef();

  const submitHandler = (e) => {
    e.preventDefault();
    const username = usernameInp.current.value;
    const password = passwordInp.current.value;
    if (isLoginForm) {
      console.log("登录", username, password);
    } else {
      const email = emailInp.current.value;
      // console.log("注册", username, password, email);
      regFn({
        username,
        password,
        email,
      }).then((res) => {
        // console.log(res);
        if (!res.error) setIsLoginForm(true);
      });
    }
  };

  return (
    <div>
      <p style={{ color: "red" }}>{regError && regError.data.error.message}</p>
      <h2>{isLoginForm ? "登录" : "注册"}</h2>
      <form onSubmit={submitHandler}>
        <div>
          <input type="text" placeholder="用户名" ref={usernameInp} />
        </div>
        {!isLoginForm && (
          <div>
            <input type="email" placeholder="邮箱" ref={emailInp} />
          </div>
        )}
        <div>
          <input type="text" placeholder="密码" ref={passwordInp} />
        </div>
        <div>
          <button>{isLoginForm ? "登录" : "注册"}</button>
          <div>
            <a
              href="#"
              onClick={(e) => {
                e.preventDefault();
                setIsLoginForm((pre) => !pre);
              }}
            >
              {isLoginForm ? "没有账号?点击注册" : "已有账号?点击登录"}
            </a>
          </div>
        </div>
      </form>
    </div>
  );
};

export default AuthForm;

3. src/store/index.js

import { configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/dist/query/react";
import { authApi } from "./api/authApi";

const store = configureStore({
  reducer: {
    [authApi.reducerPath]: authApi.reducer,
  },
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(authApi.middleware),
});

setupListeners(store.dispatch);

export default store;

4. src/store/api/authApi.js

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

export const authApi = createApi({
  reducerPath: "authApi",
  baseQuery: fetchBaseQuery({
    baseUrl: "http://localhost:1337/api/",
  }),
  endpoints: (build) => {
    return {
      register: build.mutation({
        query: (user) => {
          return {
            url: "/auth/local/register",
            method: "post",
            body: user,
          };
        },
      }),
    };
  },
});

export const { useRegisterMutation } = authApi;

(四)完成登录注册

1. src/components/AuthForm.jsx

import React, { useState } from "react";
import { useRef } from "react";
import { useLoginMutation, useRegisterMutation } from "../store/api/authApi";

const AuthForm = () => {
  const [isLoginForm, setIsLoginForm] = useState(true);

  const [regFn, { error: regError }] = useRegisterMutation();
  const [logFn, { error: logError }] = useLoginMutation();

  const usernameInp = useRef();
  const passwordInp = useRef();
  const emailInp = useRef();

  const submitHandler = (e) => {
    e.preventDefault();
    const username = usernameInp.current.value;
    const password = passwordInp.current.value;
    if (isLoginForm) {
      // console.log("登录", username, password);
      logFn({
        identifier: username,
        password,
      }).then((res) => {
        // console.log(res);
        if (!res.error) {
          // 登录成功后需要向系统中添加一个标识,标记用户的登录状态
          // 登录状态(布尔值,token【jwt】,用户信息)
          // 跳转到首页
        }
      });
    } else {
      const email = emailInp.current.value;
      // console.log("注册", username, password, email);
      regFn({
        username,
        password,
        email,
      }).then((res) => {
        // console.log(res);
        if (!res.error) setIsLoginForm(true);
      });
    }
  };

  return (
    <div>
      <p style={{ color: "red" }}>{regError && regError.data.error.message}</p>
      <p style={{ color: "red" }}>{logError && logError.data.error.message}</p>
      <h2>{isLoginForm ? "登录" : "注册"}</h2>
      <form onSubmit={submitHandler}>
        <div>
          <input type="text" placeholder="用户名或邮箱" ref={usernameInp} />
        </div>
        {!isLoginForm && (
          <div>
            <input type="email" placeholder="邮箱" ref={emailInp} />
          </div>
        )}
        <div>
          <input type="text" placeholder="密码" ref={passwordInp} />
        </div>
        <div>
          <button>{isLoginForm ? "登录" : "注册"}</button>
          <div>
            <a
              href="#"
              onClick={(e) => {
                e.preventDefault();
                setIsLoginForm((pre) => !pre);
              }}
            >
              {isLoginForm ? "没有账号?点击注册" : "已有账号?点击登录"}
            </a>
          </div>
        </div>
      </form>
    </div>
  );
};

export default AuthForm;

2. src/store/api/authApi.js

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

export const authApi = createApi({
  reducerPath: "authApi",
  baseQuery: fetchBaseQuery({
    baseUrl: "http://localhost:1337/api/",
  }),
  endpoints: (build) => {
    return {
      register: build.mutation({
        query: (user) => {
          return {
            url: "/auth/local/register",
            method: "post",
            body: user,
          };
        },
      }),
      login: build.mutation({
        query: (user) => {
          return {
            url: "/auth/local",
            method: "post",
            body: user,
          };
        },
      }),
    };
  },
});

export const { useRegisterMutation, useLoginMutation } = authApi;

(五)使用 redux 存储登录状态

1. src/components/AuthForm.jsx

import React, { useState } from "react";
import { useRef } from "react";
import { useLoginMutation, useRegisterMutation } from "../store/api/authApi";
import { useDispatch } from "react-redux";
import { login } from "../store/reducer/authSlice";
import { useNavigate } from "react-router-dom";

const AuthForm = () => {
  const [isLoginForm, setIsLoginForm] = useState(true);

  // 引入登录注册的API
  const [regFn, { error: regError }] = useRegisterMutation();
  const [logFn, { error: logError }] = useLoginMutation();

  // 获取dispatch派发器
  const dispatch = useDispatch();

  // 获取Navigate
  const nav = useNavigate();

  const usernameInp = useRef();
  const passwordInp = useRef();
  const emailInp = useRef();

  const submitHandler = (e) => {
    e.preventDefault();
    const username = usernameInp.current.value;
    const password = passwordInp.current.value;
    if (isLoginForm) {
      // console.log("登录", username, password);
      logFn({
        identifier: username,
        password,
      }).then((res) => {
        // console.log(res);
        if (!res.error) {
          // 登录成功后需要向系统中添加一个标识,标记用户的登录状态
          // 登录状态(布尔值,token【jwt】,用户信息)
          dispatch(
            login({
              token: res.data.jwt,
              user: res.data.user,
            }),
          );
          // 跳转到首页
          nav("/", {
            replace: true,
          });
        }
      });
    } else {
      const email = emailInp.current.value;
      // console.log("注册", username, password, email);
      regFn({
        username,
        password,
        email,
      }).then((res) => {
        // console.log(res);
        if (!res.error) setIsLoginForm(true);
      });
    }
  };

  return (
    <div>
      <p style={{ color: "red" }}>{regError && regError.data.error.message}</p>
      <p style={{ color: "red" }}>{logError && logError.data.error.message}</p>
      <h2>{isLoginForm ? "登录" : "注册"}</h2>
      <form onSubmit={submitHandler}>
        <div>
          <input type="text" placeholder="用户名或邮箱" ref={usernameInp} />
        </div>
        {!isLoginForm && (
          <div>
            <input type="email" placeholder="邮箱" ref={emailInp} />
          </div>
        )}
        <div>
          <input type="text" placeholder="密码" ref={passwordInp} />
        </div>
        <div>
          <button>{isLoginForm ? "登录" : "注册"}</button>
          <div>
            <a
              href="#"
              onClick={(e) => {
                e.preventDefault();
                setIsLoginForm((pre) => !pre);
              }}
            >
              {isLoginForm ? "没有账号?点击注册" : "已有账号?点击登录"}
            </a>
          </div>
        </div>
      </form>
    </div>
  );
};

export default AuthForm;

2. src/components/MainMenu.jsx

import React from "react";
import { Link } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { logout } from "../store/reducer/authSlice";

const MainMenu = () => {
  const auth = useSelector((state) => state.auth);
  const dispatch = useDispatch();

  return (
    <div>
      <ul>
        <li>
          <Link to={"/"}>首页</Link>
        </li>
        {!auth.isLogged && (
          <li>
            <Link to={"/auth-form"}>登录/注册</Link>
          </li>
        )}
        {auth.isLogged && (
          <>
            <li>
              <Link to={"/profile"}>{auth.user.username}</Link>
            </li>
            <li>
              <Link to={"/"} onClick={() => dispatch(logout())}>
                登出
              </Link>
            </li>
          </>
        )}
      </ul>
    </div>
  );
};

export default MainMenu;

3. src/store/index.js

import { configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/dist/query/react";
import { authApi } from "./api/authApi";
import { authSlice } from "./reducer/authSlice";

const store = configureStore({
  reducer: {
    [authApi.reducerPath]: authApi.reducer,
    auth: authSlice.reducer,
  },
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(authApi.middleware),
});

setupListeners(store.dispatch);

export default store;

4. src/store/reducer/authSlice.js

import { createSlice } from "@reduxjs/toolkit";

export const authSlice = createSlice({
  name: "auth",
  initialState: {
    isLogged: false,
    token: null,
    user: null,
  },
  reducers: {
    login(state, action) {
      state.isLogged = true;
      state.token = action.payload.token;
      state.user = action.payload.user;
    },
    logout(state, action) {
      state.isLogged = false;
      state.token = null;
      state.user = null;
    },
  },
});

export const { login, logout } = authSlice.actions;

(六)解决登录前后页面跳转问题

1. src/index.js

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter as Router } from "react-router-dom";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <Router>
      <App />
    </Router>
  </Provider>,
);

2. src/App.js

import React from "react";
import { Route, Routes } from "react-router-dom";
import Layout from "./components/Layout";
import NeedAuth from "./components/NeedAuth";
import AuthFormPage from "./pages/AuthFormPage";
import HomePage from "./pages/HomePage";
import ProfilePage from "./pages/ProfilePage";

const App = () => {
  return (
    <Layout>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route
          path="/profile"
          element={
            <NeedAuth>
              <ProfilePage />
            </NeedAuth>
          }
        />
        <Route path="/auth-form" element={<AuthFormPage />} />
      </Routes>
    </Layout>
  );
};

export default App;

3. src/components/AuthForm.jsx

import React, { useState } from "react";
import { useRef } from "react";
import { useLoginMutation, useRegisterMutation } from "../store/api/authApi";
import { useDispatch } from "react-redux";
import { login } from "../store/reducer/authSlice";
import { useLocation, useNavigate } from "react-router-dom";

const AuthForm = () => {
  const [isLoginForm, setIsLoginForm] = useState(true);

  // 引入登录注册的API
  const [regFn, { error: regError }] = useRegisterMutation();
  const [logFn, { error: logError }] = useLoginMutation();

  // 获取dispatch派发器
  const dispatch = useDispatch();

  const navigate = useNavigate();
  const location = useLocation();
  const fromPage = location.state?.preLocation?.pathname || "/";

  const usernameInp = useRef();
  const passwordInp = useRef();
  const emailInp = useRef();

  const submitHandler = (e) => {
    e.preventDefault();
    const username = usernameInp.current.value;
    const password = passwordInp.current.value;
    if (isLoginForm) {
      // console.log("登录", username, password);
      logFn({
        identifier: username,
        password,
      }).then((res) => {
        // console.log(res);
        if (!res.error) {
          // 登录成功后需要向系统中添加一个标识,标记用户的登录状态
          // 登录状态(布尔值,token【jwt】,用户信息)
          dispatch(
            login({
              token: res.data.jwt,
              user: res.data.user,
            }),
          );
          // 跳转到之前访问过的页面
          navigate(fromPage, {
            replace: true,
          });
        }
      });
    } else {
      const email = emailInp.current.value;
      // console.log("注册", username, password, email);
      regFn({
        username,
        password,
        email,
      }).then((res) => {
        // console.log(res);
        if (!res.error) setIsLoginForm(true);
      });
    }
  };

  return (
    <div>
      <p style={{ color: "red" }}>{regError && regError.data.error.message}</p>
      <p style={{ color: "red" }}>{logError && logError.data.error.message}</p>
      <h2>{isLoginForm ? "登录" : "注册"}</h2>
      <form onSubmit={submitHandler}>
        <div>
          <input type="text" placeholder="用户名或邮箱" ref={usernameInp} />
        </div>
        {!isLoginForm && (
          <div>
            <input type="email" placeholder="邮箱" ref={emailInp} />
          </div>
        )}
        <div>
          <input type="text" placeholder="密码" ref={passwordInp} />
        </div>
        <div>
          <button>{isLoginForm ? "登录" : "注册"}</button>
          <div>
            <a
              href="#"
              onClick={(e) => {
                e.preventDefault();
                setIsLoginForm((pre) => !pre);
              }}
            >
              {isLoginForm ? "没有账号?点击注册" : "已有账号?点击登录"}
            </a>
          </div>
        </div>
      </form>
    </div>
  );
};

export default AuthForm;

4. src/components/NeedAuth.jsx

import React from "react";
import { useSelector } from "react-redux";
import { Navigate, useLocation } from "react-router-dom";

const NeedAuth = (props) => {
  const auth = useSelector((state) => state.auth);
  const location = useLocation();

  return auth.isLogged ? props.children : <Navigate state={{ preLocation: location }} to="/auth-form" replace />;
};

export default NeedAuth;

5. src/reducer/authSlice.js

import { createSlice } from "@reduxjs/toolkit";

export const authSlice = createSlice({
  name: "auth",
  initialState: {
    isLogged: false,
    token: null,
    user: null,
  },
  reducers: {
    login(state, action) {
      state.isLogged = true;
      state.token = action.payload.token;
      state.user = action.payload.user;
    },
    logout(state, action) {
      state.isLogged = false;
      state.token = null;
      state.user = null;
    },
  },
});

export const { login, logout } = authSlice.actions;

(七)使用本地存储实现登录持久化

1. src/reducer/authSlice.js

import { createSlice } from "@reduxjs/toolkit";

export const authSlice = createSlice({
  name: "auth",
  initialState: () => {
    const token = localStorage.getItem("token");
    if (!token)
      return {
        isLogged: false,
        token: null,
        user: null,
      };
    return {
      isLogged: true,
      token,
      user: JSON.parse(localStorage.getItem("user")),
    };
  },
  reducers: {
    login(state, action) {
      state.isLogged = true;
      state.token = action.payload.token;
      state.user = action.payload.user;
      // 数据同时存储到本地
      localStorage.setItem("token", state.token);
      localStorage.setItem("user", JSON.stringify(state.user));
    },
    logout(state, action) {
      state.isLogged = false;
      state.token = null;
      state.user = null;
      // 情况本地存储
      localStorage.removeItem("token");
      localStorage.removeItem("user");
    },
  },
});

export const { login, logout } = authSlice.actions;

(八)token 失效自动登出

1. src/App.js

import React from "react";
import { Route, Routes } from "react-router-dom";
import Layout from "./components/Layout";
import NeedAuth from "./components/NeedAuth";
import AuthFormPage from "./pages/AuthFormPage";
import HomePage from "./pages/HomePage";
import ProfilePage from "./pages/ProfilePage";
import useAutoLogout from "./hooks/useAutoLogout";

const App = () => {
  // token失效后自动登出
  useAutoLogout();

  return (
    <Layout>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route
          path="/profile"
          element={
            <NeedAuth>
              <ProfilePage />
            </NeedAuth>
          }
        />
        <Route path="/auth-form" element={<AuthFormPage />} />
      </Routes>
    </Layout>
  );
};

export default App;

2. src/reducer/authSlice.js

import { createSlice } from "@reduxjs/toolkit";

export const authSlice = createSlice({
  name: "auth",
  initialState: () => {
    const token = localStorage.getItem("token");
    if (!token)
      return {
        isLogged: false,
        token: null,
        user: null,
        expirationTime: 0, // token失效时间
      };
    return {
      isLogged: true,
      token,
      user: JSON.parse(localStorage.getItem("user")),
      expirationTime: +localStorage.getItem("expirationTime"),
    };
  },
  reducers: {
    login(state, action) {
      state.isLogged = true;
      state.token = action.payload.token;
      state.user = action.payload.user;
      // 获取当前时间戳
      const currentTime = Date.now();
      // 设置登录有效时间
      const timeout = 1000 * 60 * 60 * 24 * 7; // 1 week
      // const timeout = 1000 * 10; // 1 min
      // 设置失效日期
      state.expirationTime = currentTime + timeout;
      // 数据同时存储到本地
      localStorage.setItem("token", state.token);
      localStorage.setItem("user", JSON.stringify(state.user));
      localStorage.setItem("expirationTime", state.expirationTime + "");
    },
    logout(state, action) {
      state.isLogged = false;
      state.token = null;
      state.user = null;
      // 情况本地存储
      localStorage.removeItem("token");
      localStorage.removeItem("user");
      localStorage.removeItem("expirationTime");
    },
  },
});

export const { login, logout } = authSlice.actions;

3. src/hooks/useAutoLogout.js

import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { logout } from "../store/reducer/authSlice";

const useAutoLogout = () => {
  const auth = useSelector((state) => state.auth);
  const dispatch = useDispatch();
  useEffect(() => {
    const timeout = auth.expirationTime - Date.now();
    if (timeout < 1000 * 60) {
      dispatch(logout());
      return;
    }
    const timer = setTimeout(() => {
      dispatch(logout());
    }, timeout);
    return () => {
      clearTimeout(timer);
    };
  }, [auth, dispatch]);
};

export default useAutoLogout;

(九)引入学生列表

1. src/App.js

import React from "react";
import { Route, Routes } from "react-router-dom";
import useAutoLogout from "./hooks/useAutoLogout";
import Layout from "./components/Layout";
import NeedAuth from "./components/NeedAuth";
import AuthFormPage from "./pages/AuthFormPage";
import HomePage from "./pages/HomePage";
import ProfilePage from "./pages/ProfilePage";
import StudentPage from "./pages/StudentPage";

const App = () => {
  // token失效后自动登出
  useAutoLogout();

  return (
    <Layout>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route
          path="/profile"
          element={
            <NeedAuth>
              <ProfilePage />
            </NeedAuth>
          }
        />
        <Route path="/auth-form" element={<AuthFormPage />} />
        <Route
          path="/student"
          element={
            <NeedAuth>
              <StudentPage />
            </NeedAuth>
          }
        />
      </Routes>
    </Layout>
  );
};

export default App;

2. src/store/index.js

import { configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/dist/query/react";
import { authApi } from "./api/authApi";
import { studentApi } from "./api/studentApi";
import { authSlice } from "./reducer/authSlice";

const store = configureStore({
  reducer: {
    [authApi.reducerPath]: authApi.reducer,
    auth: authSlice.reducer,
    [studentApi.reducerPath]: studentApi.reducer,
  },
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(authApi.middleware, studentApi.middleware),
});

setupListeners(store.dispatch);

export default store;

3. src/store/api/studentApi.js

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

/*
  https://www.lilichao.com/index.php/2022/05/27/rtk-query/
  createApi() 创建Api对象
    RTK Query的所有功能都需要通过该对象来进行
    需要一个配置对象作为参数
*/
export const studentApi = createApi({
  reducerPath: "studentApi", // Api的标识,不能喝其他Api或reducer重复
  baseQuery: fetchBaseQuery({
    baseUrl: "http://localhost:1337/api/",
  }), // 指定查询的基础信息,发送请求使用的工具
  tagTypes: ["student"], // 指定Api中的标签类型
  endpoints: (build) => {
    // build是请求的构建器,通过build来设置请求的相关信息
    return {
      getStudents: build.query({
        query: () => {
          return "students";
        }, // 指定请求子路径,和baseUrl拼在一起
        transformResponse: (baseQueryReturnValue) => {
          return baseQueryReturnValue.data;
        }, // 转换响应数据的格式
        // providesTags: ["student"], // 为当前方法打标签,当标签失效时会重新执行当前方法
        providesTags: [
          {
            type: "student",
            id: "LIST",
          },
        ],
      }), // 查询
      getStudentsById: build.query({
        query: (id) => {
          return `students/${id}`;
        },
        transformResponse: (baseQueryReturnValue) => {
          return baseQueryReturnValue.data;
        },
        keepUnusedDataFor: 60, //设置数据缓存的时间,单位秒,默认60
        providesTags: (result, error, id) => [
          {
            type: "student",
            id,
          },
        ], // 只有当前id变化时才打标签
      }),
      deleteStudent: build.mutation({
        query: (id) => {
          // 如果发送的不是get请求,需要返回一个对象来设置请求信息
          return {
            url: `students/${id}`,
            method: "delete",
          };
        },
        invalidatesTags: ["student"], // 令标签失效
      }), // 修改
      addStudents: build.mutation({
        query: (stu) => {
          return {
            url: "students",
            method: "post",
            body: {
              data: stu,
            },
          };
        },
        // invalidatesTags: ["student"], // 令标签失效【全部该标签都失效】
        invalidatesTags: [
          {
            type: "student",
            id: "LIST",
          },
        ], // 只有id为LIST的student标签失效,重新执行带有id为LIST的student标签的方法
      }),
      updateStudent: build.mutation({
        query: (stu) => {
          return {
            url: `students/${stu.id}`,
            method: "put",
            body: {
              data: stu.attributes,
            },
          };
        },
        invalidatesTags: (result, error, stu) => [
          {
            type: "student",
            id: stu.id,
          },
          {
            type: "student",
            id: "LIST",
          },
        ], // 令标签失效
      }),
    };
  }, // 指定Api中的各种功能,是一个方法,需要一个对象作为返回值
});

/*
  Api对象创建后,会根据各种方法自动生成对应的钩子函数
  钩子函数可用于向服务器发送请求
  命名规则:getStudents --> useGetStudentsQuery()
*/
export const { useGetStudentsQuery, useGetStudentsByIdQuery, useDeleteStudentMutation, useAddStudentsMutation, useUpdateStudentMutation } =
  studentApi;

4. src/pages/StudentPage.js

import React from "react";
import StudentList from "../components/Student/StudentList";

const StudentPage = () => {
  return (
    <div>
      <StudentList />
    </div>
  );
};

export default StudentPage;

5. src/components/MainMenu.js

import React from "react";
import { Link } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { logout } from "../store/reducer/authSlice";

const MainMenu = () => {
  const auth = useSelector((state) => state.auth);
  const dispatch = useDispatch();

  return (
    <div>
      <ul>
        <li>
          <Link to={"/"}>首页</Link>
        </li>
        {!auth.isLogged && (
          <li>
            <Link to={"/auth-form"}>登录/注册</Link>
          </li>
        )}
        {auth.isLogged && (
          <>
            <li>
              <Link to={"/profile"}>{auth.user.username}</Link>
            </li>
            <li>
              <Link to={"/student"}>学生</Link>
            </li>
            <li>
              <Link to={"/"} onClick={() => dispatch(logout())}>
                登出
              </Link>
            </li>
          </>
        )}
      </ul>
    </div>
  );
};

export default MainMenu;

6. src/components/Student/Student.jsx

import React, { useState } from "react";
import { useDeleteStudentMutation } from "../../store/api/studentApi";
import StudentForm from "./StudentForm";

const Student = ({
  stu: {
    id,
    attributes: { name, age, gender, address },
  },
  stu,
}) => {
  const [editing, setEditing] = useState(false);

  const [delStudent, { isSuccess }] = useDeleteStudentMutation();
  const deleteStudent = () => {
    delStudent(id);
  };

  const cancelEdit = () => {
    setEditing(false);
  };

  return (
    <>
      {!editing && !isSuccess && (
        <tr>
          <td>{name}</td>
          <td>{gender}</td>
          <td>{age}</td>
          <td>{address}</td>
          <td>
            <button onClick={deleteStudent}>删除</button>
            <button onClick={() => setEditing(true)}>修改</button>
          </td>
        </tr>
      )}

      {isSuccess && (
        <tr>
          <td colSpan={5}>数据已删除!</td>
        </tr>
      )}

      {editing && <StudentForm stu={stu} stuId={id} onCancelEdit={cancelEdit} />}
    </>
  );
};

export default Student;

7. src/components/Student/StudentForm.jsx

import React, { useEffect, useState } from "react";
import { useAddStudentsMutation, useGetStudentsByIdQuery, useUpdateStudentMutation } from "../../store/api/studentApi";
import "./StudentForm.css";

const StudentForm = (props) => {
  // StudentForm一加载,自动获取最新的学生数据
  const { data: students, isSuccess: isGetSuccess } = useGetStudentsByIdQuery(props.stuId, {
    skip: !props.stuId,
    refetchOnMountOrArgChange: true,
  });
  useEffect(() => {
    if (isGetSuccess) setInputData(students.attributes);
  }, [isGetSuccess]);

  const [inputData, setInputData] = useState({
    name: "",
    gender: "男",
    age: 0,
    address: "",
  });
  const { name, gender, age, address } = inputData;
  const nameChange = (e) => {
    setInputData((pre) => ({ ...pre, name: e.target.value }));
  };
  const genderChange = (e) => {
    setInputData((pre) => ({ ...pre, gender: e.target.value }));
  };
  const ageChange = (e) => {
    setInputData((pre) => ({ ...pre, age: +e.target.value }));
  };
  const addressChange = (e) => {
    setInputData((pre) => ({ ...pre, address: e.target.value }));
  };

  const [addStudent, { isSuccess: isAddSuccess }] = useAddStudentsMutation();
  const onSubmitAdd = () => {
    addStudent(inputData);
    setInputData({
      name: "",
      gender: "男",
      age: 0,
      address: "",
    });
  };

  const [editStudent, { isSuccess: isEditSuccess }] = useUpdateStudentMutation();
  const onSubmitEdit = () => {
    editStudent({
      id: props.stuId,
      attributes: inputData,
    });
    props.onCancelEdit();
  };

  return (
    <>
      <tr className="student-form">
        <td>
          <input type="text" value={name} onChange={nameChange} />
        </td>
        <td>
          <select value={gender} onChange={genderChange}>
            <option value="男"></option>
            <option value="女"></option>
          </select>
        </td>
        <td>
          <input type="text" value={age} onChange={ageChange} />
        </td>
        <td>
          <input type="text" value={address} onChange={addressChange} />
        </td>
        <td>
          {props.stu && (
            <>
              <button onClick={() => props.onCancelEdit()}>取消</button>
              <button onClick={onSubmitEdit}>确认</button>
            </>
          )}
          {!props.stu && <button onClick={onSubmitAdd}>添加</button>}
        </td>
      </tr>
    </>
  );
};

export default StudentForm;

8. src/components/Student/StudentForm.css

.student-form input {
  width: 80px;
}

9. src/components/Student/StudentList.jsx

import React from "react";
import Student from "./Student";
import StudentForm from "./StudentForm";
import { useGetStudentsQuery } from "../../store/api/studentApi";
import "./StudentList.css";

const StudentList = () => {
  // 调用Api钩子函数查询数据
  const { data: stu, isSuccess, isLoading } = useGetStudentsQuery();
  return (
    <div className="box">
      <table>
        <caption>
          <h2>学生列表</h2>
        </caption>
        <thead>
          <tr>
            <th>姓名</th>
            <th>性别</th>
            <th>年龄</th>
            <th>地址</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          {isLoading && (
            <tr>
              <td>数据加载中...</td>
            </tr>
          )}
          {isSuccess &&
            stu.map((stu) => {
              return <Student key={stu.id} stu={stu} />;
            })}
        </tbody>
        <tfoot>
          <StudentForm />
        </tfoot>
      </table>
    </div>
  );
};

export default StudentList;

10. src/components/Student/StudentList.css

.box {
  margin: 10px auto;
  background-color: lightcoral;
  width: 90%;
  padding: 0 20px 20px;
}
table {
  border-collapse: collapse;
  margin: 0 auto;
  width: 100%;
}
tr {
  border: 1px solid #eee;
}
td {
  color: #fff;
  border: 1px solid #eee;
  text-align: center;
  width: 200px;
}
button {
  cursor: pointer;
}

(十)添加服务器验证 token

1. src/store/api/studentApi.js

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

export const studentApi = createApi({
  reducerPath: "studentApi",
  baseQuery: fetchBaseQuery({
    baseUrl: "http://localhost:1337/api/",
    prepareHeaders: (headers, { getState }) => {
      // 获取用户token
      const token = getState().auth.token;
      if (token) headers.set("Authorization", `Bearer ${token}`);
      return headers;
    }, // 用于统一设置请求头
  }),
  tagTypes: ["student"],
  endpoints: (build) => {
    return {
      getStudents: build.query({
        query: () => {
          return "students";
        },
        transformResponse: (baseQueryReturnValue) => {
          return baseQueryReturnValue.data;
        },
        providesTags: [
          {
            type: "student",
            id: "LIST",
          },
        ],
      }),
      getStudentsById: build.query({
        query: (id) => {
          return `students/${id}`;
        },
        transformResponse: (baseQueryReturnValue) => {
          return baseQueryReturnValue.data;
        },
        keepUnusedDataFor: 60,
        providesTags: (result, error, id) => [
          {
            type: "student",
            id,
          },
        ],
      }),
      deleteStudent: build.mutation({
        query: (id) => {
          return {
            url: `students/${id}`,
            method: "delete",
          };
        },
        invalidatesTags: ["student"],
      }),
      addStudents: build.mutation({
        query: (stu) => {
          return {
            url: "students",
            method: "post",
            body: {
              data: stu,
            },
          };
        },
        invalidatesTags: [
          {
            type: "student",
            id: "LIST",
          },
        ],
      }),
      updateStudent: build.mutation({
        query: (stu) => {
          return {
            url: `students/${stu.id}`,
            method: "put",
            body: {
              data: stu.attributes,
            },
          };
        },
        invalidatesTags: (result, error, stu) => [
          {
            type: "student",
            id: stu.id,
          },
          {
            type: "student",
            id: "LIST",
          },
        ],
      }),
    };
  },
});

export const { useGetStudentsQuery, useGetStudentsByIdQuery, useDeleteStudentMutation, useAddStudentsMutation, useUpdateStudentMutation } =
  studentApi;
上次编辑于: