你不知道的 React Router 4

你不知道的 React Router 4

幾個月前,React Router 4 發(fā)布,我能清晰地感覺到來自 Twitter 大家對新版本中其 大量的修改 的不同聲音。誠然,我在學習 React Router 4 的第一天,也是非常痛苦的,但是,這并不是因為看它的 API,而是反復思考使用它的模式策略,因為 V4 的變化確實有點大,V3 的功能它都有,除此之外,還增加了一些特性,我不能直接將使用 V3 的心得直接遷移過來,現(xiàn)在,我必須重新審視 routerlayout components 之間的關系

本篇文章不是把 React Router 4API 再次呈現(xiàn)給讀者看,而是簡單介紹其中最常用的幾個概念,和重點講解我在實踐的過程中發(fā)現(xiàn)的比較好的 模式策略

不過,在閱讀下文之前,你得首先保證以下的 概念 對你來說 并不陌生

  • React stateless(Functional) 組件
  • ES6 的 箭頭函數(shù) 和它的 隱式返回
  • ES6 的 解構
  • ES6 的 模板字符串

如果你就是那 萬中無一 的絕世高手,那么你也可以選擇直接 view demo

一個全新的 API

React Router 的早期版本是將 routerlayout components 分開,為了徹底搞清楚 V4 究竟有什么不同,我們來寫兩個簡單的 example 就明白了

example app 就兩個 routes,一個 home,一個 user

V3

import React from "react";
import { render } from "react-dom";
import { Router, Route, IndexRoute, Link, browserHistory } from "react-router";

const PrimaryLayout = props =>
  <div className="primary-layout">
    <header>Our React Router 3 App</header>
    <ul>
      <li>
        <Link to="/">Home</Link>
      </li>
      <li>
        <Link to="/user">User</Link>
      </li>
    </ul>
    <main>
      {props.children}
    </main>
  </div>;

const HomePage = () => <h1>Home Page</h1>;
const UsersPage = () => <h1>User Page</h1>;

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

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

上篇文章給大家推薦了一個在線 react 編譯器 stackblitz,本篇文章再給大家推薦一個不錯的,codesandbox,專門針對 react 且開源,正所謂,實踐是檢驗真理的唯一標準,這也是一種良好的學習習慣

上面代碼中有幾個關鍵的點在 V4 中就不復存在了

  • 集中式 router
  • 通過 <Route> 嵌套,實現(xiàn) Layoutpage 嵌套
  • Layoutpage 組件 是作為 router 的一部分

我們使用 V4 來實現(xiàn)相同的應用程序對比一下

import React from "react";
import { render } from "react-dom";
import { BrowserRouter, Route, Link } from "react-router-dom";

const PrimaryLayout = () =>
  <div className="primary-layout">
    <header>Our React Router 4 App</header>
    <ul>
      <li>
        <Link to="/">Home</Link>
      </li>
      <li>
        <Link to="/User">User</Link>
      </li>
    </ul>
    <main>
      <Route path="/" exact component={HomePage} />
      <Route path="/user" component={UsersPage} />
    </main>
  </div>;

const HomePage = () => <h1>Home Page</h1>;
const UsersPage = () => <h1>User Page</h1>;

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

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

注意,我們現(xiàn)在 import 的是 BrowserRouter,而且是從 react-router-dom 引入,而不是 react-router

接下來,我們用肉眼就能看出很多的變化,首先,V3 中的 router 不在了,在 V3 中,我們是將整個龐大的 router 直接丟給 DOM,而在 V4 中,除了 BrowserRouter, 我們丟給 DOM 的是我們的應用程序本身

另外,V4 中,我們不再使用 {props.children} 來嵌套組件了,替代的 <Route>,當 route 匹配時,子組件會被渲染到 <Route> 書寫的地方

Inclusive Routing

在上面的 example 中,讀者可能注意到 V4 中有 exact 這么一個 props,那么,這個 props 有什么用呢? V3 中的 routing 規(guī)則是 exclusive,意思就是最終只獲取一個 route,而 V4 中的 routes 默認是 inclusive 的,這就意味著多個 <Route> 可以同時匹配呈現(xiàn)

還是使用上面的 example,如果我們調皮地刪除 exact 這個 props,那么我們在訪問 /user 的時候,HomeUser 兩個 Page 都會被渲染,是不是一下就明白了

為了更好地理解 V4 的匹配邏輯,可以查看 path-to-regexp,就是它決定 routes 是否匹配 URL

為了演示 inclusive routing 的作用,我們新增一個 UserMenu 組件如下

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

現(xiàn)在,當訪問 /user 時,兩個組價都會被渲染,在 V3 中存在一些模式也可以實現(xiàn),但過程實在是復雜,在 V4 中,是不是感覺輕松了很多

Exclusive Routing

如果你只想匹配一個 route,那么你也可以使用 <Switch>exclusive routing

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

<Switch> 中只有一個 <Route> 會被渲染,另外,我們還是要給 HomePage 所在 <Route> 添加 exact,否則,在訪問 /user/user/add 的時候還是會匹配到 /,從而,只渲染 HomePage。同理,不知有沒同學注意到,我們將 /user/add 放在 /user 前面是保證正確匹配的很有策略性的一步,因為,/user/add 會同時匹配 /user/user/add,如果不這么做,大家可以嘗試交換它們兩個的位置,看下會發(fā)生什么

當然,如果我們給每一個 <Route> 都添加一個 exact,那就不用考慮上面的 策略 了,但不管怎樣,現(xiàn)在至少知道了我們還有其它選擇

<Redirect> 組件不用多說,執(zhí)行瀏覽器重定向,但它在 <Switch> 中時,<Redirect> 組件只會在 routes 匹配不成功的情況下渲染,另外,要想了解 <Redirect> 如何在 non-switch 環(huán)境下使用,可以參考下面的 Authorized Route

"Index Routes" 和 "Not Found"

V4 中也沒有 <IndexRoute>,但 <Route exact> 可以實現(xiàn)相同的功能,或者 <Switch><Redirect> 重定向到默認的有效路徑,甚至一個找不到的頁面

嵌套布局

接下來,你可能很想知道 V4 中是如何實現(xiàn) 嵌套布局 的,V4 確實給我們了很多選擇,但這并不一定是好事,表面上,嵌套布局 微不足道,但選擇的空間越大,出現(xiàn)的問題也就可能越多

現(xiàn)在,我們假設我們要增加兩個 user 相關的頁面,一個 browse user,一個 user profile,對 product 我們也有相同的需求,實現(xiàn)的方法可能并不少,但有的仔細思考后可能并不想采納

第一種,如下修改 PrimaryLayout

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

雖然這種方法可以實現(xiàn),但仔細觀察下面的兩個 user 頁面,就會發(fā)現(xiàn)有點潛在的 問題

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

userId 通過 props.match.params 獲取,props.match 賦予給了 <Route> 中的任何組件。除此之外,如果組件不通過 <Route> 來渲染,要訪問 props.match,可以使用 withRouter() 高階組件來實現(xiàn)

估計大家都發(fā)現(xiàn)了吧,兩個 user 頁面中都有一個<UserNav />,這明顯會導致不必要的請求,以上只是一個簡單實例,如果是在真實的項目中,不知道會重復消耗多少的流量,然而,這就是由我們以上方式使用路由引起的

接下來,我們再看看另一種實現(xiàn)方式

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

我們用 2 個 routes 替換之前的 4 個 routes

注意,這里我們沒有再使用 exact,因為,我們希望 /user 可以匹配任何以 /user 開始的 routeproducts 同理

使用這種策略,子布局也開始承擔起了渲染 routes 的責任,現(xiàn)在,UserSubLayout 長這樣

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

現(xiàn)在是不是解決了第一種方式中的生命周期,重復渲染的問題呢?

但有一點值得注意的是,routes 需要識別它的完整路徑才能匹配,為了減少我們的重復輸入,我們可以使用 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 可以幫我們獲取 userIdroutes

match 對象為我們提供了 match.paramsmatch.path,和 match.url 等屬性

match.path vs match.url

最開始,可能覺得這兩者的區(qū)別并不明顯,控制臺經常出現(xiàn)相同的輸出,比如,訪問 /user

const UserSubLayout = ({ match }) => {
  console.log(match.url)   // output: "/user"
  console.log(match.path)  // output: "/user"
  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>
  )
}

match 在組件的參數(shù)中被解構,意思就是我們可以使用 match.path 代替 props.match.path

雖然我們看不到什么明顯的差異,但需要明白的是 match.url 是瀏覽器 URL 的一部分,match.path 是我們?yōu)?router 書寫的路徑

如何選擇

如果我們是構建 route 路徑,那么肯定使用 match.path

為了說明問題,我們創(chuàng)建兩個子組件,一個 route 路徑來自 match.url,一個 route 路徑來自 match.path

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

然后,我們按下面方式來訪問

  • /user/5/comments
  • /user/5/settings

實踐后,我們發(fā)現(xiàn),訪問 comments 返回 undefined,訪問 settings 返回 5

正如 API 所述

match:
path - (string) The path pattern used to match. Useful for building nested <Route>s
url - (string) The matched portion of the URL. Useful for building nested <Link>s

避免 Match Collisions

假設我們的 App 是一個儀表盤,我們希望訪問 /user/add/user/5/edit 添加和編輯 user。使用上面的實例,user/:userId 已經指向 UserProfilePage,我們這是需要在 UserProfilePage 中再添加一層 routes 么?顯示不是這樣的

const UserSubLayou = ({ match }) =>
  <div className="user-sub-layout">
    <aside>
      <UserNav />
    </aside>
    <div className="primary-content">
      <Switch>
        <Route exact path={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>;

現(xiàn)在,看清楚這個策略了么

另外,我們使用 ${match.path}/:userId(\\d+) 作為 UserProfilePage 對應的 path,保證 :userId 是一個數(shù)字,可以避免與 /users/add 的沖突,這樣,將其所在的 <Route> 丟到最前面去也能正常訪問 add 頁面,這一招,就是我在 path-to-regexp 學的

Authorized Route

在應用程序中限制未登錄的用戶訪問某些路由是非常常見的,還有對于授權未授權的用戶 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>
    )
  }
}

現(xiàn)在,我們首先會去選擇應用程序在哪個頂級布局中,比如,/auth/login/auth/forgot-password 肯定在 UnauthorizedLayout 中,另外,當用戶登陸時,我們將判斷所有的路徑都有一個 /app 前綴以確保是否登錄。如果用戶訪問 /app 開頭的頁面但并沒有登錄,我們將會重定向到登錄頁面

下面就是我寫的 AuthorizedRoute 組件,這也是 V4 中一個驚奇的特性,可以為了滿足某種需要而書寫自己的路由

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

點擊 這里 可以查看的我的整個 Authentication

總結

React Router 4 相比 V3,變化很大,若是之前的項目使用的 V3,不建議立即升級,但 V4V3 確實存在較大的優(yōu)勢

原文鏈接:All About React Router 4

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容