(譯)React-Router4的變化

小黃瓜驕傲發表於2018-12-17

首先,這篇文章的目的並不是為了重新敘述一遍React-Router4的文件。接下來我要說的內容,將會覆蓋React-Router的大多數API,但是真正的目的是揭開React-Router4成功的模式和策略。

在開始本文之前,你需要了解一些JS的概念:

  • React無狀態函式式元件
  • ES6箭頭函式以及它的“隱式返回”
  • ES6解構賦值
  • ES6模板字串

如果你喜歡直接看demo來了解,請點選此連結檢視。

新的API以及新的心智模型

React-Router早一些的版本中,是在一個地方統一地管理所有路由規則,將它們與佈局元件分離。當然,路由也可以被拆分和組織到幾個檔案當中,但是從概念上來說,路由本身是一個單元,基本上是一個美化的配置檔案。

要了解版本4到底有哪些不同,最好的方式可能是用每一個版本寫一個兩個頁面的應用,並進行比較。示例的應用只有home頁面和user頁面。

以下是版本3的寫法:

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'))
複製程式碼

版本3中的一些概念在版本4中可能不再適用:

  • 路由集中在一個檔案當中
  • 佈局和頁面巢狀是通過<Route>元件的巢狀派生的
  • 佈局和頁面元件是完全

React-Router4不在提倡集中管理路由。相反,路由規則存在於佈局和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,這意味著它也提供了原生版本。

在使用V4版本構建的應用中,最顯著的一點是“路由”似乎消失了。在V3中,路由是我們渲染到dom的一個大玩意兒,它統籌了我們的整個專案。現在,除了<BrowserRouter> 以外,第一個拋進dom的是我們的應用本身。

另一個在V4示例中沒有展現的V3的用法是使用props.children來巢狀元件。這是因為在V4中,只要路由匹配到了,<Router>元件在哪裡編寫,子元件就會渲染在哪裡。

包含式路由

在先前的例子中,你可能已經注意到了exact屬性。那麼它到底是幹嘛的呢?在V3中,路由規則是獨一無二的,這意味著最終只有一個路由會匹配到。但是在V4中,路由是包含式的,這意味著可能會有多個路由同時匹配到並且渲染。

在先前的例子中,我們試圖依靠路徑來區分渲染home頁面或者user頁面。如果把exact屬性從該示例中移除,那麼在瀏覽‘/user’路徑時,home頁面和user頁面會同時渲染。

想要更好的瞭解匹配規則,您可以檢視path-to-regexp,React-Router4 就是使用它來匹配路由的。

為了更好地演示包含式路由起到了什麼作用,我們實現一個這樣的功能:只有在user頁面我們才在頭部新增一個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>元件來實現。<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>中的路由只有一個會被渲染。我們仍然需要在home頁面的路由上新增exact屬性,否則,當我們訪問‘/users’或者‘/users/add’時,home頁面將會首先匹配到。事實上,在使用排他性路由時,路由的位置排列是最為關鍵的事,傳統路由一直如此。我們把‘/users/add’排列在‘/users’之前以保證正確的匹配路由。因為‘/users/add’會匹配‘/users’和‘/users/add’,把‘/users/add’放在前面是最好的。

當然,如果你使用了exact屬性,那放在哪兒都無所謂,但至少我們還有選擇。

在遇到<Redirect>元件時,總是會做瀏覽器重定向,但是如果它在<Switch>元件中,那麼只有當其他路由都沒有匹配到時,才會重定向。

“Index Routes” 和 “Not Found”

在V4中我們用exact屬性代替了<IndexRoute>來做同樣的事。在沒有路由匹配到時,使用<Redirect>來重定向到預設頁面的路徑,或者404頁面。

巢狀佈局

現在你可能已經開始思考如何實現巢狀佈局了。我原本以為我不會糾結於這個概念,但我沒有...V4提供了很多選擇,這就是為什麼V4如此強大。但是,多樣的選擇意味著我們可能選擇並不理想的方案。表面上看來,巢狀佈局很簡單,但是根據你的選擇不同,你可能會遇到一些麻煩因為你組織路由的方式。

想象這樣一個場景,我們需要一個‘瀏覽使用者’的頁面以及‘使用者資訊’的頁面。對於產品頁面我們也有類似需求。使用者和產品都需要一個子佈局,並且子佈局都是唯一特有的。舉例來說,每一個子佈局擁有不同的導航卡。我們有幾種不同的方式來解決這個問題,有些比較好有些則不太好。第一種方案可能並不是那麼好,但我還是想讓你們看一看,以免以後踩入這個坑。第二種方案就顯得更好一些。

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概念: 任何由<Route>渲染的元件,V4都會提供props.match。就像你看到的,userId這個引數是有props.match.params提供的。瞭解更多V4文件。如果一個元件並不是直接通過<Route>元件渲染的,但是卻要用到props.match,那麼我們可以藉助withRouter()這個高階元件。

實現上來說,第一種方案並沒有什麼問題。但是我們仔細觀察一下,會發現UserProfilePageBrowserUsersPage的佈局是一樣的,不同的只是UserProfileBrowserUserTable。這種實現方法會導致一些冗餘程式碼,並且在UserProfilePageBrowserUsersPage之間切換時,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>
  )
}

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>
)
複製程式碼

和之前實現方式不同的是,我們的路由從4個變成了2個。BrowseUsersPageUserProfilePage元件只負責渲染不同的部分,作為UserSubLayout的子元件。需要注意的一點是,不論你路由巢狀多深,還是需要寫完整路徑去匹配。如果你不想重複書寫路徑或者方便統一修改路徑,你可以使用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>
)
複製程式碼

Match

正如我們所見,props.match在獲取userId這類引數和簡化路由書寫方面很有用處。match物件提供了一些屬性:match.paramsmatch.pathmatch.url等等

match.path 和 match.url

第一眼我們並不能清楚的區分這兩者。控制檯列印時,多數情況下也是相同的結果。舉例來說,當瀏覽器路徑是‘/users’時,match.pathmatch.url列印輸出的是相同的值。

const UserSubLayout = ({ match }) => {
  console.log(match.url)   // output: "/users"
  console.log(match.path)  // output: "/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>
  )
}
複製程式碼

現在我們還是分不清這兩者的區別,但是,如果我們用巢狀的路由,特別是帶有引數的匹配模式,在更深一層的元件中列印,我們就能看到區別。比如我們瀏覽的是‘/users/5’這個路徑,match.url返回的是‘/users/5’,而match.path返回的是‘/users/:userId’。這樣就一目瞭然了,match.url返回的是具體的路由地址字串,而match.path返回的是路由匹配模式的字串。

如何選擇?

在用<Route>做巢狀路由時,建議使用match.path,因為你可能在你的子元件中需要使用match.param物件,在<Link>元件做路由跳轉時,使用match.url

避免match衝突

假設現在我們需要一個編輯頁面對使用者資訊進行編輯,那此頁面的路由規則應該類似於‘/users/:userId/edit’。那麼問題來了,前面的例子中,使用者資訊頁面的路由匹配規則是/users/:userId,當我們訪問‘/users/5/edit’的時候,將會首先匹配到使用者資訊頁面而不是編輯頁。那是否意味著我們要像之前做的那樣,在原有的資訊頁面同時新增一個編輯的子頁面,再去匹配呢?並不一定。我們可以通過將‘/users/:userId/edit’規則放在/users/:userId之前來達到此效果。或者對/users/:userId進行進一步約定,比如限制:userId為數字:users/:userId(\\d+)

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>
)
複製程式碼

授權路由

在我們的專案中,根據使用者的登入狀態來限制使用者訪問某些路由的功能是很常見的。讓授權頁面(應用程式的主要頁面)和未授權頁面(比如登入頁面以及忘記密碼頁面)擁有不同的UI和感受也是常見的需求。

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>
    )
  }
}
複製程式碼

這種方法有幾個關鍵點。首先,根據我們所處的應用程式的哪個部分,我在兩個頂級佈局之間進行選擇。訪問路徑,如“/auth/login”或“/auth/forget-password”將使用未經授權的佈局。當使用者登入時,我們將保證所有路徑都有一個' /app '字首,它使用AuthorizedRoute元件來確定使用者是否登入。如果使用者試圖訪問以' /app '開頭的頁面,但他們沒有登入,他們將被重定向到登入頁面。

但是AuthorizedRoute並不是V4本身提供的,而是我自己實現的。V4中一個驚人的新特性是能夠為特定目的建立自己的路由。不同於通過傳遞一個元件屬性到<Route>,而是傳遞一個rendr回撥:

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意味著請求還沒結束。

點選此連結你可以檢視完整的登入限制的程式碼實現。

登入限制動態圖

其他注意點

React-Router V4還有其他更多炫酷的特性。最後,我們來了解一些小特性,以防遇到時犯迷糊。

<Link><NavLink>

在V4中,有兩種方式可以將錨點和路由整合:<Link><NavLink>

<NavLink>的原理和<Link>一樣,不同的是,<NavLink>能根據瀏覽器URL是否匹配從而提供額外的樣式定製。詳情可以線上檢視該demo。部分程式碼如下:

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>匹配到當前URL時,它允許我們在此<NavLink>上新增一個自定義的class,以便控制樣式。

URL查詢字串

在V4中已經沒有方式可以直接得到URL的查詢字串了。在我看來,做出這個決定是因為對於如何處理複雜的查詢字串沒有標準。因此,v4沒有在模組中引入相關方法,而是決定讓開發人員選擇如何處理查詢字串。這是一件好事。

就我個人而言,我使用sindresorhus大神(twitter)編寫的query-string模組來處理查詢字串。

動態路由

V4最出色的一點是幾乎所有的東西都是一個React元件,包括<Route>。路由再也不是什麼魔法,我們可以在任何需要的時候有條件的渲染它們。想象一下,當滿足某些條件時,您的應用程式的整個部分都可用於路由。當這些條件不滿足時,我們可以刪除路由。我們甚至可以做一些炫酷的事,比如遞迴路由。

React-Router4變得更為簡單了,因為萬物皆元件。

相關文章