简单实现 react-router-dom 的 BrowserRouter

150 阅读1分钟

一、前置内容

1.history API

History 接口允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录。

2.Path-to-RegExp

Turn a path string such as /user/:name into a regular expression.

npm install path-to-regexp --save

二、项目目录

├── src/                                // 源码目录
    ├── components/                     // 组件目录
        ├── BrowserRouter.tsx         
        ├── Link.tsx
        ├── Route.tsx
        ├── Router.tsx
        ├── Switch.tsx
    ├── context/                        // React.Context 目录
        ├── RouterContext.tsx
    ├── pages/                          // 页面目录
        ├── Home.tsx
        ├── About.tsx
        ├── Login.tsx
    ├── App.tsx
     

三、组件代码

1. Router

// src/components/BrowserRouter.tsx
import { PropsWithChildren } from "react";
import { RouterProvider } from "../context/RouterContext";

const Router = ({ children }: PropsWithChildren) => {
  return <RouterProvider>{children}</RouterProvider>;
};

export default Router;

2. BrowserRouter

// src/components/BrowserRouter.tsx
import { PropsWithChildren } from "react";
import Router from "./Router";

const BrowserRouter = ({ children }: PropsWithChildren) => {
  return <Router>{children}</Router>;
};

export default BrowserRouter;

3. Switch

// src/components/Switch.tsx
import { Children, isValidElement, PropsWithChildren, useContext } from "react";
import { RouterContext } from "../context/RouterProvider";
import { IRoute } from "./Route";
import * as ptr from "path-to-regexp";
import type { Match, ParamData } from "path-to-regexp";

type MatchType =
  | (Exclude<Match<ParamData>, false> & { exact?: boolean; url: string })
  | undefined;

const Switch = (props: PropsWithChildren) => {
  const { pathname } = useContext(RouterContext);

  let match: MatchType = undefined;
  let Cmpt!: IRoute["component"];

  if (props.children) {
    Children.forEach(props.children, (item) => {
      if (!match && isValidElement(item)) {
        const childProps = item.props as IRoute;
        const { path, exact = false, component } = childProps;
        const result = ptr.match(path, { end: exact })(pathname);

        Cmpt = component;

        if (result) {
          match = {
            ...result,
            exact,
            url: "http://",
          };
        }
      }
    });
  }

  return match ? <Cmpt /> : null;
};

export default Switch;

4. Route

// src/components/Route.tsx
import { Component, FC, PropsWithChildren } from "react";

export interface IRoute {
  path: string;
  component: FC | typeof Component;
  exact?: boolean;
}

const Route = (props: PropsWithChildren<IRoute>) => {
  return null;
};

export default Route;

5. Link

// src/components/Link.tsx
import { PropsWithChildren, SyntheticEvent } from "react";

interface ILink {
  to: string;
}

const Link = ({ children, to }: PropsWithChildren<ILink>) => {
  const onClick = (e: SyntheticEvent) => {
    // 阻止默认行为
    e.preventDefault();
    history.pushState({}, "", to);

    const popStateEvent = new PopStateEvent("popstate");
    dispatchEvent(popStateEvent);
  };

  return (
    <a
      style={{
        textDecoration: "underline",
        lineHeight: 2,
        cursor: "pointer",
      }}
      onClick={onClick}
    >
      {children}
    </a>
  );
};

export default Link;

5. RouterContext

// src/context/RouterContext.tsx
import { createContext, PropsWithChildren, useEffect, useState } from "react";

interface IRouterContext {
  pathname: string;
}

export const RouterContext = createContext<IRouterContext>(undefined!);

export const RouterProvider = ({ children }: PropsWithChildren) => {
  const [value, setValue] = useState({
    pathname: window.location.pathname,
  });

  const onPopstate = () => {
    setValue({
      ...value,
      pathname: window.location.pathname,
    });
  };

  const mount = () => {
    window.addEventListener("popstate", onPopstate);
  };

  const unmount = () => {
    window.removeEventListener("popstate", onPopstate);
  };

  useEffect(() => {
    mount();
    return unmount;
  }, []);

  return (
    <RouterContext.Provider value={value}>{children}</RouterContext.Provider>
  );
};

6. Home

// src/pages/Home.tsx
const Home = () => {
  return <div>Home</div>;
};

export default Home;

7. App

// src/App.tsx
import Router from "./components/BrowserRouter";
import Link from "./components/Link";
import Route from "./components/Route";
import Switch from "./components/Switch";
import About from "./pages/About";
import Home from "./pages/Home";
import Login from "./pages/Login";

function App() {
  return (
    <div>
      <Router>
        <Link to="/home">home</Link>&nbsp;&nbsp;
        <Link to="/about">about</Link>&nbsp;&nbsp;
        <Link to="/login">login</Link>
        <Switch>
          <Route path="/login" component={Login} />
          <Route path="/home" component={Home} />
          <Route path="/about" component={About} />
        </Switch>
      </Router>
    </div>
  );
}

export default App;