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

React Router v6

Michael Jackson
联合创始人

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

这个版本的发布已经酝酿了很久。上一次我们发布主要的破坏性 API 更改是在四年多前的 2017 年 3 月,当时我们发布了第 4 版。你们中的一些人可能那时甚至还没出生。不用说,从那时起发生了很多事情

  • React Router 的下载量从 2017 年 3 月的每月 34 万次增长到 2021 年 10 月的每月 2100 万次,增长了 60 倍(6000%)
  • 我们发布了第 5 版,没有重大更改(我已经写过关于主版本号提升原因的 其他文章
  • 我们发布了 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,并且一旦你使用 tree-shaking 打开你的打包器运行它,你的实际结果将会更小。

一个可组合的路由器

为了演示如何在 v6 中使用 hooks 改进你的代码,让我们从一些非常简单的事情开始,例如从当前 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> prop 使得将自定义 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 props
  • 我们在 Reach Router 中拥有的路由排名能力,而不会使你的路由组件的 prop 类型混乱

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

好吧,不用说,我们非常高兴地推出一个满足所有这些要求的路由 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> 的 prop。
  • <Route element> prop 允许你将自定义 props (甚至是 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 功能、社区活动和教程的人。