React Router 4:痛過之后的豁然開朗

第一次獨(dú)自承擔(dān)一個(gè)項(xiàng)目,在進(jìn)行技術(shù)選型時(shí),由于看到了 React Router 4 的文檔十分完善,果斷地選擇了它,盡管公司里現(xiàn)有的項(xiàng)目用的是它之前的版本。然而,看著這份華麗麗的文檔對我來說也是一次痛苦的旅程。

剛開始構(gòu)建項(xiàng)目的整體框架時(shí),我照貓畫虎,將以前項(xiàng)目中的 Router 結(jié)構(gòu)照搬了過來,只不過路由庫是最新的 v4,也采用了其中比較簡單的語法。然而,到項(xiàng)目結(jié)構(gòu)越發(fā)深入時(shí),看著那篇完善的參考文檔,我卻犯難了,對路由和模塊之間的對應(yīng)關(guān)系感到困惑,不知如何下手o(╥﹏╥)o。直到看到了這篇文章 All About React Router 4,我才豁然開朗。原來 React Router 4 與之前的版本是完全不同的兩種模式,只有理解了它的模式,才能在項(xiàng)目中使用起來游刃有余。

對于 React Router v2/v3 版本的學(xué)習(xí),阮老師的文章 React Router 使用教程 寫得通俗易懂,而 v4 版本則可直接去看 官網(wǎng)的資源

All About React Router 4 中,作者介紹了 v4 中一些新的基本特性,并與 v3 的某些特性進(jìn)行了對比(VIEW DEMO),看完獲益頗豐,以下是我的不完全翻譯,以及自己實(shí)踐的心得。

一、核心區(qū)別

React Router 4 與之前的版本最大的不同便是 router 在項(xiàng)目中的位置:

  • v2/v3 的版本采用的方式是將路由看成是一個(gè)整體的單元,與別的組件是分離的,一般會單獨(dú)放到一個(gè) router 文件中,對其進(jìn)行集中式管理;并且,布局和頁面的嵌套由路由的嵌套所決定。
import { Router, Route, IndexRoute, browserHistory } 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>

// 集中的 router,也可將其單獨(dú)放到一個(gè) router 文件中
const App = () => (
  <Router history={browserHistory}>
    <Route path="/" component={PrimaryLayout}>
      <IndexRoute component={HomePage} />
      <Route path="/users" component={UsersPage} />
    </Route>
  </Router>
)

render(<App />, document.getElementById('root'))
  • v4 的版本則將路由進(jìn)行了拆分,將其放到了各自的模塊中,不再有單獨(dú)的 router 模塊,充分體現(xiàn)了組件化的思想;另外,<BrowserRouter> 的使用與之前作為 history 屬性傳入的方式也不同了。
// v4 中改從 'react-router-dom' 引入的原因是因?yàn)檫€有個(gè) native 版本,這個(gè)意味著是 web 版本
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'))

React Router v4 的這種方式讓路由和組件之間的關(guān)系變得特別好理解,可以將 Route 就當(dāng)做 component 組件一樣使用,只不過此時(shí)的 URL 是其對應(yīng)的 path;當(dāng) path 匹配時(shí),則會渲染 Route 所對應(yīng)的組件內(nèi)容。

二、包含式路由與exact

在之前的版本中,在 Route 中寫入的 path,在路由匹配時(shí)是獨(dú)一無二的,而 v4 版本則有了一個(gè)包含的關(guān)系:如匹配 path="/users" 的路由會匹配 path="/"的路由,在頁面中這兩個(gè)模塊會同時(shí)進(jìn)行渲染。因此,v4中多了 exact 關(guān)鍵詞,表示只對當(dāng)前的路由進(jìn)行匹配。

// 當(dāng)匹配 /users 時(shí),會同時(shí)渲染 UsersMenu 和 UsersPage
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>
)

三、獨(dú)立路由:Switch

如果想要只匹配一個(gè)路由,除了 exact 屬性之外,還可以使用 Swtich 組件。

const PrimaryLayout = () => (
  <div className="primary-layout">
    <PrimaryHeader />
    <main>
      <Switch>
        <Route path="/" exact component={HomePage} />  // 必須加上 exact,要不然 /users 也會匹配到該路由
        <Route path="/users/add" component={UserAddPage} />
        <Route path="/users" component={UsersPage} />
        <Redirect to="/" />
      </Switch>
    </main>
  </div>
)

采用 <Switch>,只有一個(gè)路由會被渲染,并且總是渲染第一個(gè)匹配到的組件。因此,在第一個(gè)路由中,還是需要使用 exact,否則,當(dāng)我們渲染 '/users' 或 '/users/add' 時(shí),只會顯示匹配 '/' 的組件(PS:如果不使用 <Switch>,當(dāng)我們不使用 exact 時(shí),會渲染匹配的多個(gè)組件)。所以,將 '/user/add' 路由放在 '/users' 之前更好,因?yàn)楹笳甙饲罢撸?dāng)然,我們也可以同樣使用 exact,這樣就可以不用關(guān)注順序了。

再來說一下 <Redirect> 組件,單獨(dú)使用時(shí),一旦當(dāng)路由匹配到的時(shí)候,瀏覽器就會進(jìn)行重定向跳轉(zhuǎn);而配合 <Switch> 使用時(shí),只有當(dāng)沒有路由匹配的時(shí)候,才會進(jìn)行重定向。例如,上面的例子,地址欄輸入 '/test' 時(shí),則會跳轉(zhuǎn)到 '/',渲染 HomePage 頁面。

四、"Index Routes" 和 "Not Found"

在 v4 的版本中廢棄了 <IndexRoute>,而該用 <Route exact> 的方式進(jìn)行代替。如果沒有匹配的路由,也可通過 <Redirect> 來進(jìn)行重定向到默認(rèn)頁面或合理的路徑。

五、嵌套布局

首先,作者在文中給出了嵌套布局的場景:如想要擴(kuò)展“用戶模塊”,需要有一個(gè)“瀏覽用戶”的頁面和“每個(gè)用戶個(gè)人信息”的頁面,對于“產(chǎn)品模塊”也是同樣。對于此類場景,作者在文中給出了兩種實(shí)現(xiàn)方案進(jìn)行對比。

首先,是最容易想到的方案,但是卻不是很理想:

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>
)

很容易看到,BrowseUsersPageUserProfilePage 的布局是重復(fù)的,每次渲染子頁面的時(shí)候,都會渲染整體的布局。如果項(xiàng)目比較大的話,會產(chǎn)生很多重復(fù)冗余的代碼,也會影響整體的性能。

來看一下另一種 較優(yōu)的方法,充分利用了 Route 的組件化思想:

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>
)

const BrowseUsersPage = () => <BrowseUserTable />
const UserProfilePage = props => <UserProfile userId={props.match.params.userId} />

這種方法將包含子頁面的模塊單獨(dú)看成一個(gè)整體模塊,然后將子模塊嵌套在該模塊中,那么當(dāng)整體模塊渲染的時(shí)候,布局就一次性渲染了;當(dāng)匹配子模塊路由的時(shí)候,就只會單獨(dú)渲染子模塊的那一部分。要 注意 的一點(diǎn)是:子頁面還是需要明確父模塊的路徑(如 '/users'),以保證能夠被匹配到。

將路由隨著模塊走,以組件化搭積木的方式來完成項(xiàng)目,真的是清晰很多啊~~

還可以通過 match 等對路徑進(jìn)行優(yōu)化,減少重復(fù)性的代碼輸入:

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

const UserProfilePage = ({match}) => <UserProfile userId={match.params.userId} />

// 以下是自己加的測試代碼
const UserNav = () => (
    <div>User Nav</div>
)
const BrowseUserTable = ({match}) => (
    <ul>
        <li><Link to={`${match.path}/bob`}>Bob</Link></li>
        <li><Link to={`${match.path}/Tom`}>Tom</Link></li>
        <li><Link to={`${match.path}/Jack`}>Jack</Link></li>
    </ul>
)
const UserProfile = ({ userId }) => <div>User: {userId}</div>;

六、Match

props.match 包含4個(gè)屬性match.paramsmatch.isExactmatch.pathmatch.url

看一下 <UserProfile />match 屬性:

match

1)match.path vs match.url

當(dāng)沒有參數(shù)的時(shí)候,match.pathmatch.url 是一樣的,而當(dāng)有參數(shù)的時(shí)候,兩者就有區(qū)別了:

  • match.path:是指寫在 <Route> 中的 path 參數(shù);
  • match.url:是指在瀏覽器中顯示的真實(shí) URL。
const UserSubLayout = ({ match }) => {
    console.log(match.path)   // output: "/users"
    console.log(match.url)  // output: "/users"
    return (
      <div className="user-sub-layout">
        <aside>
          <UserNav />
        </aside>
        <div className="primary-content">
          <Switch>
            <Route path={match.path} exact component={BrowseUserTable} />
            <Route path={`${match.path}/:userId`} component={UserProfilePage} />
          </Switch>
        </div>
      </div>
    )
  }

const UserProfilePage = ({match}) => {
    console.log(match.path); // output: "/users/:userId"
    console.log(match.url); // output: "/users/bob"
    return <UserProfile userId={match.params.userId} />
}

作者強(qiáng)烈建議在寫路由路徑時(shí)使用 match.path,因?yàn)槭褂?match.url 最終會產(chǎn)生不可預(yù)料的場景,如下面這個(gè)例子:

const UserComments = ({ match }) => {
    console.log(match.params);  // output: {}
    return <div>UserId: {match.params.userId}</div>
}

const UserSettings = ({ match }) => {
    console.log(match.params);  // output: {userId: "5"}
    return <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>
)
  • 當(dāng)訪問 '/users/5/comments' 時(shí)渲染 'UserId: undefined';
  • 當(dāng)訪問 '/users/5/settings' 時(shí)渲染 'UserId: 5'。

為什么會 match.path 能夠正常渲染,而使用 match.url 則不能呢?造成這種區(qū)別的原因是由于 {${match.url}/comments} 相當(dāng)于硬編碼 {'/users/5/comments'},在路徑中并沒有參數(shù),只有一個(gè)寫死的 5,這樣,子模塊便無法獲取到 match.params 參數(shù),因此,便不能正常渲染。

match.path 可用于構(gòu)造嵌套的 <Route>,而 match.url 可用于構(gòu)造嵌套的 <Link>

2)如何避免 Match 的沖突?

考慮這樣一種情況:如果我們想要通過 '/users/add' 和 '/users/5/edit' 來對用戶進(jìn)行添加和編輯,但是在前面的例子中,我們知道 users/:userId 已經(jīng)指向了 UserProfilePage,那按照之前的例子,是否意味著 users/:userId 需要指向一個(gè)可以同時(shí)實(shí)現(xiàn)編輯和預(yù)覽功能的子頁面模塊?未必如此,因?yàn)?edit 和 profile 共享一個(gè)子頁面,則可以通過以下方式進(jìn)行實(shí)現(xiàn):

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>
)

將 add 和 edit 頁面放在 profile 之前,這樣就可以實(shí)現(xiàn)按需匹配,如果將 profile 路徑放在第一位的話,那么當(dāng)訪問 add 頁面時(shí),則會匹配 profile 頁面,因?yàn)?add 匹配了 :userId

還有一種替代方法,可以將 profile 放在第一位:采用正則(path-to-regexp)對路徑進(jìn)行約束,如${match.path}/:userId(\\d+),這樣 :userId 只能為 number 類型,則訪問 /users/add 路徑時(shí)便不會產(chǎn)生沖突。

七、其它

文中,作者最后自行實(shí)現(xiàn)了一個(gè)授權(quán)路由,另外,他還提到了React Router v4 中的其它部分,如 <Link> vs <NavLink>、URL Query Strings 以及 Dynamic Routes。

這里我比較感興趣的是 URL 查詢,因?yàn)樵陧?xiàng)目開發(fā)中,我需要通過 URL 的查詢字符串來實(shí)現(xiàn)一些功能。如 URL的查詢字符串為 /users?bar=baz:

  • 在之前的版本中,可以通過 this.props.location.query.bar 進(jìn)行獲取(React Router 使用教程 );
  • 在 v4 中,首先看一下 this.props.location
props.location

由此可見,v4 版本中的 query 參數(shù)已經(jīng)不見了,也就意味著,在該版本中,已經(jīng)無法獲得 URL 的查詢字符串了。但是,我們卻可以獲取到 search 字段,只不過需要我們自行對其進(jìn)行處理。文中,作者推薦了 query-string 用于處理 URL 查詢字符串。

★、彩蛋(^_-)

嘿嘿~推薦一部國慶看了2遍的電影:《Me Before You》

剛開始只覺得女主的眉毛很喜感,看完深深地喜歡上了這個(gè)樂觀善良、十分有趣的姑娘(有趣的靈魂萬里挑一);特別喜歡她的穿著打扮,給絕望之中的人帶去生機(jī)與活力。印象最深刻的是女主在灑滿陽光的床上被鬧鈴叫醒,然后打扮地美美的迎接每一天,搭配著《Happy with me》的音樂,感覺為每天沉悶單調(diào)的生活點(diǎn)綴上了一抹絢麗的色彩。

一句話概括:美好而感傷的愛情故事啊~ps:看完電影去刷豆瓣的時(shí)候,才發(fā)現(xiàn)女主是《權(quán)利的游戲》中的龍媽~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容