【React 01】使用 React-router-v6 实现路由守卫

5,286 阅读4分钟

最近学习了 React-router-v6 相关知识,由于 React 框架并没有像 Vue 那样提供路由守卫,如果需要用到路由守卫功能,就需要自己实现一个,为了不让自己忘记也方便以后复习,所以就有了此文。

另一个原因就是上周更新的文章燃烧了亿点点心血,打算这周就简单的"水"一篇文章吧!

环境搭建

首先是项目环境搭建,需要已经安装nodevite 以及yarn此部分内容就省略,毕竟这不是主角!项目创建步骤如下

yarn create vite
​
# 输入project-name

cd project-name
​
yarn
​
yarn dev
# 安装路由依赖
yarn add react-router-dom

删除App.jsx中的内容,删除app.css 文件,不要让这些无关紧要的东西印象我们的视线!

function App() {
  return (
    <>
    </>
  )
}
export default App

创建view文件夹,并在该文件夹下添加三个基础的页面

export default function AnotherPage() {
    return <h3>AnotherPage</h3>;
}
export default function ProtectedPage() {
    return <h3>Protected</h3>;
}
export default function PublicPage() {
    return <h3>Public</h3>;
}

创建 Layout.jsx,代码如下

import { Link, Outlet } from "react-router-dom";
​
export default function Layout() {
    return (
        <div>
            <ul>
                <li>
                    <Link to="/">Public Page</Link>
                </li>
                <li>
                    <Link to="/protected">Protected Page</Link>
                </li>
                <li>
                    <Link to="/another">Protected2 Page</Link>
                </li>
            </ul>
            <Outlet />
        </div>
    );
}

Link 标签是 react-router-dom 中的元素,可以把他当作原始的 a 标签看待,Outlet 是V6版本中新增的一个用来渲染子组件的标签,即渲染<ProtectedPage /> or <AnotherPage /> 当浏览器地址为/protected or /another 时,可以看作vue中 <RouterView /> 标签 。

App.jsx 进行如下配置

export default function App() {
    return (
        <Routes >
            <Route element={<Layout />}>
                <Route path="/" element={<PublicPage />} />
                <Route path="/protected" element={ <ProtectedPage /> } />
                <Route path="/another" element={ <AnotherPage />} />
            </Route>
        </Routes>
    );
}

Routes 标签是 react-router-v6 版本新增加的,替换了 v5 中的 Switch标签。

对应的 main.jsx 中,代码如下

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import { BrowserRouter } from "react-router-dom";
​
ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
)

此时可通过点击这三个 Link 标签进行页面切换,且在 Url 输入路由也可进行页面切换,但往往这种需求是不符合实际项目情况,毕竟总有些页面需要登录之后才能看见,不会让我们直接输入url 就可以访问!但是 React 并没有像 Vue 那样提供前置路由守卫,因此需要我们自己实现一个路由守卫功能。

路由守卫

实现路由守卫这个功能我们首先需要使用createContext创建一个用于传递值的 context ,具体代码如下:

import { createContext, useState, useContext } from "react";
// create
let AuthContext = createContext(null);
// 供其他组件读取我们创建的AuthContext中的value值
export function useAuth() {
    return useContext(AuthContext);
}
​
export function AuthProvider({ children }) {
    let [user, setUser] = useState(null);
    let signin = (newUser, callback) => {
        console.log("模拟登录操作")
        setTimeout(()=>{
            setUser(newUser)
            callback()
        },500)
    };
    let signout = (callback) => {
        setTimeout(()=>{
            console.log("模拟登出操作")
            setUser(null)
            callback()
        },500)
    };
    // 此value就可以被其他子组件读取
    let value = { user, signin, signout };
    //  provider context
    return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

AuthStatus.jsx 中通过我们自定义的useAuth去读取值

import { useAuth } from "./AuthProvider";
import { Navigate, useLocation, useNavigate } from "react-router";
​
// 判断是否登录 
export function AuthStatus() {
    let auth = useAuth();
    let navigate = useNavigate();
    // user 是否为null
    if (!auth.user) {
        return <p>You are not logged in.</p>;
    }
    return (
        <p>
            Welcome {auth.user}!{" "}
            <button
                onClick={() => {
                    auth.signout(() => navigate("/"));
                }}
            >
                Sign out
            </button>
        </p>
    );
}
// 通过 RequireAuth 去包裹需要被保护的视图组件
export function RequireAuth({ children }) {
    let auth = useAuth();
    let location = useLocation();
    // 如果没有登录 重定向至login页面
    if (!auth.user) {
        return <Navigate to="/login" state={{ from: location }} replace />;
    }
    return children;
}

新建LoginPage组件,具体代码如下所示:

export function LoginPage() {
    let navigate = useNavigate();
    let location = useLocation();
    let auth = useAuth();
    let from = location.state?.from?.pathname || "/";
    function handleSubmit(event) {
        event.preventDefault();
        let formData = new FormData(event.currentTarget);
        let username = formData.get("username")
        // 登录
        auth.signin(username, () => {
            console.log("登录")
            navigate(from, { replace: true });
        });
    }
    return (
        <div>
            <p>plz login</p>
            <form onSubmit={handleSubmit}>
                <label>
                    Username: <input name="username" type="text" />
                </label>{" "}
                <button type="submit">Login</button>
            </form>
        </div>
    );
}

修改 app.jsx

export default function App() {

    return (
        <AuthProvider>
            <Routes >
                <Route element={<Layout />}>
                    <Route path="/" element={<PublicPage />} />
                    <Route path="/login" element={<LoginPage />} />
                    <Route path="/protected" element={<RequireAuth> <ProtectedPage /> </RequireAuth>} />
                    <Route path="/another" element={<RequireAuth> <AnotherPage /> </RequireAuth>} />
                </Route>
            </Routes>
        </AuthProvider>
    );
}

可以看到,但凡是需要鉴权的地方,我们都通过使用useAuth去获取我们的context,通过去读取user值,去判断是否可以访问该路由对应的组件!

写法优化

上面的写法在被保护的路由很少的情况下,没什么问题,但是当被保护的路由很多时,就需要在App.jsx中写大量的路由,况且在v6版本中,并不推荐这种方式进行路由配置,那么有没有其他写法呢?

答案如下:

export const router = createBrowserRouter(
    createRoutesFromElements(
      <>
        {/*不需要保护的其他路由*/} 
        <Route path="/another" element={ <AnotherPage /> } />
        <Route element={<AuthProvider><Layout /></AuthProvider>}>
            <Route path="/" element={<PublicPage />} />
            <Route path="/login" element={<LoginPage />} />
            {/*需要保护的路由*/} 
            <Route path="/protected" element={<RequireAuth> <ProtectedPage /> </RequireAuth>}/>
        </Route>
      </>
    )
);
// main.jsx
ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
)

需要注意的是:上面的模拟用户登录登出这一部分,需要根据实际的项目情况进行更改,这里的方法并不能够直接投入使用,因为这里即使登录使用ctrl + r 刷新之后,仍然需要重新登录,至于为什么?如果不明白为什么,那就自己好好想想。

参考资料如下: