你不知道的 React Router 4

幾個月前,React Router 4 發佈,我能清晰地感受到來自 Twitter 你們對新版本中其 大量的修改 的不一樣聲音。誠然,我在學習 React Router 4 的第一天,也是很是痛苦的,可是,這並非由於看它的 API,而是反覆思考使用它的模式策略,由於 V4 的變化確實有點大,V3 的功能它都有,除此以外,還增長了一些特性,我不能直接將使用 V3 的心得直接遷移過來,如今,我必須從新審視 routerlayout components 之間的關係react

圖片描述

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

不過,在閱讀下文以前,你得首先保證如下的 概念 對你來講 並不陌生react-router

  • React stateless(Functional) 組件app

  • ES6 的 箭頭函數 和它的 隱式返回less

  • ES6 的 解構dom

  • ES6 的 模板字符串ide

若是你就是那 萬中無一 的絕世高手,那麼你也能夠選擇直接 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> 嵌套,實現 Layoutpage 嵌套

  • Layoutpage 組件 是做爲 router 的一部分

咱們使用 V4 來實現相同的應用程序對比一下

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

注意,咱們如今 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 規則是 exclusive,意思就是最終只獲取一個 route,而 V4 中的 routes 默認是 inclusive 的,這就意味着多個 <Route> 能夠同時匹配呈現

仍是使用上面的 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>;

如今,當訪問 /user 時,兩個組價都會被渲染,在 V3 中存在一些模式也能夠實現,但過程實在是複雜,在 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,若是不這麼作,你們能夠嘗試交換它們兩個的位置,看下會發生什麼

固然,若是咱們給每個 <Route> 都添加一個 exact,那就不用考慮上面的 策略 了,但無論怎樣,如今至少知道了咱們還有其它選擇

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

"Index Routes" 和 "Not Found"

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

嵌套佈局

接下來,你可能很想知道 V4 中是如何實現 嵌套佈局 的,V4 確實給咱們了不少選擇,但這並不必定是好事,表面上,嵌套佈局 微不足道,但選擇的空間越大,出現的問題也就可能越多

如今,咱們假設咱們要增長兩個 user 相關的頁面,一個 browse user,一個 user profile,對 product 咱們也有相同的需求,實現的方法可能並很多,但有的仔細思考後可能並不想採納

第一種,以下修改 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>
  );
};

雖然這種方法能夠實現,但仔細觀察下面的兩個 user 頁面,就會發現有點潛在的 問題

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() 高階組件來實現

估計你們都發現了吧,兩個 user 頁面中都有一個<UserNav />,這明顯會致使沒必要要的請求,以上只是一個簡單實例,若是是在真實的項目中,不知道會重複消耗多少的流量,然而,這就是由咱們以上方式使用路由引發的

接下來,咱們再看看另外一種實現方式

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 的責任,如今,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>;

如今是否是解決了第一種方式中的生命週期,重複渲染的問題呢?

但有一點值得注意的是,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

最開始,可能以爲這二者的區別並不明顯,控制檯常常出現相同的輸出,好比,訪問 /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 在組件的參數中被解構,意思就是咱們可使用 match.path 代替 props.match.path

雖然咱們看不到什麼明顯的差別,但須要明白的是 match.url 是瀏覽器 URL 的一部分,match.path 是咱們爲 router 書寫的路徑

如何選擇

若是咱們是構建 route 路徑,那麼確定使用 match.path

爲了說明問題,咱們建立兩個子組件,一個 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

實踐後,咱們發現,訪問 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>;

如今,看清楚這個策略了麼

另外,咱們使用 ${match.path}/:userId(\\d+) 做爲 UserProfilePage 對應的 path,保證 :userId 是一個數字,能夠避免與 /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>
    )
  }
}

如今,咱們首先會去選擇應用程序在哪一個頂級佈局中,好比,/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 確實存在較大的優點

原文連接:All About React Router 4 (BRAD WESTFALL)

相關文章
相關標籤/搜索