React-Router是React生态里面很重要的一环,现在React的单页应用的路由基本都是前端自己管理的,而不像以前是后端路由,React管理路由的库常用的就是React-Router。React-Router V6 相对 V5 版本存在很多改动和升级,功能更为强大。关于React-Router V6的API,官方文档已经写得很好了,这里就不做过多介绍。这里就用一个常见的前端鉴权模型场景来看看React-Router V6是怎么用的吧。
开始
通过运行以下命令打开终端并创建一个新的React项目(项目引入typescript):
> npx create-react-app ReactRouterAuthDemo --template typescript
> cd ReactRouterAuthDemo
接下来,将React Router作为依赖项安装在React应用程序中:
npm install react-router-dom
一旦安装了React-Router依赖项,我们就需要编辑src/index.tsx文件。
React-Router V6 相对 V5 多了几种定义路由的方式,这里我们不采用Router Components的方式,采用新的数据路由器对象方式。
从react-router-dom导入 createBrowserRouter,然后将路由对象数组传递给 createBrowserRouter,如下所示:
// router.tsx
import { createBrowserRouter } from 'react-router-dom'
import Home from '../pages/home/Home'
const router = createBrowserRouter([
{
path: '/',
element: <Home />,
},
])
export default router
同时在 App.tsx 中使用 RouterProvider (RouterProvider 所有数据路由器对象都会传递到此组件,以呈现您的应用程序并启用其余的数据API)。
// App.tsx
import { RouterProvider } from 'react-router-dom'
import router from './routes/routes'
function App() {
return <RouterProvider router={router} />
}
export default App
现在,我们都可以在应用程序的任何地方使用React-Router组件和挂钩。
基本路由
现在根据前端路由鉴权demo的需要,我们使用一些真实路由先简单替换 router.tsx 和 App.tsx 文件中的样板代码。
// router.tsx
import { lazy } from 'react'
import { createBrowserRouter } from 'react-router-dom'
import Layout from '../layout'
import Home from '../pages/home/Home'
// lazy-loading 需要结合 React.Suspense
const Backend = lazy(() => import('../pages/backend/Backend'))
const Admin = lazy(() => import('../pages/admin/Admin'))
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: <Home />,
},
{
path: 'backend',
element: <Backend />,
},
{
path: 'admin',
element: <Admin />,
},
],
},
])
export default router
App.tsx 引入 React.Suspense,配合 React.lazy() 实现 lazy-loading
// App.tsx
import { Suspense } from 'react'
import { RouterProvider } from 'react-router-dom'
import router from './routes/routes'
function App() {
return (
<Suspense fallback={<div>loading...</div>}>
<RouterProvider router={router} />
</Suspense>
)
}
export default App
// Layout.tsx
import { Outlet } from 'react-router-dom'
export default function Layout() {
return (
<div>
<Outlet />
</div>
)
}
// Home.tsx
import { Link } from 'react-router-dom'
export default function Home() {
return (
<ul>
<li>
<Link to="/login">登录</Link>
</li>
<li>
<Link to="/backend">后台</Link>
</li>
<li>
<Link to="/admin">管理员</Link>
</li>
</ul>
)
}
// Backend.tsx
import { Link } from 'react-router-dom'
export default function Backend() {
return (
<>
<h1>后台页面</h1>
<ul>
<li>
<Link to="/">回到首页</Link>
</li>
</ul>
</>
)
}
// Admin.tsx
import { Link } from 'react-router-dom'
export default function Admin() {
return (
<>
<h1>管理员页面</h1>
<ul>
<li>
<Link to="/">回到首页</Link>
</li>
</ul>
</>
)
}
接下来,我们将运行此命令来启动应用程序:
> npm start
现在基本设置已经完成,让我们看看如何创建受保护的路由,使未经身份验证的用户无法访问我们应用程序中的某些内容。
创建封装鉴权相关context,hooks
在创建受保护的路由(也称为专用路由)之前,让我们创建一个自定义hook,该hook将使用Context API和useContext挂钩处理经过身份验证的用户的状态:
模拟登录的相关代码
// auth.ts
// 模拟登录的相关代码
export const fakeAuthProvider = {
isAuthenticated: false,
signin(callback: VoidFunction) {
fakeAuthProvider.isAuthenticated = true
setTimeout(callback, 100) // fake async
},
signout(callback: VoidFunction) {
fakeAuthProvider.isAuthenticated = false
setTimeout(callback, 100)
},
}
创建权限 context
// AuthContext.ts
// 创建权限 context
import { createContext } from 'react'
interface AuthContextType {
user: any
role?: string // '' | 'public' | 'admin' | 'user'
signin: (user: string, callback: VoidFunction) => void
signout: (callback: VoidFunction) => void
}
const AuthContext = createContext<AuthContextType>(null!)
export default AuthContext
传递权限信息 Provider
为了即使在页面刷新时也能保持用户的状态,使用sessionStorage做临时持久化存储,我们将使用 useSessionStorage hook,它将同步浏览器本地存储中的状态值:
// useSessionStorage.ts
import { useState } from 'react'
export const useSessionStorage = (keyName: any, defaultValue: any) => {
const [storedValue, setStoredValue] = useState(() => {
try {
const value = window.sessionStorage.getItem(keyName)
if (value) {
return JSON.parse(value)
} else {
window.sessionStorage.setItem(keyName, JSON.stringify(defaultValue))
return defaultValue
}
} catch (err) {
return defaultValue
}
})
const setValue = (newValue: any) => {
try {
window.sessionStorage.setItem(keyName, JSON.stringify(newValue))
} catch (err) {}
setStoredValue(newValue)
}
return [storedValue, setValue]
}
AuthProvider.tsx
// AuthProvider.tsx
import { useMemo } from 'react'
import { useLocalStorage } from '../hooks/useLocalStorage'
import { fakeAuthProvider } from './auth'
import AuthContext from './AuthContext'
export default function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useSessionStorage('user', null)
const [role, setRole] = useSessionStorage('role', null)
const signin = (newUser: string, callback: VoidFunction) => {
return fakeAuthProvider.signin(() => {
setUser(newUser)
// 模拟管理员登录
newUser === 'admin' ? setRole('admin') : setRole('user')
callback()
})
}
const signout = (callback: VoidFunction) => {
return fakeAuthProvider.signout(() => {
setUser(null)
setRole(null)
callback()
})
}
const value = useMemo(() => ({ user, role, signin, signout }), [role, user])
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
获取权限信息的hooks
useAuth.ts
import { useContext } from "react";
import AuthContext from "../auth/AuthContext";
export default function useAuth() {
return useContext(AuthContext)
}
使用 useAuth 钩子,我们将公开用户的状态以及用于用户登录和注销的两种方法。当用户注销时,我们可以使用React-Router 的 useNavigate 钩子将他们重定向到主页。
路由鉴权
创建鉴权路由
<AuthRoute /> 组件将简单地从 useAuth 钩子检查当前用户状态,然后在用户未通过身份验证的情况下重定向到主屏幕:
AuthRoute.tsx
import { Navigate, Outlet, useLocation } from 'react-router-dom'
import useAuth from '../hooks/useAuth'
export default function AuthRoute(props: any) {
const auth = useAuth()
const location = useLocation()
if (!auth.user) {
return <Navigate to="/login" state={{ from: location }} replace />
}
return props.children ? props.children : <Outlet />
}
我们在创建一个登录状态的组件,方便我们观察登录以及执行退出登录操作
AuthStatus.tsx
import { useNavigate } from 'react-router-dom'
import useAuth from '../hooks/useAuth'
export default function AuthStatus() {
const auth = useAuth()
const navigate = useNavigate()
if (!auth.user) {
return <h3>You are not logged in.</h3>
}
return (
<p>
Welcome {auth.user}!{' '}
<button
onClick={() => {
auth.signout(() => navigate('/'))
}}
>
Sign out
</button>
</p>
)
}
接下来我们对 layout文件、页面组件文件和 router文件进行一些改造
Layout.tsx 引入 AuthStatus
// Layout.tsx
import { Outlet } from 'react-router-dom'
import AuthStatus from '../components/AuthStatus'
export default function Layout() {
return (
<div>
<AuthStatus />
<Outlet />
</div>
)
}
首页对跳转链接进行权限盘
// Home.tsx
import { Link } from 'react-router-dom'
import useAuth from '../../hooks/useAuth'
export default function Home() {
const auth = useAuth()
return (
<ul>
{!auth.user ? (
<li>
<Link to="/login">登录</Link>
</li>
) : (
<li>
<Link to="/backend">后台</Link>
</li>
)}
{auth.role === 'admin' ? (
<li>
<Link to="/admin">管理员</Link>
</li>
) : null}
</ul>
)
}
登录页面
// Login.tsx
import { Link, useLocation, useNavigate } from 'react-router-dom'
import useAuth from '../../hooks/useAuth'
export default function Login() {
const navigate = useNavigate()
const location = useLocation()
const auth = useAuth()
const from = location.state?.from?.pathname || '/'
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
let formData = new FormData(event.currentTarget)
let username = formData.get('username') as string
auth.signin(username, () => {
navigate(from, { replace: true })
})
}
return (
<>
<h1>登录页</h1>
<div>
<form onSubmit={handleSubmit}>
<label>
Username: <input name="username" type="text" />
</label>
<button type="submit">Login</button>
</form>
</div>
<Link to="/">回首页</Link>
</>
)
}
修改 router 文件
// router.tsx
import { lazy } from 'react'
import { createBrowserRouter } from 'react-router-dom'
import Layout from '../layout'
import AuthRoute from '../components/AuthRoute'
import Home from '../pages/home/Home'
// lazy-loading 需要结合 React.Suspense
const Login = lazy(() => import('../pages/login/Login'))
const Backend = lazy(() => import('../pages/backend/Backend'))
const Admin = lazy(() => import('../pages/admin/Admin'))
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: <Home />,
},
{
path: 'login',
element: <Login />,
},
{
path: 'backend',
element: (
<AuthRoute>
<Backend />
</AuthRoute>
),
},
{
path: 'admin',
element: (
<AuthRoute>
<Admin />
</AuthRoute>
),
},
],
},
])
export default router
到此,我们便使用 React-Router V6.x 实现了一个前端路由鉴权。
附上代码文件路径截图
总结
React-Router v6比以前的版本有了巨大的改进。它快速、稳定、可靠。除了更容易使用外,它还有许多新功能,如<Outlet/>和改进的<Route/>组件,大大简化了React应用程序中的路由。
有了v6.x中提供的新路由器和 Data API,您可以轻松处理UI、pending 和错误状态。您可以在组件外部抽象和加载数据,直到数据准备就绪,再显示UI
我希望您本文的介绍,能够对 React-Router v6的使用有一定帮助,以及如何使用 React-Router v6 处理用户身份验证有了更好的了解。