[譯]關於ReactRouter4的一切

玄學醬發表於2017-10-16
本文講的是[譯] 關於 React Router 4 的一切,

關於 React Router 4 的一切

我在 React Rally 2016 大會上第一次遇到了 Michael Jackson,不久之後便寫了一篇 an article on React Router 3。Michael 與 Ryan Florence 都是 React Router 的主要作者。遇到一位我非常喜歡的工具的建立者是激動人心的,但當他這麼說的時候,我感到很震驚。“讓我向你們展示我們在 React Router 4 的想法,它的方式是截然不同的!”。老實說,我真的不明白新的方向以及為什麼它需要如此大的改變。由於路由是應用程式架構的重要組成部分,因此這可能會改變一些我喜歡的模式。這些改變的想法讓我很焦慮。考慮到社群凝聚力以及 React Router 在這麼多的 React 應用程式中扮演著重要的角色,我不知道社群將如何接受這些改變。

幾個月後,React Router 4 釋出了,僅僅從 Twitter 的嗡嗡聲中我便得知,大家對於這個重大的重寫存在著不同的想法。這讓我想起了第一個版本的 React Router 針對其漸進概念的推回。在某些方面,早期版本的 React Router 符合我們傳統的思維模式,即一個應用的路由“應該”將所有的路由規則放在一個地方。然而,並不是每個人都接受使用巢狀的 JSX 路由。但就像 JSX 自身說服了批評者一樣(至少是大多數),許多人轉而相信巢狀的 JSX 路由是很酷的想法。

如是,我學習了 React Router 4。無可否認,第一天是掙扎的。掙扎的倒不是其 API,而更多的是使用它的模式和策略。我使用 React Router 3 的思維模式並沒有很好地遷移到 v4。如果要成功,我將不得不改變我對路由和佈局元件之間的關係的看法。最終,出現了對我有意義的新模式,我對路由的新方向感到非常高興。React Router 4 不僅包含 v3 的所有功能,而且還有新的功能。此外,起初我對 v4 的使用過於複雜。一旦我獲得了一個新的思維模式,我就意識到這個新的方向是驚人的!

本文的意圖並不是重複 React Router 4 已經寫得很好的文件。我將介紹最常見的 API,但真正的重點是我發現的成功模式和策略。

對於本文,以下是一些你需要熟悉的 JavaScript 概念:

如果你喜歡跳轉到演示區的話,請點這裡:

檢視演示

新的 API 和新的思維模式

React Router 的早期版本將路由規則集中在一個位置,使它們與佈局元件分離。當然,路由可以被劃分成多個檔案,但從概念上講,路由是一個單元,基本上是一個美化的配置檔案。

或許瞭解 v4 不同之處的最好方法是用每個版本編寫一個簡單的兩頁應用程式並進行比較。示例應用程式只有兩個路由,對應首頁和使用者頁面。

這裡是 v3 的:

import { Router, Route, IndexRoute } from `react-router`

const PrimaryLayout = props => (
  <div className="primary-layout">
    <header>
      Our React Router 3 App
    </header>
    <main>
      {props.children}
    </main>
  </div>
)

const HomePage =() => <div>Home Page</div>
const UsersPage = () => <div>Users Page</div>

const App = () => (
  <Router history={browserHistory}>
    <Route path="/" component={PrimaryLayout}>
      <IndexRoute component={HomePage} />
      <Route path="/users" component={UsersPage} />
    </Route>
  </Router>
)

render(<App />, document.getElementById(`root`))

以下是 v3 中的一些核心思想,但在 v4 中是不正確的:

  • 路由集中在一個地方。
  • 佈局和頁面巢狀是通過 <Route> 元件的巢狀而來的。
  • 佈局和頁面元件是完全純粹的,它們是路由的一部分。

React Router 4 不再主張集中式路由了。相反,路由規則位於佈局和 UI 本身之間。例如,以下是 v4 中的相同的應用程式:

import { BrowserRouter, Route } from `react-router-dom`

const PrimaryLayout = () => (
  <div className="primary-layout">
    <header>
      Our React Router 4 App
    </header>
    <main>
      <Route path="/" exact component={HomePage} />
      <Route path="/users" component={UsersPage} />
    </main>
  </div>
)

const HomePage =() => <div>Home Page</div>
const UsersPage = () => <div>Users Page</div>

const App = () => (
  <BrowserRouter>
    <PrimaryLayout />
  </BrowserRouter>
)

render(<App />, document.getElementById(`root`))

