React Router 6强势来袭(15分钟了解它的前世今生和未来)

1,764 阅读16分钟

本篇是译文,原文地址在文章末尾,如果有翻译问题欢迎指正和交流!

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

这个版本已经发布了很长时间。我们上一次发布重大的 API 更改是在四年前的 2017 年 3 月,当时我们发布了第 4 版。你们中的一些人可能那时还没有出生。不用说,从那时到现在发生了很多事情:

  • React Router 的下载量从 2017 年 3 月的 34 万/月增长到 2021 年 10 月的 2100 万/月,增长了 60 倍以上(6000%)
  • 我们发布了没有重大更改的第 5 版(我已经在别处写过主要版本变更的原因)
  • 我们发布了 Reach Router,目前平均每月下载量约为 1300 万次
  • 引入了 React Hooks
  • 新冠

笔者备注: 关于版本的补充,本来是发布4.4版本的,但是因为react-router-dom依赖^react-router,注意^,可能会存在4.3react-router-dom依赖4.4react-router导致context不同从而引入问题,因此被迫升级到了5 react-router-dom 和 react-router的区别

我可以轻松地至少写几页关于上述每个要点及其对我们的业务和我们自 2014 年以来一直管理的开源项目的重要性。但我不想让你厌烦过去。在过去的几年里,我们都经历了很多。其中一些很艰难,但希望你也经历了一些新的成长,像我们一样。事实上,我们彻底改变了我们的商业模式!

今天,我想聚焦于未来,以及我们如何利用过去的经验为 React Router 项目和令人难以置信的 React 社区构建最强大的未来。会有代码。但我们还将讨论业务以及您对我们的期望(提示: 它丰富多彩)。

为什么是另一个主要版本?

发布新路由器的最大原因很容易是React hooks的出现。您可能还记得Ryan在 React Conf 2018 上向世界介绍 hooks的演讲,以及当您将基于类的 React 代码重构为 hooks 时,我们习惯使用 React 的“生命周期方法”编写的很多代码都消失了。如果你不记得那次演讲,你可能应该在这里停下来看看。我会等待。

尽管我们在 v5中(v5.1) 上添加了一些hooks,但 React Router v6 是真正使用 React hook 重新实现的。它们是如此高效的低级原语,以至于我们能够通过使用hooks来消除大量样板代码。这意味着您的 v6 代码将比 v5 代码更加紧凑和优雅。

此外,不仅仅是您的代码变得更小、更高效……对我们的实现也是对此!我们压缩后的 gzip 压缩包大小在 v6 中下降了 50% 以上! React Router 现在为你的总应用程序包增加了不到4kb,一旦通过你的打包器工具运行它并打开 tree-shaking,实际结果会更小。

可组合路由器

为了演示如何使用 v6 中的hooks改进您的代码,让我们从一些非常简单的事情开始,例如从当前 URL 路径名访问参数。React Router v6 提供了一个useParams()hooks(也在 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...
  const { id } = useParams();
  return (
    <>
      <PostHeader />
      {/* ... */}
    </>
  );
}

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

在 v5 或 v5 之前的版本中如何通过render prop或高阶组件实现同样的功能。

// 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>访问router内部状态 (match相关的逻辑)和 手动传递 props 以将该状态传递到子组件的必要。

另一种说法是在router context中考虑useParams()类似useState()的东西。router知道一些state(当前的 URL 参数),并允许您随时使用hooks访问它。如果没有hooks,我们需要手动将state传递到子孙组件中。

让我们看一下使用hooks的 React Router v6 如何比 v5 强大得多的另一个简单示例。假设您希望在当前location发生变化时向您的分析服务(类似google analysis)发送“pageview”事件。V6(v5.1+)中,使用useLocation()hooks可以帮你快速实现对应功能:

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

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

当然,由于hooks提供的功能组合,您可以将所有这些都包装到一个hooks中,例如:

import { useAnalyticsTracking } from "./analytics";

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

同样,如果没有hooks,你必须做一些hack,比如渲染一个独立的<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吗?可能不是。但这就是react-router在 npm 上正式获得package name之前所说的 React Router (谢谢,Jared!)。React Router 一直在声明嵌套路由的,尽管我们表达它们的方式随着时间的推移略有变化。我将向您展示我们为 v6 提出的方案,但首先让我给您介绍一些关于 v3、v4/5 和 Reach Router 的背景故事。

在 v3 中,我们将<Route>元素直接嵌套在一个巨大的路由配置中,如本例所示。嵌套<Route>元素是可视化整个路线层次结构的好方法。然而,我们在 v3 中的实现使得代码拆分变得困难,因为所有的路由组件最终都在同一个包中(这是之前的React.lazy())。因此,随着您添加更多路线,您的捆绑包会不断增长。此外,<Route component>prop使得很难将custom props传递到您的组件。

