路由是前端应用架构中最重要的基础框架
在整个架构设计初期,路由设计是第一优先级。因为路由相对独立且基础,它通常不依赖于具体业务和数据模型,但是却为整个应用的页面和导航提供了框架 在设计路由时,应遵循以下几个原则:
- 明确性:URL路径应该清晰、简洁,能够准确反映页面内容。例如,/products应该对应产品列表,/products/:id对应具体产品详情。
- 层次性:路径应该有层次感,能够反映页面之间的层级关系。例如,/dashboard/settings显然是一个设置页面,并且位于仪表盘下。
- 可扩展性:路由设计应考虑未来的扩展需求,留有足够的灵活性。例如,可以为将来的新功能预留路径。
- 一致性:路由的命名和结构应该保持一致,避免混淆。比如所有资源的详情页都使用/:resource/:id这种形式。
路由定义了页面的访问路径,负责组织和协调各个页面组件的展示逻辑,为其他内容(页面、组件、样式、接口等)通过了基础架构和明确的内容指引
- 导航控制:路由控制用户能够访问的页面和路径,提供导航的逻辑。
- 组件加载:通过路由决定哪些组件在何时加载,包括懒加载组件以优化性能。
- 权限管理:结合路由守卫实现权限管理,决定不同用户可以访问的内容。
- 数据流管理:在路由级别进行数据加载和管理,可以决定页面加载前后需要处理的数据。
最基础的路由参数
核心就2个: RouterProvider(管理路由上下文) 和 createBrowserRouter(createHashRouter) - 创建不同类型的路由器
RouterProvider
创建的路由器对象传递给整个应用,使得路由配置生效。
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
const router = createBrowserRouter([
// ... 路由配置
])
const App = () => {
return (
<RouterProvider router={router} />
)
}
createBrowserRouter和createHashRouter 是什么?有什么区别?
创建路由的两个函数,主要基于不同的历史管理机制
createBrowserRouter
: 创建基于HTML5 history API的路由器。它使用浏览器的pushState和popState事件来管理URL。可以实现无刷新地改变URL路径,非常适合现代浏览器。由于路径不包含哈希符号,URL看起来更干净,且对SEO更友好(特别是在服务器端渲染的情况下)。需要服务器配置来处理不同的URL路径,通常需要配置URL重写规则,让所有请求都指向应用的入口文件(例如index.html)。
1. 优势1:URL美观:不带#符号的URL更美观,更符合用户习惯,且对SEO友好。
2. 优势2:现代Web开发:利用HTML5 history API,允许在不刷新页面的情况下改变URL,同时保留浏览器的前进、后退功能。
3. 优势3:SEO优化:适合需要搜索引擎优化的场景,因为干净的URL更容易被搜索引擎索引。
4. 劣势1:服务器配置:需要服务器配置支持所有URL都指向应用入口文件。常见的配置包括Apache的.htaccess
或Nginx的try_files
。
createHashRouter
: 创建基于URL哈希部分的路由器。它使用URL中的**#
**部分来管理路由,这种方式与浏览器的原生行为兼容,不需要服务器端的特别配置。适用于老旧浏览器,或在服务器不支持HTML5 history API的情况下使用。由于URL的变化仅在哈希部分进行,因此无需服务器端的URL重写规则,所有请求都可以直接加载应用的入口文件。
1. 优势1:兼容性:适用于需要兼容老旧浏览器的场景,这些浏览器可能不完全支持HTML5 history API。
2. 优势2:无需服务器配置:哈希路由器不需要特殊的服务器配置,因为URL的变化仅在客户端处理。这对于静态网站托管(如GitHub Pages)非常有用。
3. 劣势1:SEO影响:哈希路由对SEO不友好,因为搜索引擎通常不处理#之后的URL部分。这意味着使用哈希路由的页面可能不会被搜索引擎很好地索引。
4. 劣势2:URL格式:URL中带有#符号,例如http://example.com/#/about
,看起来不如直接的路径美观。
使用 createBrowserRouter
为什么需要进行服务器配置?
使用
createBrowserRouter
在一个单页应用(SPA)中涉及的前后端交互比较独特,下面描述一下这个过程
- 当用户输入网址
http://example.com
并访问网站,服务器返回index.html
文件以及相关的JavaScript、CSS
文件。这时,React Router
负责渲染与当前 URL 匹配的组件,通常是应用的首页。 - 用户在应用中点击链接到
/about
页面。使用 createBrowserRouter 的 React Router 通过调用history.push('/about')
来改变浏览器的地址栏为http://example.com/about
同时不会向服务器发送请求。React Router 接管路由逻辑,根据路由配置动态渲染 About 组件。 - 如果用户直接在浏览器地址栏输入
http://example.com/about
或在该页面上刷新页面,浏览器会向服务器发送请求寻找/about
路径。由于这是一个单页应用,服务器文件系统中并没有名为 about 的文件或目录,这通常会导致服务器返回404错误,除非进行特别的配置。
为了解决这个问题,确保单页应用能够正确处理直接访问或刷新非首页的 URL,服务器需要被配置为对于所有此类请求都返回 index.html 文件,同时由前端的 React Router 接管路由并渲染正确的组件。通常会使用 Nginx 方案
Nginx方案 & 请求路径的解析过程
server {
listen 80;
server_name example.com;
location / {
root /var/www/your-spa;
try_files $uri $uri/ /index.html;
}
}
Nginx 的 root 指令用于指定请求文件的根目录。例如,假设你的前端应用构建后的文件存储在 /var/www/your-spa 目录中,那么配置将会是:root /var/www/your-spa
假设前端应用构建后的文件存储在 /var/www/your-spa 中,目录结构如下:
/var/www/your-spa
├── index.html
├── main.js
├── main.css
└── assets/
├── logo.png
└── ...
- 当用户访问
http://example.com/
时( 访问根路径/
): Nginx 会在/var/www/your-spa
目录中查找文件 index.html。 找到并返回该文件,由浏览器渲染首页。 - 当用户访问
http://example.com/main.js
时(访问静态文件): Nginx 会在/var/www/your-spa
目录中查找文件 main.js。 找到并返回该文件,浏览器加载和执行该文件。 - 当用户访问
http://example.com/about
时(访问路径/about
):- Nginx 会在
/var/www/your-spa
目录中查找文件 about。 由于这是单页应用,实际文件系统中没有 about 文件。 - 接着 Nginx 会查找目录 /about/,也不存在。
- 最后,Nginx 返回 index.html,前端 JavaScript(例如 React Router)处理并渲染 /about 路由对应的组件。
- Nginx 会在
**try_files**
指令的作用
- $uri:检查请求路径对应的文件是否存在,例如
/var/www/your-spa/about
。 - $uri/:如果前者不存在,再检查对应的目录是否存在,例如
/var/www/your-spa/about
。 - /index.html:如果以上都不存在,返回 index.html,从而让前端的路由系统处理该路径。
createHashRouter在Nginx中不需要做任何的特殊处理,直接返回入口文件即可(通常是index.html
),路由操作交由前端处理
1. 访问 http://example.com/#/about
时,浏览器发送到服务器请求的是 http://example.com
,不包括 #/about
,这是因为 #
号后面的内容(称之为 fragment 或 hash)在HTTP请求中不被发送到服务器,而仅仅在客户端处理,这是URL规范和浏览器行为的一部分。
2. 当你在浏览器中输入 http://example.com/#/about 并回车:浏览器只会向服务器发送 http://example.com/ 请求。服务器接收到请求,并返回根目录的资源(通常是 index.html)。浏览器加载返回的 HTML 内容后,解析和处理 URL 的哈希部分(#/about),并在客户端进行相应的操作(例如,前端路由库会根据哈希部分渲染相应的组件)。
3. 为什么不发送Hash到服务器?Hash 最初用于在同一页面内导航到特定位置,例如,在页面总,有多个标题,跳转 HTML 文档到其中的某个标题。
createBrowserRouter在Nginx中会直接先查找对应的文件和文件夹,如果没有则直接返回index.html文件,交由前端处理
1. 用户访问 http://example.com/about
浏览器向服务器发送请求:http://example.com/about
。服务器首先查找 /var/www/your-spa/about
文件,如果不存在则查找 /var/www/your-spa/about/
目录。如果都不存在,返回 /var/www/your-spa/index.html
文件。
2. 浏览器加载 index.html
:前端 JavaScript 代码(例如,React 和 React Router)解析 URL 路径 /about
。前端路由库处理路径路由,并渲染相应的组件(AboutPage)。
Outlet useOutlet
Outlet
是一个占位符组件,用于渲染嵌套的子路由。它在父路由组件中定义,当子路由匹配时,Outlet
会渲染相应的子路由组件。
useOutlet
是一个 React Hook,用于在子路由组件中获取 Outlet 渲染的内容。
- 嵌套路由处理:
useOutlet
是专门处理嵌套路由设计的,在组件内部获取并渲染嵌套的子路由内容 - 条件渲染:通过
useOutlet
可以非常方便地实现根据嵌套路由,是否匹配来渲染不同的逻辑或内容 - 保持路由结构清晰:使用
useOutlet
可以保持路由结构的清晰和简洁,避免复杂的条件判断和状态管理逻辑
具体案例
路由配置:
import React from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import CustomDashboard from './CustomDashboard';
import HomePage from './HomePage';
import AboutPage from './AboutPage';
import SettingsPage from './SettingsPage';
import ProfilePage from './ProfilePage';
import Layout from './Layout';
import ErrorPage from './ErrorPage';
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
errorElement: <ErrorPage />,
children: [
{
index: true,
// element: <HomePage />
element: <CustomDashboard />
},
{
path: "about",
element: <AboutPage />
},
{
path: "settings",
element: <SettingsPage />
},
{
path: "profile",
element: <ProfilePage />
}
],
},
]);
const App = () => (
<RouterProvider router={router} />
);
export default App;
CustomDashboard.jsx
import React from 'react';
import { useOutlet } from 'react-router-dom';
const CustomDashboard = () => {
const outlet = useOutlet();
return (
<div>
<h2>Dashboard</h2>
{outlet ? (
<div>
{outlet}
</div>
) : (
<div>
<h3>Welcome to your dashboard</h3>
<p>Here you can see an overview of your account and manage your settings.</p>
</div>
)}
</div>
);
};
export default CustomDashboard;
Layout.jsx
import React from 'react';
import { Outlet, Link } from 'react-router-dom';
const Layout = () => (
<div>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/dashboard">Dashboard</Link>
</nav>
<hr />
<Outlet />
</div>
);
export default Layout;
/、/about、/settings、/profile 这四个路由命中之后,会分别执行具体的页面组件。它们会被渲染在**Outlet**
中。这就是 Outlet 的作用
useOutlet
在 CustomDashboard 组件中使用,它的目的是获取 Outlet
渲染的内容,这个意思是假如我的路由触发的是 /
那么会命中 CustomDashboard 组件,在当前组件中通过 useOutlet 去获取 Outlet 渲染的内容,只要不是这三个路由中的一个 /about、/settings、/profile,那么最后得到的值就是undefined,按照 CustomDashboard 组件内部的逻辑,会提示 Welcome to your dashboard
,如果命中了另外三个路由,那么就会命中 <div>{outlet}</div>
这套逻辑。
路由的作用和 Outlet 的功能 顶级路由: - /:当用户访问根路径 / 时,会渲染 Layout 组件。 - Layout 组件包含一个导航栏和一个 Outlet。Outlet 是一个占位符,表示在这里渲染匹配的子路由组件。 嵌套路由: - /about:匹配到 AboutPage,并在 Layout 的 Outlet 位置渲染 AboutPage 组件。 - /settings:匹配到 SettingsPage,并在 Layout 的 Outlet 位置渲染 SettingsPage 组件。 - /profile:匹配到 ProfilePage,并在 Layout 的 Outlet 位置渲染 ProfilePage 组件。 默认路由: - 当用户访问根路径 / 时,由于没有指定具体的子路径,默认会渲染 CustomDashboard 组件(通过 index: true 指定)。
useOutlet 的作用: useOutlet 在 CustomDashboard 组件中使用,用于获取当前 Outlet 渲染的内容。
功能:useOutlet 可以访问并返回当前匹配的嵌套路由组件。 逻辑: - 当访问根路径 / 时,useOutlet 返回 null,因为没有嵌套路由匹配。这时,CustomDashboard 显示欢迎信息。 - 当访问 /settings 或 /profile 时,useOutlet 返回相应的组件 (SettingsPage 或 ProfilePage) 并渲染它们。
tip: index 路由:在 App.jsx 中,
<Route index element={<CustomDashboard />} />
表示当用户访问 /dashboard 时,默认渲染 CustomDashboard 组件。如果没有其他子路由匹配时,CustomDashboard 会被渲染。index 路由:用于定义父路径的默认子路由,在没有特定子路径匹配时渲染。
CustomDashboard 中的 useOutlet
**作为子组件的默认页面**
:在这种情况下,CustomDashboard 用于处理默认页面,只有在没有特定子路由匹配时才会渲染。它和其他子路由是互斥的。
- 作为默认页面使用:CustomDashboard 配置在子路由中,作为默认页面。当没有具体子路由匹配时,显示默认内容。示例路径:/dashboard 显示 CustomDashboard,/dashboard/settings 显示 SettingsPage。
{
path: "dashboard",
element: <DashboardPage />,
children: [
{
index: true,
element: <CustomDashboard />
},
{
path: "settings",
element: <SettingsPage />
},
{
path: "profile",
element: <ProfilePage />
}
]
}
**作为父组件中的中间层**
:在这种情况下,CustomDashboard 作为一个中间层组件,用于包裹其他子组件,并且可以在此基础上进行一些扩展和条件判断。
- 作为中间层使用:CustomDashboard 配置在父路由中,作为中间层包裹其他子路由,可以扩展和进行条件判断。示例路径:/dashboard 显示 DashboardPage,/dashboard/settings 显示 SettingsPage,CustomDashboard 作为父层组件在这些路由之上添加额外逻辑或内容。
{
path: "dashboard",
element: <CustomDashboard />,
children: [
{
index: true,
element: <DashboardPage />
},
{
path: "settings",
element: <SettingsPage />
},
{
path: "profile",
element: <ProfilePage />
}
]
}
路由配置 和 路由渲染 这两个分别是什么?
路由配置:定义应用的路由规则和路径映射,包括:路径(path)、组件(element)、嵌套路由等等 路由渲染:在运行时根据当前 URL 和路由配置渲染匹配的组件,通常使用 Outlet 组件来处理嵌套路由内容。
路由配置一般有2种方案
第一种使用JSX语法配置路由
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import HomePage from './HomePage';
import AboutPage from './AboutPage';
import DashboardPage from './DashboardPage';
import Layout from './Layout';
import ErrorPage from './ErrorPage';
const App = () => (
<Router>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<HomePage />} />
<Route path="about" element={<AboutPage />} />
<Route path="dashboard" element={<DashboardPage />} />
</Route>
<Route path="*" element={<ErrorPage />} />
</Routes>
</Router>
);
export default App;
第二种使用 createBrowserRouter/createHashRouter
import React from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import HomePage from './HomePage';
import AboutPage from './AboutPage';
import DashboardPage from './DashboardPage';
import Layout from './Layout';
import ErrorPage from './ErrorPage';
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
errorElement: <ErrorPage />,
children: [
{
index: true,
element: <HomePage />
},
{
path: "about",
element: <AboutPage />
},
{
path: "dashboard",
element: <DashboardPage />
}
],
},
]);
const App = () => (
<RouterProvider router={router} />
);
export default App;
路由渲染
import React from 'react';
import { Outlet, Link } from 'react-router-dom';
const Layout = () => (
<div>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/dashboard">Dashboard</Link>
</nav>
<hr />
<Outlet />
</div>
);
export default Layout;
layout 组件包含一个导航栏和一个 Outlet 组件。Outlet 是路由渲染的核心部分,负责在匹配到子路由时渲染对应的组件。 Outlet 不仅可以用于渲染嵌套路由,还可以用来渲染顶级路由的默认页面或任何指定的子路由内容。通过正确配置 Outlet 和路由结构,可以实现复杂的页面布局和路由渲染逻辑。
Link、NavLink
Link
是一个用于创建导航链接的组件。点击 Link 会使浏览器的地址栏 URL 更新,同时不会触发页面刷新。用于在应用内部进行导航,从而避免页面刷新,提高用户体验。
import React from 'react';
import { Link } from 'react-router-dom';
const Navigation = () => (
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/dashboard">Dashboard</Link>
</nav>
);
export default Navigation;
- to:目标路径(必需)
- replace:是否替换当前历史记录(可选)
NavLink
是一个特殊的 Link,用于创建导航链接,并能够根据当前 URL 路径自动添加样式或类名。用于导航菜单,在用户访问相应路径时,可以自动高亮显示当前激活的链接。
import React from 'react';
import { NavLink } from 'react-router-dom';
const Navigation = () => (
<nav>
<NavLink to="/" end>Home</NavLink>
<NavLink to="/about">About</NavLink>
<NavLink to="/dashboard">Dashboard</NavLink>
</nav>
);
export default Navigation;
- to:目标路径(必需)
- end:精确匹配,当 end 属性设置为 true 时,NavLink 只有在路径完全匹配时才会被激活。
- className:自定义类名,可以是一个字符串或函数(可选),可以是一个字符串,表示固定的类名。可以是一个函数,返回一个类名字符串。这个函数接收一个参数 isActive,根据 isActive 动态地返回类名。
- style:自定义样式,可以是一个对象或函数(可选)
- isActive:判断链接是否激活的布尔值(可选)
<NavLink
to="/"
end
style={({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? 'green' : 'blue'
})}
>
Profile
</NavLink>
它们的基本功能相同,都是用于创建导航链接。但 NavLink 具有一些 Link 不具备的功能:
- 自动激活样式和类名:NavLink 会根据当前 URL 路径自动添加激活样式或类名,以便在用户导航时指示当前页面。
- 精确匹配(exact matching):NavLink 可以通过 end 属性实现路径的精确匹配,只有当路径完全匹配时才应用激活样式。
Navigate、useNavigate
Navigate 和 useNavigate 是 React Router 提供的两个工具,用于在应用中实现编程式导航。它们提供了比 和
Navigate
是一个用于在渲染时进行导航的组件。当该组件渲染时,会立即导航到指定的路径。
import React from 'react';
import { Navigate } from 'react-router-dom';
const RedirectToHome = ({ condition }) => {
if (condition) {
return <Navigate to="/" />;
}
return <div>Condition not met</div>;
};
export default RedirectToHome;
useNavigate
:定义:useNavigate 是一个 React Hook,返回一个函数,该函数用于执行编程式导航。用于在事件处理函数或效果钩子中进行导航,例如在表单提交后导航到另一个页面。
import React from 'react';
import { useNavigate } from 'react-router-dom';
const LoginForm = () => {
const navigate = useNavigate();
const handleSubmit = (event) => {
event.preventDefault();
// Perform login logic here
navigate('/dashboard');
};
return (
<form onSubmit={handleSubmit}>
<button type="submit">Login</button>
</form>
);
};
export default LoginForm;
- Navigate:用于在渲染时立即导航到指定路径,常用于条件重定向或自动重定向。
- useNavigate:用于在事件处理函数或效果钩子中进行编程式导航,提供了更灵活的导航方式。
useMatches和useLocation
路由信息访问:useMatches和useLocation用于访问当前路由和位置的信息。
useMatches
是一个 Hook,用于获取当前路由匹配的所有路径段(matches)。useMatches 不需要任何参数。它返回一个数组,包含每个匹配的路由对象。用于获取当前路由匹配的详细信息,特别是在嵌套路由中,可以用来获取父级和子级的路由信息。其中每个元素是一个对象,表示一个路由匹配的详细信息。每个对象可能包含以下属性:
- id:路由的唯一标识符。
- pathname:匹配的路径。
- params:路径参数的对象。
- data:通过路由加载的任何数据。
- handle:路由的处理程序。
import React from 'react';
import { useMatches } from 'react-router-dom';
const MatchesInfo = () => {
const matches = useMatches();
return (
<div>
<h2>Current Matches</h2>
<ul>
{matches.map((match, index) => (
<li key={index}>
Path: {match.pathname}
</li>
))}
</ul>
</div>
);
};
export default MatchesInfo;
useLocation
是一个 Hook,用于获取当前的位置信息。useLocation 不需要任何参数。它返回一个包含当前路径信息的对象,例如 pathname、search 和 hash。用于在组件中访问当前的 URL 信息,可以根据当前路径或查询参数做出相应的处理。返回一个对象,包含以下属性:
- pathname:当前的路径名。
- search:URL 中的查询字符串部分,包括问号。
- hash:URL 中的哈希部分,包括井号。
- state:通过导航传递的状态对象。
- key:唯一标识位置的字符串。
import React from 'react';
import { useLocation } from 'react-router-dom';
const LocationInfo = () => {
const location = useLocation();
return (
<div>
<h2>Current Location</h2>
<p>Pathname: {location.pathname}</p>
<p>Search: {location.search}</p>
<p>Hash: {location.hash}</p>
<p>State: {JSON.stringify(location.state)}</p>
</div>
);
};
export default LocationInfo;