新的 API 概念:由於我們的應用程式是用於瀏覽器的,所以我們需要將它封裝在來自 v4 的BrowserRouter 中。還要注意的是我們現在從 react-router-dom 中匯入它(這意味著我們安裝的是 react-router-dom 而不是 react-router)。提示!現在叫做 react-router-dom 是因為還有一個 native 版本

對於使用 React Router v4 構建的應用程式,首先看到的是“路由”似乎丟失了。在 v3 中,路由是我們的應用程式直接呈現給 DOM 的最巨大的東西。 現在,除了 <BrowserRouter> 外,我們首先拋給 DOM 的是我們的應用程式本身。

另一個在 v3 的例子中有而在 v4 中沒有的是,使用 {props.children} 來巢狀元件。這是因為在 v4 中,<Route> 元件在何處編寫,如果路由匹配,子元件將在那裡渲染。

包容性路由

在前面的例子中,你可能已經注意到了 exact 這個屬性。那麼它是什麼呢?V3 的路由規則是“排他性”的,這意味著只有一條路由將獲勝。V4 的路由預設為“包含”的,這意味著多個<Route> 可以同時進行匹配和渲染。

在上一個例子中,我們試圖根據路徑渲染 HomePage 或者 UsersPage。如果從示例中刪除了exact 屬性,那麼在瀏覽器中訪問 /users 時,HomePage 和 UsersPage 元件將同時被渲染。

要更好地瞭解匹配邏輯,請檢視 path-to-regexp,這是 v4 現在正在使用的,以確定路由是否匹配 URL。

為了演示包容性路由是有幫助的,我們在標題中包含一個 UserMenu,但前提是我們在應用程式的使用者部分:

const PrimaryLayout = () => (
  <div className="primary-layout">
    <header>
      Our React Router 4 App
      <Route path="/users" component={UsersMenu} />
    </header>
    <main>
      <Route path="/" exact component={HomePage} />
      <Route path="/users" component={UsersPage} />
    </main>
  </div>
)

現在,當使用者訪問 /users 時,兩個元件都會渲染。類似這樣的事情在 v3 中通過特定的匹配模式也是可行的,但它更復雜。得益於 v4 的包容性路由,現在能夠很輕鬆地實現。

排他性路由

如果你只需要在路由列表裡匹配一個路由,則使用 <Switch> 來啟用排他路由:

const PrimaryLayout = () => (
  <div className="primary-layout">
    <PrimaryHeader />
    <main>
      <Switch>
        <Route path="/" exact component={HomePage} />
        <Route path="/users/add" component={UserAddPage} />
        <Route path="/users" component={UsersPage} />
        <Redirect to="/" />
      </Switch>
    </main>
  </div>
)

在給定的 <Switch> 路由中只有一條將渲染。在 HomePage 路由上,我們仍然需要 exact 屬性,儘管我們會先把它列出來。否則,當訪問諸如 /users 或 /users/add 的路徑時,主頁路由也將匹配。事實上,戰略佈局是使用排他路由策略(因為它總是像傳統路由那樣使用)時的關鍵。請注意,我們在 /users 之前策略性地放置了 /users/add 的路由,以確保正確匹配。由於路徑 /users/add 將匹配 /users 和 /users/add,所以最好先把 /users/add 放在前面。

當然,如果我們以某種方式使用 exact,我們可以把它們放在任何順序上,但至少我們有選擇。

如果遇到,<Redirect> 元件將會始終執行瀏覽器重定向,但是當它位於 <Switch> 語句中時,只有在其他路由不匹配的情況下,才會渲染重定向元件。想了解在非切換環境下如何使用 <Redirect>,請參閱下面的授權路由

“預設路由”和“未找到”

儘管在 v4 中已經沒有 <IndexRoute> 了,但可以使用 <Route exact> 來達到同樣的效果。如果沒有路由解析,則可以使用 <Switch> 與 <Redirect> 重定向到具有有效路徑的預設頁面(如同我對本示例中的 HomePage 所做的),甚至可以是一個“未找到頁面”。

巢狀佈局

你可能開始期待巢狀子佈局,以及如何實現它們。我原本不認為我會糾結這個概念,但我確實糾結了。React Router v4 給了我們很多選擇,這使它變得很強大。但是,選擇意味著有選擇不理想策略的自由。表面上看,巢狀佈局很簡單,但根據你的選擇,可能會因為你組織路由的方式而遇到阻礙。

