4-4 React 路由管理

47 阅读4分钟

原文链接(格式更好):《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>