react-router-dom6实现私密路由

1,319 阅读5分钟

场景

react项目里面有些时候部分页面需要登陆或者部分权限才能访问 这个时候需要私有路由

在创建私有路由之前,需要一种方法来确定用户是否被认证。这里说的是React Router保护路由的方法,而不是关于认证,所以使用一个假的useAuth Hook来确定我们用户的认证 "状态"。

创建文件APP.jsx, hooks/useAuth.jsx, Login.jsx, Nav.jsx

/pricing和/login路径是公开的,/dashboard和/settings路线将是私有的。现在,先是像普通路由一样渲染它们。

创建Login.jsx和Nav.jsx,vscode里快捷键rafce快速生成登录和退出

创建App.jsx,渲染路由

/**
 * @file App.jsx
 * @description 创建几个组件用于路由使用
 * 主页、定价、仪表板、设置和登录
 */
import { Routes, Route } from 'react-router-dom';
import Nav from './Nav';
import Login from './Login';

const Home = () => <h1>Home (Public)</h1>;
const Pricing = () => <h1>Pricing (Public)</h1>;

const Dashboard = () => <h1>Dashboard (Private)</h1>;
const Settings = () => <h1>Settings (Private)</h1>;


/**
 * @description /, /pricing, /login routes是共有
 * /dashboard, /settings route是私有
 */
export default function App() {
  return (
    <div>
      <Nav />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/pricing" element={<Pricing />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/login" element={<Login />} />
      </Routes>
    </div>
  );
}

在创建hooks/useAuth模拟确定我们用户的认证 "状态"。

有了这个useAuth就知道用户是否被授权、登录或注销 useAuth Hook可以有很多不同的工作方式。 也许它向API端点发出HTTP请求,以验证一个cookie。或者它可以解码存储在浏览器本地存储器中的JWT令牌。或者你可以使用第三方认证解决方案 总之:在任何情况下,目标都是一样的:找出用户当前是否已被认证。

import React from "react";
import { useContext, useState } from 'react'
const authContext = React.createContext()

function useAuth() {
  const [authed,setAuthed] = useState() //状态

   return {
    //认证状态
     authed,
     //登录
     login() {
       return new Promise ((res) => {
         setAuthed(true)
         res()
       })
     },
     //退出
     logout() {
       return new Promise((res)=>{
         setAuthed(false)
         res()
       })
     }
   }
}
//用context存储auth状态来进行传递
export function AuthProvider( {children} ) {
  const auth = useAuth()
  return <authContext.Provider value={auth}>{children}</authContext.Provider>
}

export default function AuthConsumer() {
  const { auth } = useContext(authContext);
  return auth
}

接下来进入Login.jsx完成登录授权

/**
 * @todo 现在开始做一些授权工作 首先完善登录组件。这个组件的目标自然是允许用户登录
 */
import React from 'react'
import { useNavigate } from "react-router-dom";
import useAuth  from './hooks/useAuth';

/**
 *当用户点击按钮时,调用login(从useAuth Hook获得),然后一旦他们登录,使用navigate,跳转到/dashboard。
 */
const Login = () => {
  const navigate = useNavigate();
  const { login } = useAuth();
  /**
  *@function
  *@description 登陆的同时authed状态为true并跳转/dashboard
  */
  const handleLogin = () => {
    login().then(() => {
      navigate("/dashboard");
    });
  };
  return (
    <div>
      <h1>Login</h1>
      <button onClick={handleLogin}>Log in</button>
    </div>
  )
}
export default Login

接下来Nav.jsx里面添加注销的功能

//接下来,让我们添加注销的功能。同样,我们已经有了来自useAuth Hook的注销方法,所以这也应该是简单地添加一些用户界面。所有的改变都将发生在我们的导航组件上。
import { useNavigate } from "react-router-dom";
import useAuth from "./useAuth";

