React 是一个用于构建用户界面的 JavaScript 库,它是单页面应用(SPA). 单页面应用,顾名思义:只有一个页面,它是没有路由导航机制的. 这时候往往需要一种路由机制,以便在不同的视图之间切换而不用刷新整个网页. React-Router 就是一个扩展 React 从而实现多页面跳转的第三方库。
React-Router 入门可以参考这篇文章React Router 入门完全指南(包含 Router Hooks)🛵
在这篇文章中, 我们不借助任何脚手架。从零到一实现下图中的效果!
安装依赖
yarn add react-router-dom
yarn add @types/react-router-dom --dev
路由基础配置
我们先实现一个简单的路由配置及页面跳转功能
import React from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
export default function BasicExample() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/dashboard">Dashboard</Link>
</li>
</ul>
<hr />
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/dashboard">
<Dashboard />
</Route>
</Switch>
</div>
</Router>
);
}
function Home() {
return (
<div>
<h2>Home</h2>
</div>
);
}
function About() {
return (
<div>
<h2>About</h2>
</div>
);
}
function Dashboard() {
return (
<div>
<h2>Dashboard</h2>
</div>
);
}
我们通过导入三个组件,并渲染在浏览器中。这三个组件就对应了三个动态路由
Home
组件对应路径/
About
组件对应路径/about
Dashboard
组件对应路径/dashboard
Link
组件用于切换页面并且不会刷新整个页面,React Router
还会使 URL 保存到浏览器历史记录 📝 中,并且可以通过浏览器的回退
,前进
按钮进行操作
<Switch>
会遍历它下面的所有<Route>
,然后渲染第一个路径与当前网址匹配的组件。
<Switch>
匹配到第一个路由就不会继续匹配了,如果不加Route
里不加exact
,那么凡是Link
里面to
的路径包含了/
,那么就会被匹配到 exact 是精准匹配,只有路径完全一致才能被匹配到
路由嵌套
在真实的前端场景中,路由下面还有它的子路由-即路由嵌套
比如/user
路径下有 /user/login
,/user/register
那么当出现这样的场景时,路由应该怎么配置呢?
先来看一个 React Router 官方的路由嵌套例子
可以发现
- 首先在最外层组件有一份
Switch
包裹的路由配置-包含了/
的子路径页面 - 然后在
Topics
组件内有一份Switch
包裹的路由配置-包含了/topics
的子路径页面. 在Topics
组件就用到了路由嵌套的逻辑,即在组件内部再声明一份Switch
包裹的路由配置,此配置是当前路径对应的子路径
因为路由也是 React 组件,因此Switch
/Route
可以渲染在代码中的任何位置,包括作为子元素。当需要对应用进行代码拆分为多个包时,这是非常有用的
所谓代码代码拆分,就是将不同业务进行代码拆分,相同业务的代码装载在一起。
那么如何区别业务相同与不同呢?
这时就可以通过路由来进行区别拆分, 然后把子路由也渲染到拆分的包里面去(在子组件内部再声明一份Switch
包裹的路由配置)。 这就非常符合代码分离的思想了,不是吗?ლ(′◉❥◉ ` ლ)
配置式路由
真实开发场景,我们往往只需要一个路由配置文件生成路由及菜单,以方便管理所有的路由,这也符合 React 中 UI=F(data)
的数据驱动视图思想
主要思路是: 路由配置文件作为一份静态数据存储
- 通过循环遍历生成
<Switch>
和<Route>
,即路由配置 - 通过循环遍历生成侧边栏菜单
先来看一个 React Router 官方的路由配置例子
侧边栏菜单
有一种最常见的左右布局方式-左边菜单,右边内容。
我们结合 React-Router 应该如何实现呢。其实只需要修改一下我们前面代码的布局样式即可
主要思路是: 路由配置文件作为一份静态数据存储
- 通过循环遍历生成
<Switch>
和<Route>
,即路由配置 - 通过循环遍历生成侧边栏菜单
- 修改 CSS 样式实现左右布局
先来看一个 React Router 官方的侧边栏菜单
React Router + Antd 实现侧边栏菜单
从前面路由嵌套的例子 🌰 中
const routes = [
{
path: "/sandwiches",
component: Sandwiches,
},
{
path: "/tacos",
// 公共布局组件
component: Tacos,
routes: [
{
path: "/tacos/bus",
component: Bus,
},
{
path: "/tacos/cart",
component: Cart,
},
],
},
];
function Tacos({ routes }) {
return (
<div>
<h2>Tacos</h2>
<ul>
<li>
<Link to="/tacos/bus">Bus</Link>
</li>
<li>
<Link to="/tacos/cart">Cart</Link>
</li>
</ul>
<Switch>
<Route path="/tacos/bus" component={Bus} />
<Route path="/tacos/cart" component={Cart} />
</Switch>
</div>
);
}
我们发现 /tacos
对应的组件主要作用是界面公共布局和渲染子路由,如果了解过Antd Design Pro
路由配置的同学应该很快就能理解。
那么接下来让我们改造代码,让我们的项目看起来更像Antd Design Pro
布局文件
Antd Design Pro
中有多个布局文件
BasicLayout
:用于有主页界面的布局UserLayout
:用于用户登录/注册界面的布局BlankLayout
:空白布局
路由文件
真正的嵌套路由配置可以这样理解
渲染路由
const RouteWithSubRoutes = (routeDatas: menuType[]) => {
if (Array.isArray(routeDatas) && routeDatas.length > 0) {
return routeDatas.map((item) => {
const bool = Array.isArray(item.routes) && item.routes.length > 0;
if (bool) {
return (
<Route
key={item.path}
path={item.path}
render={(props) =>
Array.isArray(item.routes) && item.routes.length > 0 ? (
// item.component 是 BasicLayout,UserLayout,BlankLayout其中一个布局
<item.component {...props}>
<Switch>
<Route exact path={item.path}>
<Redirect to={item.routes[0].path} />
</Route>
{/*当路由有多级的时候,递归渲染出多份路由配置文件*/}
{RouteWithSubRoutes(item.routes)}
<Route path="*">
<NoMatch />
</Route>
</Switch>
</item.component>
) : (
<Redirect
to={{
pathname: "/user/login",
state: { from: props.location },
}}
/>
)
}
/>
);
}
// 当路由只有一级的时候,直接渲染界面
return (
<Route path={item.path} key={item.path}>
<item.component />
</Route>
);
});
}
return null;
};
渲染侧边栏菜单
侧边栏菜单主要在 用户登录成功后,访问主页界面时才显示。也就是在BackLayout
中进行显示
const Index: React.FC = (props) => {
const handleClick: MenuClickEventHandler = (e) => {
console.log("click ", e);
};
// 递归生成菜单
const generateMenu = (menus: menuType[]) => {
return menus.map((item) => {
if (Array.isArray(item.routes) && item.routes.length > 0) {
return (
<SubMenu key={item.path} icon={<SettingOutlined />} title={item.name}>
{generateMenu(item.routes)}
</SubMenu>
);
}
return (
<Menu.Item key={item.path} icon={<AppstoreOutlined />}>
<Link to={item.path}>{item.name}</Link>
</Menu.Item>
);
});
};
// 从routes配置中提取出 “/home” 下的子路由, 来进行菜单渲染
const renderMenu = (datas: menuType[]) => {
if (
Array.isArray(datas) &&
datas.length > 0 &&
Array.isArray(datas[0].routes) &&
datas[0].routes.length > 0
) {
const homeMenus = datas[0].routes.filter((item) => item.path === "/home");
if (
Array.isArray(homeMenus) &&
homeMenus.length > 0 &&
Array.isArray(homeMenus[0].routes) &&
homeMenus[0].routes.length > 0
) {
const realHomeMenus = homeMenus[0].routes;
return generateMenu(realHomeMenus);
}
}
return null;
};
return (
<Layout style={{ minHeight: "100vh" }}>
<Sider collapsible collapsed={false}>
<div className="logo" />
<Menu onClick={handleClick} mode="inline" theme="dark">
{renderMenu(routes)}
</Menu>
</Sider>
<Layout className="site-layout">
<Header className="site-layout-background" style={{ padding: 0 }} />
<Content style={{ margin: "16px 0 0 16px" }}>
<div
className="site-layout-background"
style={{ padding: 24, minHeight: 360 }}
>
{props.children}
</div>
</Content>
<Footer style={{ textAlign: "center" }}>
Ant Design ©2023 Created by Ant UED
</Footer>
</Layout>
</Layout>
);
};
export default Index;
常见问题
手动刷新子页面出现 404
修改开发环境的 webpack 配置
const confog = {
output: {
publicPath: "/",
},
};
不能在 if/循环语句中使用 React Hooks
界面会出现报错。因为 React Hooks 的底层是通过有序链表实现的,如果放到 if/循环语句就可能导致执行顺序发生改变,从而导致意料之外的错误 ❎
最后
这里我们总结一下本篇文章做过的事
- 手动创建路由配置文件
- 根据路由文件生成路由
- 根据路由文件生成菜单
其实还有路由权限校验的知识这里没有说明,感兴趣的同学欢迎在评论区发表你的见解