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 程式碼重構為 hooks 時,我們過去習慣用 React 的「生命週期方法」編寫的許多程式碼是如何消失的。如果您不記得那個演講,您可能應該在這裡停下來並去看一下。我會等你的。

雖然我們在 5.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 或更早版本中使用 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> 來存取路由器內部狀態 (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> 屬性允許您將自訂屬性 (甚至 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 將在可預見的未來繼續接收更新,所以不用急著升級。

如果您迫不及待,而且認為您想自己進行升級,這裡有一些連結應該可以幫助您

除了官方升級指南外,我還發布了一些注意事項,這些注意事項應該可以幫助您慢慢開始遷移。請記住,任何遷移的目標都是能夠完成一些工作,然後再發佈它。沒有人喜歡長期執行的升級分支!

以下是一些關於已棄用模式以及您今天可以在 v5 應用程式中實作的修復程式的注意事項,然後再嘗試升級到 v6。

再次強調,請不要感到有壓力要進行此遷移。我們認為 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 功能、社群活動和教學課程的人。