A fork in the road in the middle of the woods
2021 年 11 月 3 日

React Router v6

迈克尔·杰克逊
联合创始人

今天,我们非常高兴地宣布 React Router v6 的稳定版本发布。

这个版本已经酝酿了很久。我们上次发布重大 API 更改是在 2017 年 3 月,当时我们发布了 v4 版本。其中有些人可能那时还没出生。不用说,从那时起发生了很多事情。

  • React Router 的下载量增长了 60 倍以上(6000%),从 2017 年 3 月的每月 34 万次增长到 2021 年 10 月的每月 2100 万次。
  • 我们发布了 v5 版本,没有任何重大更改(我已经在 其他地方 写过关于主要版本升级的原因)。
  • 我们发布了 Reach Router,目前每月大约有 1300 万次下载。
  • React Hooks 诞生了。
  • COVID-19。

我可以轻松地写几页关于上面每个要点及其对我们业务和自 2014 年以来一直管理的开源项目的意义。但我不想让你厌烦过去。我们过去几年经历了很多。其中一些很艰难,但希望你也有所成长。我们当然有。事实上,我们彻底改变了我们的业务模式!

今天我想关注未来,以及我们如何利用过去的经验来构建 React Router 项目和不可思议的 React 社区的最强大的未来。将会有代码。但我们也会谈论业务以及你对我们的期望(提示:它 非常多彩)。

为什么还要发布一个主要版本?

发布一个新的路由器版本的最主要原因是 React Hooks 的出现。你可能还记得 Ryan 的演讲,他在 2018 年的 React Conf 上向世界介绍了 Hooks,以及我们使用 React 的“生命周期方法”编写的大量代码如何在将基于类的 React 代码重构为 Hooks 时消失。如果你不记得那个演讲,你可能应该在这里停下来去看一下。我会等你的。

虽然我们在 v5.1 中将一些 Hooks 添加到了 v5,但 React Router v6 是使用 React Hooks 从头开始构建的。它们是如此高效的低级原语,我们能够通过提供执行此工作的 Hooks 来消除大量样板代码。这意味着你的 v6 代码将比你的 v5 代码更紧凑、更优雅。

此外,不仅仅是你的代码变得更小、更高效……我们的代码也是!我们的 缩小的 gzip 捆绑包大小 在 v6 中减少了 50% 以上!React Router 现在只会在你的应用程序捆绑包中增加不到 4kb,在你使用树摇功能通过捆绑器运行它之后,你实际的结果会更小。

可组合的路由器

为了展示 Hooks 如何在 v6 中改进你的代码,让我们从访问当前 URL 路径名的参数这样简单的事情开始。React Router v6 提供了 一个 useParams() Hook(在 5.1 中也有),它允许你访问当前 URL 参数,无论你在哪里需要它们。

import { Routes, Route, useParams } from "react-router-dom";

function App() {
  return (
    <Routes>
      <Route path="blog/:id" element={<BlogPost />} />
    </Routes>
  );
}

function BlogPost() {
  // You can access the params here...
  let { id } = useParams();
  return (
    <>
      <PostHeader />
      {/* ... */}
    </>
  );
}

function PostHeader() {
  // or here. Just call the hook wherever you need it.
  let { id } = useParams();
}

现在将这个简单的例子与你在 v5 或更早版本中使用渲染道具或高阶组件来做同样的事情的方式进行对比。

// React Router v5 code
import * as React from "react";
import { Switch, Route } from "react-router-dom";

class App extends React.Component {
  render() {
    return (
      <Switch>
        <Route
          path="blog/:id"
          render={({ match }) => (
            // Manually forward the prop to the <BlogPost>...
            <BlogPost id={match.params.id} />
          )}
        />
      </Switch>
    );
  }
}

class BlogPost extends React.Component {
  render() {
    return (
      <>
        {/* ...and manually pass props down to children... booo */}
        <PostHeader id={this.props.id} />
      </>
    );
  }
}

Hooks 消除了使用 <Route render> 访问路由器内部状态(match以及手动传递 props 来将该状态传播到子组件的需要。

换句话说,可以将 useParams() 看作是路由器上下文中的 useState()。路由器知道一些状态(当前 URL 参数),并允许你随时使用 Hook 访问它。如果没有 Hook,我们需要一种方法来手动将状态转发到树中较低位置的元素。

让我们再看一个快速示例,说明 Hooks 如何使 React Router v6 比 v5 强大得多。假设你想要在当前位置发生更改时向你的分析服务发送“页面浏览”事件。在 v6 中,useLocation() Hook 已经为你准备好了。

import { useEffect } from "react";
import { useLocation } from "react-router-dom";

function App() {
  let location = useLocation();
  useEffect(() => {
    window.ga("set", "page", location.pathname + location.search);
    window.ga("send", "pageview");
  }, [location]);
}

当然,由于 Hooks 提供的功能组合,你可能只想将所有这些封装成一个 Hook,例如

import { useAnalyticsTracking } from "./analytics";

function App() {
  useAnalyticsTracking();
  // ...
}

同样,在一个没有 Hooks 的世界中,你必须做一些奇怪的事情,比如渲染一个独立的 <Route path="/"> 来渲染 null,这样你才能访问 location 以及它的变化。此外,如果没有 useEffect() 来触发副作用,你必须执行 componentDidMount + componentDidUpdate 这样的舞蹈,以确保你只在 location 发生更改时才发送页面浏览事件。

// React Router v5 code
import * as React from "react";
import { Switch, Route } from "react-router-dom";

class PageviewTracker extends React.Component {
  trackPageview() {
    let { location } = this.props;
    window.ga("set", "page", location.pathname + location.search);
    window.ga("send", "pageview");
  }

  componentDidMount() {
    this.trackPageview();
  }

  componentDidUpdate(prevProps) {
    if (prevProps.location !== this.props.location) {
      this.trackPageview();
    }
  }

  render() {
    return null; // lol
  }
}

class App extends React.Component {
  return (
    <>
      {/* This route isn't really a piece of the UI, it's just here
          so we can access the current location... */}
      <Route path="/" component={PageviewTracker} />

      <Switch>
        {/* your actual routes... */}
      </Switch>
    </>
  );
}

那段代码很疯狂,对吧?好吧,当你没有 Hooks 时,你必须使用这些花招。

总之,我们发布了 React Router 的一个新主要版本,这样你就可以发布更小、更高效的应用程序,进而带来更好的用户体验。就这么简单。

你可以查看我们 API 文档中列出的 v6 中所有可用的 Hooks

仍在使用 React.Component?别担心,我们仍然支持类组件!查看此 GitHub 线程以获取更多信息

路由改进

还记得 react-nested-router 吗?可能不记得。但这就是我们在 npm 上获得 react-router 包名称之前对 React Router 的称呼(谢谢,Jared!)。React Router 一直都是关于嵌套路由的,尽管我们表达它们的方式随着时间的推移略有变化。我会向你展示我们在 v6 中想出的方法,但首先让我先给你讲一下 v3、v4/5 和 Reach Router 的一些背景故事。

在 v3 中,我们将 <Route> 元素直接嵌套在一个巨大的路由配置中,就像 这个例子 一样。嵌套的 <Route> 元素是可视化整个路由层次结构的好方法。但是,我们在 v3 中使用的实现使得代码拆分变得很困难,因为所有路由组件最终都出现在同一个捆绑包中(这发生在 React.lazy() 出现之前)。因此,随着你添加的路由越来越多,你的捆绑包也会不断增长。此外,<Route component> 属性使得将自定义 props 传递到组件变得很困难。

在 v4 中,我们针对大型应用程序进行了优化。代码拆分!在 v4 中,你不再将 <Route> 元素嵌套,而是嵌套自己的组件,并在子组件中放置另一个 <Switch>。你可以看到它在 这个例子 中是如何工作的。这使得构建大型应用程序变得很容易,因为代码拆分一个 React Router 应用程序与代码拆分任何其他 React 应用程序相同,并且你可以使用当时可用的几种不同的工具来进行 React 中的代码拆分,这些工具与 React Router 无关。但是,这种方法的一个意外副作用是,<Route path> 只能匹配 URL 路径名的开头,因为每个路由组件可能在树的更下方有更多的子路由。因此,React Router v5 应用程序必须在没有子路由的情况下(每个叶子路由)每次都使用 <Route exact>。糟糕。

在我们的实验性 Reach Router 项目中,我们借鉴了 Preact Router 的一个想法,并执行了自动路由排名,以尝试确定哪个路由最适合匹配 URL,而不管它定义的顺序如何。这是对 v5 的 <Switch> 元素的重大改进,并帮助开发人员避免因错误顺序定义路由而导致的错误,从而导致不可达路由。但是,Reach Router 缺少 <Route> 组件,在使用 TypeScript 时会导致一些问题,因为你的所有路由组件也必须接受路由特定的 props,例如 path(我在 这里 详细介绍了这个问题)。

那么 React Router v6 将我们带到哪里呢?理想情况下,我们可以拥有我们迄今为止探索过的每个 API 的最佳功能,同时避免它们存在的问题。具体来说,我们希望

  • 在 v3 中拥有我们拥有的共置嵌套 <Route> 的可读性,但也支持代码拆分以及将自定义 props 传递到路由组件。
  • 在 v4/5 中拥有我们拥有的将路由拆分到多个组件中的灵活性,而不会在所有地方都散布 exact 属性。
  • 在 Reach Router 中拥有我们拥有的路由排名功能,而不会弄乱路由组件的 prop 类型。

哦,我们还想拥有 我们在 v3 中拥有的基于对象的路由 API,它允许你将路由定义为普通的 JavaScript 对象,而不是 <Route> 元素,以及我们在 v4/5 的 react-router-config 附加组件中提供的静态匹配和渲染函数。

好吧,不用说,我们非常高兴能够引入一个满足所有这些要求的路由 API。查看我们网站上的文档中 v6 API。它实际上看起来很像 v3。

import { render } from "react-dom";
import { BrowserRouter } from "react-router-dom";
// import your route components too

render(
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<App />}>
        <Route index element={<Home />} />
        <Route path="teams" element={<Teams />}>
          <Route path=":teamId" element={<Team />} />
          <Route path="new" element={<NewTeamForm />} />
          <Route index element={<LeagueStandings />} />
        </Route>
      </Route>
    </Routes>
  </BrowserRouter>,
  document.getElementById("root"),
);