為了演示,假設我們想擴充套件我們的使用者部分,所以我們會有一個“使用者列表”頁面和一個“使用者詳情”頁面。我們也希望產品也有類似的頁面。使用者和產品都需要其個性化的子佈局。例如,每個可能都有不同的導航選項卡。有幾種方法可以解決這個問題,有的好,有的不好。第一種方法不是很好,但我想告訴你,這樣你就不會掉入這個陷阱。第二種方法要好很多。

第一種方法,我們修改 PrimaryLayout,以適應使用者和產品對應的列表及詳情頁面:

const PrimaryLayout = props => {
  return (
    <div className="primary-layout">
      <PrimaryHeader />
      <main>
        <Switch>
          <Route path="/" exact component={HomePage} />
          <Route path="/users" exact component={BrowseUsersPage} />
          <Route path="/users/:userId" component={UserProfilePage} />
          <Route path="/products" exact component={BrowseProductsPage} />
          <Route path="/products/:productId" component={ProductProfilePage} />
          <Redirect to="/" />
        </Switch>
      </main>
    </div>
  )
}

雖然這在技術上可行的,但仔細觀察這兩個使用者頁面就會發現問題:

const BrowseUsersPage = () => (
  <div className="user-sub-layout">
    <aside>
      <UserNav />
    </aside>
    <div className="primary-content">
      <BrowseUserTable />
    </div>
  </div>
)

const UserProfilePage = props => (
  <div className="user-sub-layout">
    <aside>
      <UserNav />
    </aside>
    <div className="primary-content">
      <UserProfile userId={props.match.params.userId} />
    </div>
  </div>
)

新 API 概念props.match 被賦到由 <Route> 渲染的任何元件。你可以看到,userId 是由props.match.params 提供的,瞭解更多請參閱 v4 文件。或者,如果任何元件需要訪問props.match,而這個元件沒有由 <Route> 直接渲染,那麼我們可以使用 withRouter() 高階元件。

每個使用者頁面不僅要渲染其各自的內容,而且還必須關注子佈局本身(並且每個子佈局都是重複的)。雖然這個例子很小,可能看起來微不足道,但重複的程式碼在一個真正的應用程式中可能是一個問題。更不用說,每次 BrowseUsersPage 或 UserProfilePage 被渲染時,它將建立一個新的 UserNav 例項,這意味著所有的生命週期方法都將重新開始。如果導航標籤需要初始網路流量,這將導致不必要的請求 —— 這都是我們決定使用路由的方式造成的。

這裡有另一種更好的方法:

const PrimaryLayout = props => {
  return (
    <div className="primary-layout">
      <PrimaryHeader />
      <main>
        <Switch>
          <Route path="/" exact component={HomePage} />
          <Route path="/users" component={UserSubLayout} />
          <Route path="/products" component={ProductSubLayout} />
          <Redirect to="/" />
        </Switch>
      </main>
    </div>
  )
}

與每個使用者和產品頁面相對應的四條路由不同,我們為每個部分的佈局提供了兩條路由。

請注意,上述示例沒有使用 exact 屬性,因為我們希望 /users 匹配任何以 /users 開頭的路由,同樣適用於產品。

通過這種策略,渲染其它路由將成為子佈局的任務。UserSubLayout 可能如下所示:

const UserSubLayout = () => (
  <div className="user-sub-layout">
    <aside>
      <UserNav />
    </aside>
    <div className="primary-content">
      <Switch>
        <Route path="/users" exact component={BrowseUsersPage} />
        <Route path="/users/:userId" component={UserProfilePage} />
      </Switch>
    </div>
  </div>
)

新策略中最明顯的勝出在於所有使用者頁面之間的不重複佈局。這是一個雙贏,因為它不會像第一個示例那樣具有相同生命週期的問題。

有一點需要注意的是,即使我們在佈局結構中深入巢狀,路由仍然需要識別它們的完整路徑才能匹配。為了節省重複輸入(以防你決定將“使用者”改為其他內容),請改用props.match.path

const UserSubLayout = props => (
  <div className="user-sub-layout">
    <aside>
      <UserNav />
    </aside>
    <div className="primary-content">
      <Switch>
        <Route path={props.match.path} exact component={BrowseUsersPage} />
        <Route path={`${props.match.path}/:userId`} component={UserProfilePage} />
      </Switch>
    </div>
  </div>
)

匹配

到目前為止,props.match 對於知道詳情頁面渲染的 userId 以及如何編寫我們的路由是很有用的。match 物件給我們提供了幾個屬性,包括 match.paramsmatch.pathmatch.url其他幾個

match.path vs match.url

起初這兩者之間的區別似乎並不清楚。控制檯日誌有時會顯示相同的輸出,這使得它們之間的差異更加模糊。例如,當瀏覽器路徑為 /users 時,它們在控制檯日誌將輸出相同的值:

const UserSubLayout = ({ match }) => {
  console.log(match.url)   // 輸出:"/users"
  console.log(match.path)  // 輸出:"/users"
  return (
    <div className="user-sub-layout">
      <aside>
        <UserNav />
      </aside>
      <div className="primary-content">
        <Switch>
          <Route path={match.path} exact component={BrowseUsersPage} />
          <Route path={`${match.path}/:userId`} component={UserProfilePage} />
        </Switch>
      </div>
    </div>
  )
}

ES2015 概念: match 在元件函式的引數級別將被解構

雖然我們看不到差異,但 match.url 是瀏覽器 URL 中的實際路徑,而 match.path 是為路由編寫的路徑。這就是為什麼它們是一樣的,至少到目前為止。但是,如果我們更進一步,在UserProfilePage 中進行同樣的控制檯日誌操作,並在瀏覽器中訪問 /users/5,那麼match.url 將是 "/users/5" 而 match.path 將是 "/users/:userId"

選擇哪一個?

如果你要使用其中一個來幫助你構建路由路徑,我建議你選擇 match.path。使用 match.url來構建路由路徑最終會導致你不想看到的場景。下面是我遇到的一個情景。在一個像UserProfilePage(當使用者訪問 /users/5 時渲染)的元件中,我渲染瞭如下這些子元件:

const UserComments = ({ match }) => (
  <div>UserId: {match.params.userId}</div>
)

const UserSettings = ({ match }) => (
  <div>UserId: {match.params.userId}</div>
)

const UserProfilePage = ({ match }) => (
  <div>
    User Profile:
    <Route path={`${match.url}/comments`} component={UserComments} />
    <Route path={`${match.path}/settings`} component={UserSettings} />
  </div>
)

為了說明問題,我渲染了兩個子元件,一個路由路徑來自於 match.url,另一個來自match.path。以下是在瀏覽器中訪問這些頁面時所發生的事情:

  • 訪問 /users/5/comments 渲染 “UserId: undefined”。
  • 訪問 /users/5/settings 渲染 “UserId: 5″。

那麼為什麼 match.path 可以幫助我們構建路徑 而 match.url 則不可以呢?答案就是這樣一個事實:{${match.url}/comments} 基本上就像和硬編碼的 {`/users/5/comments`} 一樣。這樣做意味著後續元件將無法正確地填充 match.params,因為路徑中沒有引數,只有硬編碼的5

直到後來我看到文件的這一部分,才意識到它有多重要:

match:

  • path – (string) 用於匹配路徑模式。用於構建巢狀的 <Route>
  • url – (string) URL 匹配的部分。 用於構建巢狀的 <Link>

避免匹配衝突

假設我們製作的應用程式是一個儀表版,所以我們希望能夠通過訪問 /users/add 和/users/5/edit 來新增和編輯使用者。但是在前面的例子中,users/:userId 已經指向了UserProfilePage。那麼這是否意味著帶有users/:userId 的路由現在需要指向另一個子子佈局來容納編輯頁面和詳情頁面?我不這麼認為,因為編輯和詳情頁面共享相同的使用者子佈局,所以這個策略是可行的:

const UserSubLayout = ({ match }) => (
  <div className="user-sub-layout">
    <aside>
      <UserNav />
    </aside>
    <div className="primary-content">
      <Switch>
        <Route exact path={props.match.path} component={BrowseUsersPage} />
        <Route path={`${match.path}/add`} component={AddUserPage} />
        <Route path={`${match.path}/:userId/edit`} component={EditUserPage} />
        <Route path={`${match.path}/:userId`} component={UserProfilePage} />
      </Switch>
    </div>
  </div>
)

請注意,為了確保進行適當的匹配,新增和編輯路由需要戰略性地放在詳情路由之前。如果詳情路徑在前面,那麼訪問 /users/add 時將匹配詳情(因為 “add” 將匹配 :userId)。

或者,如果我們這樣建立路徑 ${match.path}/:userId(\d+),來確保 :userId 必須是一個數字,那麼我們可以先放置詳情路由。然後訪問 /users/add 將不會產生衝突。這是我在 path-to-regexp 的文件中學到的技巧。

授權路由

在應用程式中,通常會根據使用者的登入狀態來限制使用者訪問某些路由。對於未經授權的頁面(如“登入”和“忘記密碼”)與已授權的頁面(應用程式的主要部分)看起來不一樣也是常見的。為了解決這些需求,需要考慮一個應用程式的主要入口點:

class App extends React.Component {
  render() {
    return (
      <Provider store={store}>
        <BrowserRouter>
          <Switch>
            <Route path="/auth" component={UnauthorizedLayout} />
            <AuthorizedRoute path="/app" component={PrimaryLayout} />
          </Switch>
        </BrowserRouter>
      </Provider>
    )
  }
}

使用 react-redux 與 React Router v4 非常類似,就像之前一樣,只需將 BrowserRouter 包在<Provider> 中即可。

通過這種方法可以得到一些啟發。第一個是根據我們所在的應用程式的哪個部分,在兩個頂層佈局之間進行選擇。像訪問 /auth/login 或 /auth/forgot-password 這樣的路徑會使用UnauthorizedLayout —— 一個看起來適於這種情況的佈局。當使用者登入時,我們將確保所有路徑都有一個 /app 字首,它使用 AuthorizedRoute 來確定使用者是否登入。如果使用者在沒有登入的情況下,嘗試訪問以 /app 開頭的頁面,那麼將被重定向到登入頁面。

雖然 AuthorizedRoute 不是 v4 的一部分,但是我在 v4 文件的幫助下自己寫了。v4 中一個驚人的新功能是能夠為特定的目的建立你自己的路由。它不是將 component 的屬性傳遞給<Route>,而是傳遞一個 render 回撥函式:

class AuthorizedRoute extends React.Component {
  componentWillMount() {
    getLoggedUser()
  }

  render() {
    const { component: Component, pending, logged, ...rest } = this.props
    return (
      <Route {...rest} render={props => {
        if (pending) return <div>Loading...</div>
        return logged
          ? <Component {...this.props} />
          : <Redirect to="/auth/login" />
      }} />
    )
  }
}

const stateToProps = ({ loggedUserState }) => ({
  pending: loggedUserState.pending,
  logged: loggedUserState.logged
})

export default connect(stateToProps)(AuthorizedRoute)

可能你的登入策略與我的不同,我是使用網路請求來 getLoggedUser(),並將 pending 和logged 插入 Redux 的狀態中。pending 僅表示在路由中請求仍在繼續。

點選此處檢視 CodePen 上完整的身份驗證示例

其他提示

React Router v4 還有很多其他很酷的方面。最後,一定要提幾件小事,以免到時它們讓你措手不及。

<Link> vs <NavLink>

在 v4 中,有兩種方法可以將錨標籤與路由整合:<Link> 和 <NavLink>

<NavLink> 與 <Link> 一樣,但如果 <NavLink> 匹配瀏覽器的 URL,那麼它可以提供一些額外的樣式能力。例如,在示例應用程式中,有一個<PrimaryHeader> 元件看起來像這樣:

const PrimaryHeader = () => (
  <header className="primary-header">
    <h1>Welcome to our app!</h1>
    <nav>
      <NavLink to="/app" exact activeClassName="active">Home</NavLink>
      <NavLink to="/app/users" activeClassName="active">Users</NavLink>
      <NavLink to="/app/products" activeClassName="active">Products</NavLink>
    </nav>
  </header>
)

使用 <NavLink> 可以讓我給任何一個啟用的連結設定一個 active 樣式。而且,需要注意的是,我也可以給它們新增 exact 屬性。如果沒有 exact,由於 v4 的包容性匹配策略,那麼在訪問 /app/users 時,主頁的連結將處於啟用中。就個人經歷而言,NavLink 帶 exact 屬性等價於 v3 的 <link>,而且更穩定。

URL 查詢字串

再也無法從 React Router v4 中獲取 URL 的查詢字串了。在我看來,做這個決定是因為沒有關於如何處理複雜查詢字串的標準。所以,他們決定讓開發者去選擇如何處理查詢字串,而不是將其作為一個選項嵌入到 v4 的模組中。這是一件好事。

就個人而言,我使用的是 query-string,它是由 sindresorhus 大神寫的。

動態路由

關於 v4 最好的部分之一是幾乎所有的東西(包括 <Route>)只是一個 React 元件。路由不再是神奇的東西了。我們可以隨時隨地渲染它們。想象一下,當滿足某些條件時,你的應用程式的整個部分都可以路由到。當這些條件不滿足時,我們可以移除路由。甚至我們可以做一些瘋狂而且很酷的遞迴路由

因為它 Just Components™,React Router 4 更簡單了。





原文釋出時間為:2017年8月17日

本文來自雲棲社群合作伙伴掘金,瞭解相關資訊可以關注掘金網站。


相關文章