实现一个简易 react-router

94 阅读3分钟

引言

实现一个简单的路由系统,首先看下官方用法,最外层使用 BrowserRouter 来包裹,Routes 里面使用 Route 书写 path 和 element 的对应关系,完整代码

// Index.js
import {
  Link,
  Route,
  Routes,
  BrowserRouter,
} from "react-router-dom";

const About = () => {
  return <div>about</div>;
};

const Version = () => {
  return <div>version</div>;
};

const Index = () => {
  return (
    <BrowserRouter>
      <Link to="/about">去关于页面</Link>
      <hr />
      <Link to="/version">去版本页面</Link>
      <hr />
      <Routes>
        <Route path="/about" element={<About />}></Route>
        <Route path="/version" element={<Version />}></Route>
      </Routes>
    </BrowserRouter>
  );
};

export default Index;

实战

需要准备一些物料,history 是对浏览器 history 对象的增强,后续也会用到

"react": "^18.3.1",
"react-dom": "^18.3.1",
"history": "^5.3.0"

分析

其实路由的本质就是,根据当前 path,去路由表里面查找对应的组件,然后执行组件的挂载与销毁,综上所述,需要实现三步

  1. 实现一个函数,入参为 react 组件,出参是路由表,便于后续查找 path 与 element 的对应关系

  2. 实现 Routes 组件,根据 path 返回对应的 element,借鉴 useRoutes hook

  3. 实现 BrowserRouter 组件,主要负责更新界面,派发相应的 history,便于第二步使用

childrenToConfig

先搭建一个框架,代码如下

import React from 'react'

const BrowserRouter = ({ children }) => {
    // ...
};

// 第一步,将 dom 变成 config
const childrenToConfig = (tree) => {
  const res = [];
  // 使用 React 内置的遍历方法
  React.Children.forEach(tree, (node) => {
    // 记录 path 和 element
    const route = {
      path: node.props.path,
      element: node.props.element,
    };
    // 递归处理
    if (node.props.children) {
      route.children = childrenToConfig(node.props.children);
    }
    res.push(route);
  });
  // 返回路由表
  return res;
};

const Routes = ({ children }) => {
    const routeMap = childrenToConfig(children)
    // ...
};

const Route = () => {
    // ...
};

export { BrowserRouter, Routes, Route };

Routes

Routes 组件可以理解为一个占位符,其永远返回当前 path 下的 element,现在有了路由表,还需要一个函数来根据当前 path,返回对应的 element,称之为 useRoutes

 // 第二步,拿到当前 pathname,去路由表里面找对应的 element,返回,useLocation 待实现
const useRoutes = (config) => {
  // 假设 useLocation 已经实现
  const location = useLocation();
  const path = location.pathname || "";
    
  // 递归查找 element
  const find = (arr) => {
    for (let c of arr) {
      if (c.path === path) {
        return c.element;
      }
      if (c.children) {
        const el = find(c.children);
        if (el) {
          return el;
        }
      }
    }
  };
  return find(config) || null;
}

const Routes = ({ children }) => {
    return useRoutes(childrenToConfig(children))
}

BrowserRouter

该组件主要负责维护、派发、更新状态,包括上面的 useLocation,代码如下

const LocationContext = createContext({});
const BrowserRouter = ({ children }) => {
    // 防止后续多次创建,使用 useRef 缓存
    const historyRef = useRef()
    if (!historyRef.current) {
        historyRef.current = createBrowserHistory()
    }
    const history = historyRef.current
    // 存储 location
    const [state, setState] = useState({
        location: history.location
    })
    
    // history 变化时,重新渲染
    useEffect(() => {
        history.listen(setState)
    }, [history])
    
    // 这里的 children 就是要渲染的组件,也就是 Routes 组件的返回值
    return (
        <LocationContext.Provider 
            value={history}
            children={children}
        >
        </LocationContext.Provider>
    )
}
const useLocation = () => useContext(LocationContext)

总结

整体流程如下图所示 image.png

完整代码如下

// router.js
import React, {
  useEffect,
  useRef,
  useState,
  createContext,
  useContext,
  useMemo,
} from "react";
import { createBrowserHistory } from "history";

const LocationContext = createContext({});
const HistoryContext = createContext({});

const BrowserRouter = ({ children }) => {
  const historyRef = useRef();
  if (!historyRef.current) {
    historyRef.current = createBrowserHistory();
  }
  const history = historyRef.current;
  const [state, setState] = useState({
    location: history.location,
  });

  useEffect(() => {
    history.listen(setState);
  }, [history]);
  return (
    <Router children={children} location={state.location} history={history} />
  );
};

const Router = ({ children, location, history }) => {
  // 缓存 location 和 history 的结果,并且派发 context
  const locationValue = useMemo(() => location, [location]);
  const historyValue = useMemo(() => history, [history]);
  return (
    <LocationContext.Provider value={locationValue}>
      <HistoryContext.Provider
        value={historyValue}
        children={children}
      ></HistoryContext.Provider>
    </LocationContext.Provider>
  );
};

const useLocation = () => {
  return useContext(LocationContext);
};

// 第二步,拿到当前 pathname,去路由表里面找对应的 element,返回
const useRoutes = (config) => {
  const location = useLocation();
  const path = location.pathname || "";

  const find = (arr) => {
    for (let c of arr) {
      if (c.path === path) {
        return c.element;
      }
      if (c.children) {
        const el = find(c.children);
        if (el) {
          return el;
        }
      }
    }
  };
  return find(config) || null;
};

// 第一步,将 dom 变成 config,传给 useRoutes
const childrenToConfig = (tree) => {
  const res = [];
  React.Children.forEach(tree, (node) => {
    const route = {
      path: node.props.path,
      element: node.props.element,
    };
    if (node.props.children) {
      route.children = childrenToConfig(node.props.children);
    }
    res.push(route);
  });
  return res;
};
const Routes = ({ children }) => {
  return useRoutes(childrenToConfig(children));
};

const Route = () => {};

export { BrowserRouter, Routes, Route };

使用方代码如下

// Index.js
import { BrowserRouter , Routes , Route } from  "./router.jsx" ;

const About = () => {
  return <div>about</div>;
};

const Version = () => {
  return <div>version</div>;
};

const Index = () => {
  return (
    <BrowserRouter>
      <a href="/about">去关于页面</a>
      <hr />
      <a href="/version">去版本页面</a>
      <hr />
      <Routes>
        <Route path="/about" element={<About />}></Route>
        <Route path="/version" element={<Version />}></Route>
      </Routes>
    </BrowserRouter>
  );
};

export default Index;