但是,如果你仔细观察,你会发现我们多年来的工作带来的几个微妙的改进。

  • 我们正在使用 <Routes> 而不是 <Switch><Routes> 不会按顺序扫描路由,而是会自动为当前 URL 选择最适合的那个。它还允许你将路由分散在整个应用程序中,而不是像我们在 v3 中那样将它们全部定义为 <Router> 的属性。
  • <Route element> 属性允许您将自定义属性(甚至 children)传递到路由元素中。它还使您能够轻松地使用 <React.Suspense> 延迟加载您的路由元素,以防它是 React.lazy() 组件。我们在从 v5 升级的说明中详细介绍了 <Route element> API 的优势。
  • 与其在所有叶子路由中添加 <Route exact> 来选择退出深度匹配,不如在路由路径的末尾使用 *选择加入深度匹配,这样您仍然可以像这样拆分您的路由配置
import { Routes, Route } from "react-router-dom";

function App() {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route path="users" element={<Users />}>
          <Route index element={<UsersIndex />} />

          {/* This route will match /users/*, allowing more routing
              to happen in the <UsersSplat> component */}
          <Route path="*" element={<UsersSplat />} />
        </Route>
      </Route>
    </Routes>
  );
}

function UsersSplat() {
  // More routes here! These won't be defined until this component
  // mounts, preserving the dynamic routing semantics we had in v5.
  // All paths defined here are relative to /users since this element
  // renders inside /users/*
  return (
    <Routes>
      <Route path=":id" element={<UserProfile />}>
        <Route path="friends" element={<UserFriends />} />
        <Route path="messages" element={<UserMessages />} />
      </Route>
    </Routes>
  );
}

我们的路由 API 还有很多内容,我很乐意在这里展示给您,但很难在一篇博文中尽善尽美地展现。但幸运的是,您可以阅读代码。因此,我只会链接一些示例,希望它们比我在这里能写的内容更能说明问题。每个示例都包含一个按钮,允许您在在线编辑器中启动它,以便您可以试用它。

请随时查看此处 v6 的其他示例,如果您发现我们错过了您想看到的示例,请务必向我们发送 PR!

我们从 v3 中引入的另一个重要功能是通过新的 <Outlet> 元素对布局路由的原生支持。您可以在v6 概述中了解更多有关布局的信息。

这确实是我们设计过的最灵活、最强大的路由 API,我们对它将帮助我们构建的应用程序类型感到非常兴奋。

React Router v6 的另一个重大改进是相对 <Route path><Link to> 值,我们在升级指南中详细介绍了 React Router v5 的升级指南。基本上,它归结为以下几点

  • 相对 <Route path> 值始终相对于父路由。您不必再从 / 开始构建它们了。
  • 相对 <Link to> 值始终相对于路由路径。如果它只包含一个搜索字符串(例如 <Link to="?framework=react">),则它相对于当前位置的路径名。
  • 相对 <Link to> 值比 <a href> 不那么含糊,并且始终指向同一个位置,无论当前 URL 是否有尾部斜杠。

请参阅v5 升级指南中有关 <Link to> 值的说明,以了解有关相对 <Link to> 值如何比 <a href> 值不那么含糊,以及如何使用前导 .. 段链接回父路由。

相对路由和链接是朝着使路由更容易使用迈出的重大一步,它不再要求您在嵌套路由中构建绝对 <Route path><Link to> 值。实际上,这应该是它一直以来的工作方式,我们认为您会真正喜欢以这种方式构建应用程序的便捷性和直观性。

注意:绝对路径在 v6 中仍然有效,以帮助使升级更容易。您甚至可以完全忽略相对路径,并始终继续使用绝对路径,如果您愿意的话。我们不会介意。

升级到 React Router v6

我们要非常清楚地说明这一点:React Router v6 是所有先前版本的 React Router(包括 v3 和 v4/5)的继任者。它也是 Reach Router 的继任者。我们鼓励所有 React Router 和 Reach Router 用户尽可能升级到 v6。我们对 v6 有很大的计划,我们不想在我们在 6.x 中引入一些非常酷的东西时让你被落下!(是的,即使是那些坚持使用 onEnter 钩子的 v3 用户也不想错过这一点)。

但是,我们意识到让所有人都升级对于一套每月下载量达 3400 万次的库来说是一个相当雄心勃勃的目标。我们已经在为 React Router v5 用户开发一个向后兼容层,并且很快将与一些客户进行测试。我们的计划是为 Reach Router 用户开发一个类似的层。如果您有一个大型应用程序,并且升级到 v6 似乎令人生畏,请不要担心。我们的向后兼容层即将推出。此外,v5 将在可预见的未来继续接收更新,因此不要着急。

如果您迫不及待,并且认为您想自己进行升级,这里有一些链接可以帮助您

除了官方的升级指南之外,我还发布了一些笔记,可以帮助您逐步开始迁移。请记住,任何迁移的目标都是能够完成一些工作,然后发布它。没有人喜欢长时间运行的升级分支!

以下是一些关于已弃用模式的说明,以及您可以在尝试升级到 v6 之前在 v5 应用程序中今天就能实现的修复方法

再次强调,请不要感到有压力要进行此迁移。我们认为 React Router v6 是我们构建过的最好的路由器,但您可能在工作中还有更大的问题需要处理。当您准备升级时,我们将随时为您提供帮助。

如果您是 Reach Router 用户,担心会失去它提供的可访问性功能,您会很高兴地知道我们仍在努力解决这个问题。事实证明,Reach Router 的自动焦点管理实际上在某些情况下比什么都不做还要糟糕。我们意识到我们需要比仅仅是位置变化更多的信息才能正确管理焦点。但是,这是一次有价值的尝试,我们学到了很多。我们的下一个项目将帮助您构建比以往更具可访问性的应用程序……

未来:Remix

React Router 为当今许多最雄心勃勃和最令人印象深刻的 Web 应用程序提供了基础。打开像 Netflix、Twitter、Linear 或 Coinbase 这样的 Web 应用程序的开发者控制台,并看到 React Router 被用于这些企业的旗舰应用程序,这对我来说是一种难以置信的感觉。这些公司中的每一家都拥有非凡的人才库和资源,并且它们以及许多其他公司选择在 React 和 React Router 上构建他们的业务。

人们真正喜欢 React Router 的一件事是它如何完成它的工作,然后不再妨碍您。它从未真正尝试成为一个有主见框架,因此它完美地融入您现有的堆栈中。也许您正在进行服务器渲染,也许没有。也许您正在进行代码拆分,也许没有。也许您正在使用客户端路由和数据渲染一个动态网站,或者也许您只是渲染一堆静态页面。React Router 会很乐意做您想做的任何事情。

但是您如何构建应用程序的其余部分呢?路由只是一部分。数据加载和变异呢?缓存和性能呢?您应该进行服务器渲染吗?进行代码拆分的最佳方式是什么?您应该如何部署和托管您的应用程序?

碰巧的是,我们对所有这些都有一些非常强烈的意见。这就是为什么我们正在构建 Remix,一个新的 Web 框架,它将帮助您构建更好的网站。

随着近年来 Web 应用程序变得越来越复杂,前端 Web 开发团队承担了比以往更多的责任。他们不仅需要了解如何编写 HTML、CSS 和 JavaScript。他们还需要了解 TypeScript、编译器和构建管道。此外,他们需要了解捆绑器和代码拆分,并了解应用程序在客户浏览网站时的加载方式。需要考虑的事情太多了!Remix 和令人惊叹的 Remix 社区将像您的团队中的一名额外成员一样,帮助您管理并做出明智的决策,以了解如何完成所有这些以及更多。

我们已经研究 Remix 超过一年了,最近获得了部分资金并聘请了一支团队来帮助我们构建它。我们将在今年年底之前在开源许可下发布代码。React Router v6 是 Remix 的核心。随着 Remix 的发展并变得越来越好,路由器也会随之发展。您将继续看到来自我们关于 React Router 和 Remix 的稳定发布和改进。

我们非常感谢迄今为止收到的所有支持,以及多年来相信我们的众多朋友和客户。我们真诚地希望您喜欢使用 React Router v6 和 Remix!


获取有关最新 Remix 新闻的更新

成为第一个了解 Remix 新功能、社区活动和教程的人。