最近学习了 React-router-v6 相关知识,由于 React 框架并没有像 Vue 那样提供路由守卫,如果需要用到路由守卫功能,就需要自己实现一个,为了不让自己忘记也方便以后复习,所以就有了此文。
另一个原因就是上周更新的文章燃烧了亿点点心血,打算这周就简单的"水"一篇文章吧!
环境搭建
首先是项目环境搭建,需要已经安装node
和vite
以及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
刷新之后,仍然需要重新登录,至于为什么?如果不明白为什么,那就自己好好想想。
参考资料如下: