从头构建一个 react-router 克隆

2025-05-28

从头构建一个 react-router 克隆

React Router 是我几乎所有项目都会用到的一个包。不久前
,迈克尔·杰克逊发了一条推文。这让我很好奇,从头开始重新构建 React Router 到底有多难。

在深入探讨之前,我想先澄清一下,如果你的项目需要路由器,你应该直接使用 React Router。它功能更丰富,能处理更多边缘情况,而且测试非常完善。这纯粹是一次学习练习。

在这篇文章中,我们将构建一个基于较新的 v6 API 的简化版本的 react-router 。

React Router 的核心是一个名为“history”的包。这个包负责管理路由器的历史记录。在本文中,我们只关注如何为 Web 创建一个路由器,因此我们将把它直接嵌入到 React 组件中。我们首先需要一个根Router组件和一个供其他组件使用的上下文。让我们从上下文开始。

我们的路由器将比 React Router 更加简化,因为我们将不再支持位置状态、哈希值以及 React Router 提供的其他情况。我们的路由器上下文将提供两个键:location 和 push:

  • 位置只是当前路径的字符串。
  • push 是一个可以调用来改变当前路径的函数。

这样我们就可以创建我们的基本路由器上下文。

const RouterContext = React.createContext({
  location: "",
  push: () => {},
});
Enter fullscreen mode Exit fullscreen mode

如果没有渲染提供程序,此上下文将毫无用处。我们将在主Router组件内部实现此功能。此组件的职责是提供当前路由的相关信息,并提供操作方法。我们将在 React 状态中存储当前位置路径。这样,当我们更新位置时,组件将重新渲染。我们还需要push为上下文提供一个函数,该函数将简单地更新浏览器位置并更新我们的位置状态。最后,我们还监听窗口的“popstate”事件,以便在使用浏览器导航按钮时更新我们的位置。

function Router({ children }) {
  const [location, setLocation] = React.useState(window.location.pathname);

  const handlePush = useCallback(
    (newLocation) => {
      window.history.pushState({}, "", newLocation);
      setLocation(newLocation);
    },
    []
  );

  const handleHashChange = useCallback(() => {
    setLocation(window.location.pathname);
  }, []);

  useEffect(() => {
    window.addEventListener("popstate", handleHashChange);
    return () => window.removeEventListener("popstate", handleHashChange);
  }, [handleHashChange]);

  const value = useMemo(() => {
    return { location, push: handlePush }
  }, [location, handlePush])

  return (
    <RouterContext.Provider value={value}>
      {children}
    </RouterContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

为了测试我们的组件,我们需要一种方法来更新当前路由,以检查组件是否渲染正确。让我们Link为此创建一个组件。我们的链接组件只需接受to新路径作为参数,并push在点击时从路由器上下文中调用我们的函数。

function Link({ to, children }) {
  const { push } = React.useContext(RouterContext);

  function handleClick(e) {
    e.preventDefault();
    push(to);
  }

  return (
    <a href={to} onClick={handleClick}>
      {children}
    </a>
  );
}
Enter fullscreen mode Exit fullscreen mode

现在我们有了导航的方法,接下来我们需要一个方法来渲染一些路由!让我们创建一个RoutesandRoute组件来处理这个问题。我们从Route组件开始,因为它需要做的就是渲染我们传入的子元素。

function Route({ children }) {
  return children;
}
Enter fullscreen mode Exit fullscreen mode

接下来我们需要Routes组件。我们需要遍历路由组件,找到一个与当前位置匹配的组件。我们还需要在路由上下文中渲染匹配的路由,以便路由子节点可以访问路径中匹配的任何参数。首先,我们需要创建匹配路由所需的函数。首先,我们需要一个函数,它接收路由的 path 属性,并将其转换为正则表达式,以便我们匹配当前位置。

function compilePath(path) {
  const keys = [];

  path = path.replace(/:(\w+)/g, (_, key) => {
    keys.push(key);
    return "([^\\/]+)";
  });

  const source = `^(${path})`;

  const regex = new RegExp(source, "i");
  return { regex, keys };
}
Enter fullscreen mode Exit fullscreen mode

这还将为我们提供一个表示路径模式中任何参数的键数组。

compilePath("/posts/:id");
// => { regex: /^(/posts/([^\/]+))/i, keys: ["id"] }
Enter fullscreen mode Exit fullscreen mode

接下来我们需要一个新函数,它将遍历每个子路由并使用该compilePath函数测试它是否与当前位置匹配,同时提取任何匹配的参数。

function matchRoutes(children, location) {
  const matches = [];

  React.Children.forEach(children, (route) => {
    const { regex, keys } = compilePath(route.props.path);
    const match = location.match(regex);

    if (match) {
      const params = match.slice(2);
      matches.push({
        route: route.props.children,
        params: keys.reduce((collection, param, index) => {
          collection[param] = params[index];
          return collection;
        }, {}),
      });
    }
  });

  return matches[0];
}
Enter fullscreen mode Exit fullscreen mode

最后,我们可以创建一个新的RouteContext路由组件并将其组合起来。我们将提供的子组件传递给matchRoutes函数,以查找匹配的路由,并将其渲染到路由上下文的提供程序中。

const RouteContext = React.createContext({
  params: {},
});

function Routes({ children }) {
  const { location } = useContext(RouterContext);
  const match = useMemo(() => matchRoutes(children, location), [
    children,
    location,
  ]);

  const value = useMemo(() => {
    return { params: match.params }
  }, [match])

  // if no routes matched then render null
  if (!match) return null;

  return (
    <RouteContext.Provider value={value}>
      {match.route}
    </RouteContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

至此,我们实际上已经拥有了一个可以正常运行的路由器,然而,我们还缺少一个虽小却至关重要的部分。每个优秀的路由器都需要一种从 URL 中提取参数的方法。借助我们的钩子,RouteContext我们可以轻松地创建一个useParams钩子,供我们的路由提取这些参数。

function useParams() {
  return useContext(RouteContext).params;
}
Enter fullscreen mode Exit fullscreen mode

有了这些,我们就有了自己的 React Router 的基本工作版本!

function Products() {
  return (
    <>
      <h4>Example Products</h4>
      <ul>
        <li>
          <Link to="/products/1">Product One</Link>
        </li>
        <li>
          <Link to="/products/2">Product Two</Link>
        </li>
      </ul>
    </>
  );
}

function Product() {
  const { id } = useParams();
  return (
    <>
      <h4>Viewing product {id}</h4>
      <Link to="/">Back to all products</Link>
    </>
  );
}

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/products/:id">
          <Product />
        </Route>
        <Route path="/">
          <Products />
        </Route>
      </Routes>
    </Router>
  );
}
Enter fullscreen mode Exit fullscreen mode
文章来源:https://dev.to/thomascullen/build-a-react-router-clone-from-scratch-38dp
PREV
超过 20 个资源助您提升 JavaScript 技能
NEXT
如果我不得不重新开始编程,我会做不同的事情!不要害怕寻求帮助 休息非常重要 除了编程之外,追求其他东西 不要因为别人的工作而给自己太大压力 不要把感激与负债混为一谈 与其他开发人员合作 🤝 做不完美的工作也没关系 最后但同样重要的一点:享受吧!!!🎉💃 一些总结: