關於 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 時,HomePageUsersPage 組件將同時被渲染。

要更好地瞭解匹配邏輯,請查看 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() 高階組件。

每個用戶頁面不僅要渲染其各自的內容,而且還必須關注子佈局本身(並且每個子佈局都是重複的)。雖然這個例子很小,可能看起來微不足道,但重複的代碼在一個真正的應用程序中可能是一個問題。更不用說,每次 BrowseUsersPageUserProfilePage 被渲染時,它將創建一個新的 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(),並將 pendinglogged 插入 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 時,主頁的鏈接將處於激活中。就個人經歷而言,NavLinkexact 屬性等價於 v3 的 <link>,而且更穩定。

URL 查詢字符串

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

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

動態路由

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

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


作者:undead25
鏈接:https://juejin.im/post/5995a2506fb9a0249975a1a4

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章