摘要
本文探讨了前端路由的演变历史,介绍了传统 Web 开发中的服务端路由与现代 SPA 中的前端路由。详细讲解了前端路由的核心原理和常见的路由类型,包括 Hash 路由、History 路由和 Memory 路由,以及它们各自的特点和适用场景。重点介绍了 React Router v6 和 vue-router 的关键功能与 API,展示了如何在实际开发中使用这些工具进行路由管理,并提供了自定义路由系统的实现思路。通过这些知识,开发者可以更好地理解和应用路由技术,提升应用的用户体验和开发效率。
前端路由的发展历史
前端常见的路由有 React Router 和 Vue Router。
在传统 Web 开发过程 (前后端未分离时期)中, 通常都是由后端来控制路由, 比如 MVC 分层架构下 Controller 来控制路由分发。在 SPA ( Single Page Application ) 出现之后前端才接管路由的。
前端控制路由的核心是: 不刷新页面切换组件。
服务端控制路由
前端控制路由
前端路由是什么?
界面的变化意味着组件的变化。组件的变化意味着数据的变化。
路由的核心
监听 path/url 的变化, 然后根据 path 和 components 的对应关系, 触发一些组件 unmount 和 mount, 同时使用 context 注入上下文, location, navigator。
根据 path 变化, unmount 组件, mount 组件。
路由的分类
在现代前端开发中,路由系统的选择对应用的架构和用户体验有着重要影响。了解不同类型的路由及其适用场景,可以帮助我们更好地设计和优化应用。常见的路由分类包括 History 路由、Hash 路由和 Memory 路由。每种路由都有其独特的特性和优缺点,我们可以根据实际需求选择合适的方案。
History 路由 和 Hash 路由 是最常用的前端路由类型
Hash 路由
Hash 路由 通常使用 # 号标记,虽然实现简单,但不够美观,且最初设计为锚点,不会发起额外的请求。此外,Hash 路由不支持服务器端渲染(SSR)。
window.location.hash
History 路由
History 路由 更为优雅,支持复杂的 URL 路径,并能够与 SSR 兼容。然而,它在部署时需要特别的配置,以确保服务端能够正确处理前端路由和后端路由的请求。
history./(go|back|pushState|replaceState|forward)/
Memory 路由
不是主流。
Memory 路由是一个在内存中处理路由的方式,主要用于开发和测试场景,不适合用于生产环境的实际用户访问。它为测试提供了灵活性,但在实际应用中,通常会选择 History 路由或 Hash 路由来处理前端路由和用户体验。
它的主要特点包括:
- 不依赖于浏览器的 URL:Memory 路由将路由信息存储在内存中,而不是在浏览器的地址栏中。这意味着它不会影响或显示在浏览器的 URL 中。
- 适用于测试:由于 Memory 路由不依赖于 URL,它非常适合用于测试环境。你可以在测试中轻松地模拟不同的路由状态,而无需处理实际的 URL 变化。
- 不会引起页面刷新:由于它不涉及浏览器地址栏的变化,Memory 路由不会引起页面刷新。这对于开发单页应用(SPA)时,尤其是在状态管理和测试中,能够提供更好的控制。
- 不适合生产环境:Memory 路由不适用于实际的生产环境,因为它无法与服务器端路由或浏览器的历史记录交互。生产环境通常需要 History 路由或 Hash 路由来确保正确的 URL 和浏览体验。
React Router 中的 MemoryRouter 就是一个 Memory 路由的实现,它可以用来在测试中模拟不同的路由状态,而不必依赖实际的浏览器环境。
History 路由和 Hash 路由有什么区别?
-
Hash 路由一般会带一个 # 号, 不够美观;
-
Hash 路由最早是个锚点, 不会发起请求;
-
Hash 不支持 SSR
-
History 路由部署的时候需要特殊处理一下, 因为服务端也不知道你这是前端路由还是后端路由。
-
# nginx 拦截配置 location / { try_files uri $uri /xx/xx/xxx/xx/index.html # 锁在这里, 不管怎么样都访问这个地址,都指向它。 }
-
React 路由
React Router v6 和 react-router-dom
- v6
- react-router-dom
在 React 应用开发中,路由管理是构建单页应用(SPA)不可或缺的核心组件。React Router 是当前最被广泛采用的路由解决方案,其中 React Router v6 代表了该库的最新主要版本,带来了更高效和更灵活的路由功能。通过使用 react-router-dom,开发者可以在 React 应用中实现精细的路由配置,确保用户体验的流畅性和一致性。
React Router v6 是 React Router 库的最新主要版本,提供了增强的路由功能和改进的 API 设计。react-router-dom 是 React Router v6 的一个核心包,专门用于在浏览器环境中实现路由功能。它提供了与 React 应用紧密集成的路由组件和钩子,使开发者能够高效地配置和管理前端路由。
简而言之,React Router v6 是一个路由解决方案的版本,而 react-router-dom 是该版本在浏览器环境中的实现,二者密不可分,共同为 React 应用提供灵活而强大的路由管理功能。
React Router v6
提供了一些核心的 API, 如 Router、Route 这些, 但是不提供 DOM 相关的。
引入了一系列关键特性:
- 优化的 API:v6 版本简化了路由配置和管理,增强了开发者的操作便捷性。
- 增强的嵌套路由支持:提供更强大的嵌套路由能力,使复杂页面结构的构建更加直观。
- 数据加载机制:直接在路由配置中进行数据加载,减少了数据获取的复杂性。
- 灵活的重定向和导航功能:改进的导航和重定向机制,使得路由行为的控制更加精准。
React-router-dom
提供 BrowserRouter, HashRouter, Link 这些 API, 可以通过 DOM 操作触发事件, 控制路由。
使用 react-router-dom,开发者能够在 React 应用中高效地配置和管理路由,显著提升用户的互动体验。
History 库
模拟/封装浏览器的 History 的一个库。V6 版本已经把这个库内置了,并且导出成了 navigation。
pnpm add react-router-dom hisotry
React Router 使用方式
基本使用方式
import { BrowserRouter, Route, Routes } from "react-router-dom";
function App() {
// 最常见的一种方法, 我们把路由信息嵌套在页面
return (
<div className="App">
<BrowserRouter>
<header>
<a href="/">首页</a>
<a href="/news">新闻列表</a>
<a href="/about">关于我们</a>
<a href="/hot">热点事件</a>
</header>
<Routes>
<Route element={<div>新闻动态页面</div>} path={"/news"} />
<Route element={<div>关于我们页面</div>} path={"/about"} />
<Route element={<div>热点事件页面</div>} path={"/hot"} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;
路由的本质应该是一个树形结构
import { BrowserRouter, Link, Outlet, Route, Routes } from "react-router-dom";
// { /*占位符, 相当于 Vue 中的 <router-view>*/ }
const Menu = () => (
<div>
<header>
{ /*Link标签的好处: 页面不会再刷新了, network 不会再向服务端请求html资源*/ }
<Link to="/">首页</Link>
<Link to="/news">新闻列表</Link>
<Link to="/about">关于我们</Link>
<Link to="/hot">热点事件</Link>
</header>
<Outlet />
</div>
);
;export default function App() {
// 路由的本质应该是一个树形结构
return (
<div className="App">
<BrowserRouter>
<Routes>
<Route path={"/"} element={<Menu />}>
<Route element={<div>新闻动态页面</div>} path={"/news"} />
<Route element={<div>关于我们页面</div>} path={"/about"} />
<Route element={<div>热点事件页面</div>} path={"/hot"} />
</Route>
</Routes>
</BrowserRouter>
</div>
);
}
提供了 Route, Routes, Link,NavLink, Outlet 等一系列组件
import {
BrowserRouter,
Link,
NavLink,
Outlet,
Route,
Routes,
} from "react-router-dom";
const NavMenu = ({ to, children }) => {
return (
<NavLink to={to} className={(isActive) => (isActive ? "nav-active" : "")}>
{children}
</NavLink>
);
};
const Menu = () => (
<div>
<header>
{ /*Link标签的好处: 页面不会再刷新了, network 不会再向服务端请求html资源*/ }
<NavLink to="/">首页</NavLink>
<NavLink to="/news">新闻列表</NavLink>
<NavLink to="/about">关于我们</NavLink>
<NavLink to="/hot">热点事件</NavLink>
</header>
<Outlet />
</div>
);
function App() {
// 路由的本质应该是一个树形结构
return (
<div className="App">
<BrowserRouter>
<Routes>
<Route path={"/"} element={<Menu />}>
<Route element={<div>新闻动态页面</div>} path={"/news"} />
<Route element={<div>关于我们页面</div>} path={"/about"} />
<Route element={<div>热点事件页面</div>} path={"/hot"} />
</Route>
</Routes>
</BrowserRouter>
</div>
);
}
export default App;
提供的一些 use API
- useRoutes “根据路由配置动态生成路由树,并返回与当前路径匹配的组件。”
- useParams “获取当前路径中的动态参数,返回一个对象包含参数及其值。”
- useLocation “返回当前的 location 对象,包括路径、查询字符串和哈希值。”
- useNavigate “提供编程式导航功能,允许在应用内进行导航操作。”
import {
BrowserRouter,
Link,
NavLink,
Outlet,
Route,
Routes,
useLocation,
useNavigate,
useParams,
useRoutes,
} from "react-router-dom";
import { lazy, Suspense } from "react";
import About from "./pages/About";
// 类似 Vue 配置
const NavMenu = ({ to, children }) => {
return (
<NavLink to={to} className={(isActive) => (isActive ? "nav-active" : "")}>
{children}
</NavLink>
);
};
const Menu = () => (
<div>
<header>
{ /*Link标签的好处: 页面不会再刷新了, network 不会再向服务端请求html资源*/ }
<NavLink to="/">首页</NavLink>
<NavLink to="/news">新闻列表</NavLink>
<NavLink to="/about">关于我们</NavLink>
<NavLink to="/hot">热点事件</NavLink>
</header>
<Outlet />
</div>
);
const List = () => {
// 导航去新闻1
const navigate = useNavigate();
return (
<div>
<button onClick={() => navigate("/post/1")}>去新闻1</button>
</div>
);
};
const Post = () => {
const { id } = useParams();
const location = useLocation();
console.log(`打印location信息`, location);
//{
// "pathname": "/post/1",
// "search": "",
// "hash": "",
// "state": null,
// "key": "io19jk10"
//}
return <div>这里是新闻页, 你当前看的是id为 {id} 的新闻。 </div>;
};
const DynamicAbout = lazy(() => import("./pages/About"));
const routes = [
{
path: "/",
element: <Menu />,
children: [
{
path: "/news",
element: <List />,
},
{
path: "/post/:id",
element: <Post />,
},
{
path: "/about",
// 异步路由
element: (
<Suspense fallback={<div>loading</div>}>
<DynamicAbout />
</Suspense>
),
},
{
path: "/hot",
element: <div>hot</div>,
},
],
},
];
const Routering = () => useRoutes(routes);
export default function App() {
return (
<BrowserRouter>
<Routering />
</BrowserRouter>
);
}
const routes = [{path, element,...}]
const Routering = useRoutes(routes)
对应的是
<Routes>
<Route element={} path={} />
<Route element={} path={} />
</Routes>
Routering 本质上就是 Routes 中的内容
Routes 其实就是把所有的 , 通过一个函数 createRoutesFromChildren 创建成一棵树
实现一个路由
实现一个自定义的 React 路由系统,支持 HashRouter 和 BrowserRouter 两种路由模式。
通过创建 NavigationContext 和 LocationContext 上下文来管理和传递路由状态。
利用 useLocation 和 useNavigate 钩子提供路由信息和导航功能,useRoutes 函数匹配当前路径与定义的路由配置,并渲染对应的组件,同时 createRoutesFromChildren 函数将嵌套的 Route 组件转换为路由配置,从而支持复杂的路由结构和动态路由匹配。
核心代码
import React, { createContext, useContext, useLayoutEffect, useMemo, useRef, useState } from "react";
import { createHashHistory, createBrowserHistory } from "history";
// 创建上下文
const NavigationContext = createContext({});
const LocationContext = createContext({});
export function HashRouter({ children }) {
// 我要保证, 这个东西不用频繁创建
let historyRef = useRef();
if (historyRef.current == null) {
historyRef.current = createHashHistory();
}
let history = historyRef.current;
const [state, setState] = useState({
action: history?.action,
location: history?.location,
});
// 监听 history 的变化, 当 history 发生变化的时候, 利用 React 的副作用的能力
// 和更新的能力去把最新的 history 的值往下传递
useLayoutEffect(() => history.listen(setState), [history]);
return <Router children={children} location={state.location} navigationType={state.action} navigator={history} />;
}
export function BroswerRouter({ children }) {
// 我要保证, 这个东西不用频繁创建
let historyRef = useRef();
if (historyRef.current == null) {
historyRef.current = createBrowserHistory();
}
let history = historyRef.current;
const [state, setState] = useState({
action: history.action,
location: history.location,
});
// 监听 history 的变化, 当 history 发生变化的时候, 利用 React 的副作用的能力
// 和更新的能力去把最新的 history 的值往下传递
useLayoutEffect(() => history.listen(setState), [history]);
return <Router children={children} location={state.location} navigationType={state.action} navigator={history} />;
}
function Router({ children, location, navigator }) {
const navigationValue = useMemo(() => ({ navigator }), [navigator]);
const locationValue = useMemo(() => ({ location }), [location]);
return (
<NavigationContext.Provider value={navigationValue}>
<LocationContext.Provider value={locationValue} children={children}></LocationContext.Provider>
</NavigationContext.Provider>
);
}
// useLocation
function useLocation() {
return useContext(LocationContext).location;
}
function useNavigate() {
return useContext(NavigationContext).navigator;
}
// 我要做的事,就是 path 是什么, 当前我要给到什么 element
function useRoutes(routes) {
let location = useLocation(); // 当前的路径
let currentPath = location.pathname || "/";
console.log(location);
// 当前浏览器中的 currentPath 和 routes 中的 path 哪个匹配
// 匹配之后渲染对应的element
for (let i = 0; i < routes.length; i++) {
let { path, element } = routes[i];
let match = currentPath.match(new RegExp(`^${path}`));
if (match) return element;
}
return null;
}
function createRoutesFromChildren(children) {
let routes = [];
// React 官方提供的 foreach
React.Children.forEach(children, (node) => {
let route = {
element: node.props.element,
path: node.props.path,
};
if (node.props.children) {
route.children = createRoutesFromChildren(node.props.children);
}
routes.push(route);
});
console.log(routes);
return routes;
}
export const Routes = ({ children }) => useRoutes(createRoutesFromChildren(children));
export const Route = () => {};
App 应用
import { BroswerRouter, Route, Routes } from "./router/router";
export default function App() {
return (
<div>
<BroswerRouter>
<header>
<a href={"/news"}>news</a>
<a href={"/about"}>about</a>
<a href={"/hot"}>hot</a>
</header>
<Routes>
<Route path={"/news"} element={<div>news</div>}></Route>
<Route path={"/about"} element={<div>about</div>}></Route>
<Route path={"/hot"} element={<div>hot</div>}></Route>
</Routes>
</BroswerRouter>
</div>
);
}
扩展: 打包的两种方式
打包的两种方式: bundled(最小加载)和 undleless(最小使用)
结语
前端路由的演变让我们的应用变得更加灵活和强大。无论你是用 React Router 还是 Vue Router,了解这些路由技术能让你更得心应手地管理应用的导航。希望这篇文章能帮助你选择最适合你的路由方案,也希望你在实际开发中能玩转这些工具,打造出更流畅、用户体验更佳的应用。