在 v4 中,我们针对大型应用程序进行了优化。代码拆分(Code splitting)!而不是<Route>在 v4中嵌套元素,您只需嵌套自己的组件并将另一个组件放入<Switch>子组件中。您可以在此示例中看到它是如何工作的。这使得构建大型应用程序变得容易,因为拆分 React Router 应用程序的代码与拆分任何其他 React 应用程序的代码相同,您可以使用当时可用的几种不同工具之一在 React 中进行代码拆分,而这些工具与 React 无关路由器。然而,这种方法的一个意想不到的缺点是它<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 的最佳功能,同时还避免了它们遇到的问题。具体来说,我们想要:

  • <Route>我们在 v3 中拥有的并置、嵌套的的可读性,但也支持代码拆分和将custom props传递到您的路由组件
  • 我们在 v4/5 中拥有的跨多个组件拆分路由的灵活性,而无需exact到处传递props
  • 我们在 Reach Router 中拥有的路由排名的功能,而不会弄乱路由组件的 prop 类型

哦,我们也会喜欢基于对象的路由API,在V3中,我们允许路由作为普通的JavaScript对象使用,而不必像在V4/5中。只能使用<Route>element、static match和render functions,如果想要像使用对象一样使用的话,还需要借助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 选择最好的路由。它还允许您在整个应用程序中传递路由,而不是<Router>像我们在 v3 中那样预先将它们全部定义为 prop 。
  • <Route element>prop可以让你通过自定义custom props(甚至children)到您的route elements。如果它是一个React.lazy()组件,它还可以简单的通过<React.Suspense>延迟加载您的路由元素。我们在从 v5 升级的说明中详细介绍了<Route element>API的优势。
  • 不需要在路由的叶子节点来声明<Route exact>以确保是否可以进行深度匹配,可以将route path定义为*来选择深入匹配,所以你可以像下面这样去拆分你的route config文件
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>,它基本上可以归纳为以下几点:

  • 相对<Route path>的值(path)总是相对于父路由。您不必再从 / 构建它们。
  • 相对<Link to>的值(to)总是相对于route path。如果它只包含一个搜索字符串(即<Link to="?framework=react">),它是相对于current location的pathname。
  • 相对<Link to>的值(to)相比比<a href>的值会更清晰一些,而且始终指向同一个路径,无论当前 URL 是否带有斜杠

另请参阅v5 升级指南中有关<Link to>值的说明,以了解有关相对<Link to>的值(to)为何比<a href>的指向更清晰以及如何使用前置..段返回父路由。

相对路由和链接 使Router的易用性迈出了一大步,因为不需要您在嵌套路由中构建绝对的<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 中引入一些非常酷的东西时,我们不希望您被排除在外!(是的,即使你v3 用户坚持你的onEnterhooks也不会想错过这个)。

但是,我们意识到让每个人都升级对于一组每月下载 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 等网络应用程序的开发者控制台,可以看到 React Router 被用于这些企业的旗舰应用程序,这对我来说是一种奇妙的感觉。这些公司中的每一家都拥有出色的人才和资源库,他们和许多其他公司选择在 React 和 React Router 上建立自己的业务。

人们真正喜欢 React Router 的一件事是:它可以完美胜任路由的职责并且不用出问题。它从未真正想要成为一个封闭的框架,因此它可以任用任何的技术栈。也许你是服务器渲染,也许不是。也许你是代码拆分,也许不是。也许您正在渲染一个带有客户端路由和数据的动态站点,或者您只是在渲染一堆静态页面。React Router 会很乐意做任何你想做的事。

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

碰巧的是,我们对所有这些都有一些非常强烈的意见。这就是我们构建 Remix的原因,这是一个新的网络框架,将帮助您构建更好的网站。

近年来,随着 Web 应用程序变得越来越复杂,前端 Web 开发团队承担了比以往任何时候都多得多的责任。他们不仅要知道如何编写 HTML、CSS 和 JavaScript。他们还需要了解 TypeScript、编译器和构建流水线。此外,他们需要了解bundlers和code splitting,并了解当客户浏览网站时应用程序如何加载。有很多事情要考虑!Remix 和令人惊叹的 Remix 社区将会为您团队提供额外帮助,可以帮您管理并就如何完成所有这些以及更多事情做出明智的选择。

我们已经在 Remix 上工作了一年多,最近获得了一些资金并聘请了一个团队来帮助我们构建它。我们将在年底之前根据开源许可证发布代码。而 React Router v6 是 Remix 的核心。随着 Remix 向前发展并变得越来越好,路由器也是如此。您将继续在 React Router 和 Remix 上看到我们源源不断的发布和改进。

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

原文地址:remix.run/blog/react-…