const Nav = () => {
  const { authed, logout } = useAuth();
  const navigate = useNavigate();
  //点击退出authed为false
  const handleLogout = () => {
    logout();
    navigate("/");
  };
//authed 为true则有注销登录按钮
  return (
    <nav>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/pricing">Pricing</Link>
        </li>
      </ul>
      {authed && <button onClick={handleLogout}>Logout</button>}
    </nav>
  );
}
export default Nav

进入App.jsx中让/dashboard和/settings成为私有

在深入实施之前,先提出最终的API可能是什么样子的。对于每条我们希望是私有的路由,不给它的Routes element我们希望它直接呈现的组件,而是把它包在一个新的组件中,言简意赅的设置成RequireAuth。

最终Api的样子
/**
 * @description /, /pricing, /login routes是共有
 * /dashboard, /settings route是私有
 */
...
      <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/pricing" element={<Pricing />} />
          <Route
            path="/dashboard"
            element={
              <RequireAuth>
                <Dashboard />
              </RequireAuth>
            }
          />
          <Route
            path="/settings"
            element={
              <RequireAuth>
                <Settings />
              </RequireAuth>
            }
          />
          <Route path="/login" element={<Login />} />
      </Routes>
...

可以想到RequireAuth目的是让/dashboard和/setting成为私密路由,所以它的逻辑主要是实现两件事情。

首先,它的唯一api是一个children元素。

第二,如果用户通过了认证,它应该呈现那个children元素,如果没有,它应该把用户重定向到他们可以认证的页面

使用我们先前的useAuth Hook来建立<RequireAuth>

/**
 * 用它包裹想要验证的路由,此时当一个没有经过认证的用户试图进入/dashboard或/settings时
 * 他们会被重定向到/login。然后一旦他们登录,我们就把他们重定向到/dashboard。
 *reactrouterdom6中一个<Navigate>元素在渲染时改变当前位置,它是一个围绕useNavigate的组件包
 *装,并接受所有与props相同的参数。他替代了5版本中Switch中的<Redirect>
 */
const RequireAuth = ({ children }) => {
  const { authed } = useAuth();
  return authed === true ? children : <Navigate to="/login" replace />;
}

基本的功能就实现了

补充优化<RequireAuth/>

上面的代码总是将用户重定向到/dashboard,开发中不应该这样子,而应该将他们重定向到他们最初试图访问的路线。例如,如果他们试图访问/settings但没有登录,在我们重定向他们并且他们登录后,我们应该把他们带回/settings,而不是dashboard。 于是要用到useLocation这个钩子返回当前的位置对象,同时 reactrouterdom6中<Navigate>组件在渲染时改变当前位置,有两个参数replace和state

const RequireAuth = ({ children })=> {
  const { authed } = useAuth();
  const location = useLocation();

  return authed === true ? (
    children
  ) : (
  //使用location.state来保留之前的位置,这样你就可以在用户认证后把他们送到那里。
  //replace: true 来替换历史栈中的/login路由,这样用户在登录后点击返回按钮时就不会返回到登录页面。
    <Navigate to="/login" replace state={{ path: location.pathname }} />
  )
 }

最后,完善Login组件

目的:在用户认证后,如果原路径存在,我们会将用户重定向到原路径,如果不存在,我们会将他们带到/dashboard。 可以使用React Router的useLocation Hook来获得对location.state的访问,其中会有我们的path属性。

import { useNavigate,useLocation  } from "react-router-dom";
import useAuth  from './hooks/useAuth';

const Login = () => {
  const navigate = useNavigate();
  const { login } = useAuth();
  const { state } = useLocation();
  /**
  *@function
  *@description 登陆跳转
  */
  const handleLogin = () => {
    login().then(() => {
    //如果原路径存在,会将用户重定向到原路径,如果不存在,我们会将他们带到/dashboard
      navigate(state?.path || "/dashboard"); 
    });
  };
  return (
    <div>
      <h1>Login</h1>
      <button onClick={handleLogin}>Log in</button>
    </div>
  )
}

export default Login

完毕