简介
ReactRouter 是 React 的多策略路由器。利用react响应式原理,通过监听popstate和Histiry API来管理前端路由状态的三方库。在React项目中可以通过三种模式使用React Router,分别是声明式(我叫他组件模式)、配置和框架。
本文通过BrowserRouter模式,分析React Router组件模式工作的基本原理。整理路由的配置、导航和路由传参相关知识。旨在掌握 React Router 组件模式的基本使用,并通过理解其原理来学会分析和解决实际问题。
基本原理
根据React Router的基本使用分析,简单的介绍每个组件做了什么,了解其最基本的工作原理。React Router的基本使用如下:
function App() {
return (
//创建路由
<BrowserRouter>
<nav>
{/* 导航 */}
<Link to="/">首页</Link>
</nav>
{/* 路由配置 */}
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</BrowserRouter>
)
}
- BrowserRouter 组件负责创建路由,并提供路由信息和设置路由的上下文。在BrowserRouter中监听创建路由状态,监听popState事件,在点击浏览器导航按钮时,修改路由状态,触发更新。代码如下:
-
const RouterContext = createContext() function BrowserRouter({ children }) { const [location, setLocation] = useState({ pathname: '/', state: null, }) useEffect(()=>{ const handlePopState = ()=>{ setLocation({ pathname: window.location.pathname, state: window.location.state, }) } window.addEventListener('popstate',handlePopState) return ()=>{ window.removeEventListener('popstate',handlePopState) } }, []) return ( <RouterContext value={{ location, setLocation }}> {children} </RouterContext> ) }
-
- Routes 组件负责整合路由配置,匹配路由和展示匹配的组件。Routes组件会获取到所有Route添加的路由配置,通过context获取到当前路由状态,从而匹配到需要展示的组件。
- Route 组件负责提供路由配置。
- Link 组件负责触发路由更新逻辑。Link组件内部获取RouterContext,点击Link组件式,组件内部将配置的目标路由添加到路由状态并修改History,触发更新。代码如下:
-
function Link({ to, children }) { const { setLocation } = useContext(RouterContext) const handleClick = (e)=>{ e.preventDefault() setLocation({ pathname: to, search: '', hash: '', state: null, }) } return ( <a href={to} onClick={handleClick}> {children} </a> ) }
-
基本使用
React Router的使用分为配置路由、路由导航和路由传参。在React Router中没有类似Vue-router中的路由守卫相关的api,但可以通过添加额外的代码实现路由守卫相同的鉴权功能。
路由配置
路由的基本配置是通过Routes组嵌套Route组件实现的,Route组件接收path和element 属性,实现路由和组件的绑定。在React Router中,为了适配实际开发中所涉及到的场景需求,提供了以下几种配置方式。
-
嵌套路由:通过嵌套路由实现父子路由,子路由的组件渲染到父组件的
Outlet组件中。嵌套路由通过Route组件的嵌套实现,在一些路由较多的系统中,可以将不同功能的路由分模块配置的代码组织形式实现,便于路由的维护。嵌套路由的基本配置如下:<Routes> <Route path="/" element={<Home />} /> <Route path="/user" element={<User />}> {/* 嵌套路由。匹配 /user/info */} <Route path="info" element={<Info />} /> </Route> </Routes> -
默认子路由:当页面位于父路由状态时,默认展示一个子组件。基于嵌套路由的实现,我们希望通过
/user就可以展示Info组件,我们通过将path="info"修改为index实现。默认子路由在同一嵌套路由的同一层级只能出现一次。同时默认路由不能有子项,如果需要子项可以通过布局路由实现。<Routes> <Route path="/" element={<Home />} /> <Route path="/user" element={<User />}> {/* 嵌套路由。匹配 /user */} <Route index element={<Info />} /> </Route> </Routes> -
布局路由:与默认路由类似,不需要配置path属性,也不需要index属性,布局路由可以配置子路由。可以代替默认路由使用
<Routes> <Route path="/" element={<Home />} /> <Route path="/user" element={<User />}> {/* 嵌套路由。匹配 /user */} <Route element={<Info />} /> </Route> </Routes> -
路由前缀:是有path属性,没有绑定element组件的路由配置。通过这种机制为子路由同意增加路由前缀。
<Routes> <Route path="/" element={<Home />} /> <Route path="/user"> {/* 嵌套路由。匹配 /user/info */} <Route path="/info" element={<Info />} /> </Route> </Routes> -
动态路由:如果路径段以
:开头,则它成为 “动态段”。当路由与 URL 匹配时,动态段将从 URL 中解析出来,并作为参数提供给其他路由器 API,如useParams。动态路由是路由传参的一种手段,需要注意的是,在一条路由配置中动态段是唯一的,否则后面的会覆盖前面的动态段。<Routes> <Route path="/" element={<Home />} /> <Route path="/user:id"> {/* 嵌套路由。匹配 /user/info */} <Route path="/info" element={<Info />} /> </Route> </Routes> //使用参数 function User() { const {userId} = useParams() return ( <div> <p>用户ID: {userId}</p> </div> ) } -
可选路由:通过在路由段的末尾添加
?标识可选路由段,可选路由可以将多个路由匹配到同一组件。同时结合动态路由可实现可选路由参数功能,如:<Routes> <Route path="/" element={<Home />} /> <Route path="/user:userId?"> {/* 嵌套路由。匹配 /user/info */} <Route index element={<Info />} /> </Route> </Routes> //使用参数 function Info() { const {userId} = useParams() return ( <div> <p>用户ID: {userId ? userId : ''}</p> </div> ) } -
通配路由:也称为 “catchall” 和 “star” 段。如果路由路径模式以 /* 结尾,则它将匹配 / 后面的任何字符,包括其他 / 字符。通配路由通常做为路由的托底配置,在单页面应用中,将未匹配上的路由绑定到404页面,提供更好的用户体验。通配符也可通过
useParams获取,但是需要重新命名。<Routes> <Route path="/" element={<Home />} /> <Route path="/user:id?"> {/* 嵌套路由。匹配 /user/id 或者 /user */} <Route index element={<Info />} /> </Route> {/* 全局 404 - 必须放在最后 */} <Route path="*" element={<NotFound />} /> </Routes> function NotFound() { const {"*": splat} = useParams() return ( <div> <p>通配符:{splat}</p> </div> ) }
路由跳转
在React Router中实现路由跳转主要有以下三种方式:Link、NavLink、useNavigate。其中Link、NavLink是内置组件,useNavigate是一个hook。在不需要用户操作情况下实现路由的跳转使用useNavigate。
- Link:最基础的跳转方式,通过包裹a标签并逐步增强。通常情况下使用Link即可
<Link to="/some/path>some path</Link> - NavLink: 对Link的扩展,在其基础上增加了状态。比如菜单的激活状态(active)等,其className、style和children属性可以设置成为回调函数,函数可以获取激活状态,实现样式或者组件的切换。
<NavLink to="/message"> {({ isActive }) => ( <span className={isActive ? "active" : ""}> {isActive ? "👉" : ""} Tasks </span> )} </NavLink> - useNavigate:主要用于在不需要用户操作情况下实现路由的跳转,如表单提交成功后的页面跳转...
function LoginPage() { let navigate = useNavigate(); return ( <> <MyHeader /> <MyLoginForm onSuccess={() => { navigate("/dashboard"); }} /> <MyFooter /> </> ); }
路由传参
- 动态路由(params):路由配置中介绍了什么是动态路由,以及动态路由如何配置,这里介绍以下动态路由如何传递参数和获取。通过动态路由传递的参数会体现在URL上面, URL可分享,SEO友好,但是传递的参数较为固定,需改参数需要修改路由配置,不灵活。
//跳转传递参数 123 <Link to="/user/123">用户详情</Link> function UserDetail(params) { //useParams获取params const {userId} = useParams() return ( <div> <p>用户ID: {userId}</p> </div> ) } - 查询参数:查询参数是URL中拼接在
?后面的数据,通过useSearchParams获取。与动态路由传参一样参数会暴露在URL中,但不同的是,查询参数的使用相对灵活,可以在使用路由跳转的时候直接添加。查询参数过多会导致URL过长,影响美观。// 通过链接跳转 <Link to="/search?q=react&page=1">搜索React</Link> // 或者编程式导航 navigate('/search?q=react&page=1'); function Search() { const [searchParams, setSearchParams] = useSearchParams(); const query = searchParams.get('q'); const page = searchParams.get('page') || '1'; return <div>搜索: {query}, 页码: {page}</div>; } - state:通过state选项传递,state是通过history.state实现的,因此传递的参数不会体现在URL上面,通过分享链接打开后无法获取到state的值。state在使用的灵活性上和查询参数方式差不多。
// 通过Link组件 <Link to="/profile" state={{ from: 'homepage', userId: 123 }} > 个人资料 </Link> // 编程式导航 navigate('/profile', { state: { from: 'homepage', userId: 123 } }); function Profile() { const location = useLocation(); const { from, userId} = location.state || {}; return <div>来自: {from}, 用户ID: {userId}</div>; }
路由守卫
React Router没有提供类似于vue-router中的路由守卫,因为在React Router中添加路由首位的方式是灵活的,它不需要提供额外的API来实现。在React hooks组件开发模式中,组件的本质就是函数,路由的切换本质上会从提供路由状态的节点重新执行页面所需的所有render函数,将鉴权逻辑提取封装成函数,函数返回children属性(称为鉴权组件)。在路由配置中,针对各页面需要的权限包裹对应的鉴权组件即可实现路由守卫的功能即可。所以路由守卫和鉴权的逻辑和代码的组织形式完全是开放和灵活的。示例如下:
// 路由守卫组件
function RequireAuth({ children }) {
const isAuthenticated = !!localStorage.getItem('token');
const location = useLocation();
if (!isAuthenticated) {
// 重定向到登录页,并保存当前路径以便登录后返回
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
// 使用方式
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<RequireAuth>
<Dashboard />
</RequireAuth>
}
/>
<Route
path="/profile"
element={
<RequireAuth>
<Profile />
</RequireAuth>
}
/>
</Routes>