原文链接(格式更好):《4-4 React 路由管理》
路由的演变
之前,部署到服务器的前端项目是由多个 HTML 文件组成,每个 HTML 都有对应服务器路径,前端称其为路由,路由之间使用location.href
跳转,跳转路径就是另一个 HTML 的服务器地址。这时候的路由是由后端来管理的
后面单页应用流行,部署到服务器的前端项目就只有一个 HTML 文件,对应一个服务器路径。这时候为满足不同页面的展示,就需要借助框架提供的路由能力,至此路由的管理转移到前端身上。
路由的组成
即location
的组成:
location.protocal
协议
location.host
域名
location.port
端口(多数省略了)
location.pathname
路径
location.search
参数,[? 后面,# 之前)的内容
location.hash
锚点,# 后面的内容
路由的分类
单页应用下,分为:hash、history
hash:
路由上带 #,内容为 # 后面,用它来区分页面;
不需要服务端配合。
history:
路由上不带 #,内容为[域名后面,? 之前),用它来区分页面;
需要服务端配合。因为部署到服务器后,该模式实际上访问服务器的资源,但单页应用只有一个指向 html 的路径,所以这样访问会返回 404,一般需要配置让其指向 html 的路径
路由实现的核心原理
核心原理:监听路径的变化,找到该路径对应的组件,然后渲染到相应位置,并注入 router 等上下文。其中的对应关系就是我们常写的路由配置项。
react-router
官网:React Router
基本使用
import "./App.css";
import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom";
const Menu = () => (
<div>
<header>
<ul style={{ display: "flex" }}>
<a href="/">首页</a>
<span style={{ margin: "0 10px" }}>|</span>
<a href="/list">新闻列表</a>
<span style={{ margin: "0 10px" }}>|</span>
<a href="/about">关于我们</a>
</ul>
</header>
<Outlet />
</div>
);
function App() {
return (
<BrowserRouter>
<Menu />
<Routes>
<Route path="/" element={<div>首页 page</div>}></Route>
<Route path="/list" element={<div>新闻列表 page</div>}></Route>
<Route path="/about" element={<div>关于我们 page</div>}></Route>
</Routes>
</BrowserRouter>
);
}
export default App;
基本原理(手撸简版)
简单手撸react-router-dom
核心原理
/*
<Routes>
<Route path="/" element={<Menu />}>
<Route path="/" element={<div>首页 page</div>}></Route>
<Route path="/list" element={<div>新闻列表 page</div>}></Route>
<Route path="/about" element={<div>关于我们 page</div>}></Route>
</Route>
</Routes>
*/
// 上述代码 等价于:
/*
const routes = [
{
path: "/",
element: <Menu />,
children: [
{
path: "/",
element: <div>首页 page</div>,
},
{
path: "/list",
element: <div>新闻列表 page</div>,
},
{
path: "/about",
element: <div>关于我们 page</div>,
},
],
},
];
const Routeing = useRoutes(routes);
*/
import React from "react";
const LocationContext = React.createContext({});
const NavigationContext = React.createContext({});
/**
* BrowserRouter 是一个基于 React 的路由器组件,用于在浏览器中导航。
* 它接收一个 children 属性,该属性是一个 React 元素,表示要渲染的组件。
* 它返回一个包含 LocationContext 和 NavigationContext 的组件,这两个上下文提供了有关当前位置和导航器的信息。
*
* @param {object} props - 包含 children 属性的对象。
* @returns {ReactElement} - 返回一个包含 LocationContext 和 NavigationContext 的组件。
*/
export function BrowserRouter({ children }) {
return (
// 创建一个 LocationContext.Provider 组件,并设置其值为一个对象,该对象包含 location 属性,值为 window.location
<LocationContext.Provider value={{ location: window.location }}>
<NavigationContext.Provider value={{ navigator: window.history }}>
{children}
</NavigationContext.Provider>
</LocationContext.Provider>
);
}
export function useLocation() {
return React.useContext(LocationContext).location;
}
export function useNavigation() {
return React.useContext(NavigationContext).navigator;
}
export function findRoute(routes, pathname) {
routes.find(({ path }) => path === pathname);
return;
}
/**
* 使用路由
*
* @param routes 路由列表
* @returns 父级路由的元素,与路由匹配到的渲染组件
*/
export function useRoutes(routes) {
// 获取当前位置信息
const location = useLocation();
// 获取当前路径
const pathname = location.pathname || "/";
// 在路由列表中查找当前路径对应的路由
const parentRoute = findRoute(routes, pathname);
// 返回父级路由的元素
return parentRoute?.element;
}
/**
* 将子节点转换为路由对象数组
*
* @param children 子节点
* @returns 路由对象数组
*/
export function childrenToRoutes(children) {
const routes = [];
// 遍历子节点
React.Children.forEach(children, (node) => {
// 提取节点路径和元素
const { path, element } = node.props;
// 构建路由对象
const route = { path, element };
// 如果节点有子节点,递归调用childrenToRoutes函数
if (node.props.children) {
route.children = childrenToRoutes(node.props.children);
}
// 将路由对象添加到routes数组中
routes.push(route);
});
// 返回routes数组
return routes;
}
/**
* 定义路由组件
*
* @param children 组件列表
* @returns 使用路由组件返回结果
*/
export function Routes({ children }) {
return useRoutes(childrenToRoutes(children));
}
手撸 Router
核心原理:监听路径的变化,找到该路径对应的组件,然后渲染到相应位置,并注入 router 等上下文。其中的对应关系就是我们常写的路由配置项。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>base-index-html</title>
</head>
<body>
<div>
<nav>
<a href="#">首页</a>
<a href="#about">关于我们</a>
<a href="#list">新闻列表</a>
<a href="#post">新闻详情</a>
</nav>
<section>
<div class="router-view"></div>
</section>
</div>
<script>
class Router {
constructor(routes) {
this.routes = routes;
this.init();
}
init() {
window.addEventListener("hashchange", () => this.onHashChange());
window.addEventListener("load", () => this.onHashChange());
}
onHashChange() {
const hash = window.location.hash.slice(1);
const route = this.findRoute(hash);
this.updateView(route);
}
findRoute(hash) {
return this.routes.find((route) => route.path === "/" + hash);
}
updateView(route) {
const viewEle = document.querySelector(".router-view");
viewEle.innerHTML = route ? route.element : "404";
}
push(path) {
window.location.hash = path.slice(1);
}
}
const routes = [
{
path: "/",
element: `
<div>
<div>首页 page</div>
<button onclick="router.push('/about')">去 about</button>
</div>
`,
},
{
path: "/list",
element: `
<div>
<div>新闻列表 page</div>
</div>
`,
},
{
path: "/about",
element: `
<div>
<div>关于我们 page</div>
</div>
`,
},
{
path: "/post",
element: `
<div>
<div>新闻详情 page</div>
</div>
`,
},
];
const router = new Router(routes);
</script>
</body>
</html>