1. 程式人生 > >React Router 4 簡易入門

React Router 4 簡易入門

React Router4是一個流行的純React重寫的包。現在的版本中已不需要路由配置,現在一切皆元件。

本文涵蓋了開始使用React Router構建網站所需要的一切知識。我們將會為本地運動隊製作一個網站。

程式碼

想看網站最終效果,檢視demo

安裝

React Router被拆分成三個包:react-router,react-router-domreact-router-nativereact-router提供核心的路由元件與函式。其餘兩個則提供執行環境(即瀏覽器與react-native)所需的特定元件。

進行網站(將會執行在瀏覽器環境中)構建,我們應當安裝react-router-domreact-router-dom

暴露出react-router中暴露的物件與方法,因此你只需要安裝並引用react-router-dom即可。

npm install --save react-router-dom

路由器(Router)

在你開始專案前,你需要決定你使用的路由器的型別。對於網頁專案,存在<BrowserRouter><HashRouter>兩種元件。當存在服務區來管理動態請求時,需要使用<BrowserRouter>元件,而<HashRouter>被用於靜態網站。

通常,我們更傾向選擇<BrowserRouter>,但如果你的網站僅用來呈現靜態檔案,那麼<HashRouter>將會是一個好選擇。

對於我們的專案,將設將會有伺服器的動態支援,因此我們選擇<BrowserRouter>作為路由器元件。

歷史(History)

每個路由器都會建立一個history物件並用其保持追蹤當前location[注1]並且在有變化時對網站進行重新渲染。這個history物件保證了React Router提供的其他元件的可用性,所以其他元件必須在router內部渲染。一個React Router元件如果向父級上追溯卻找不到router元件,那麼這個元件將無法正常工作。

渲染<Router>

路由器元件無法接受兩個及以上的子元素。基於這種限制的存在,建立一個<App>元件來渲染應用其餘部分是一個有效的方法(對於服務端渲染,將應用從router元件中分離也是重要的)。

import { BrowserRouter } from 'react-router-dom'
ReactDOM.render((
  <BrowserRouter>
    <App />
  </BrowserRouter>
), document.getElementById('root'))

<App>

應用通過<App>元件定義。簡化一下,我們將應用拆分成兩個部分。<Header>元件包含網站的導航連結。<Main>元件則呈現其餘內容。

// this component will be rendered by our <___Router>
const App = () => (
  <div>
    <Header />
    <Main />
  </div>
)

注意:你可以按你喜歡的方式對應用佈局,但是將路由與導航拆分出來對於這個入門教程會成家簡單。

路由(Route)

<Route>元件是React Router中主要的結構單元。在任意位置只要匹配了URL的路徑名(pathname)你就可以建立<Route>元素進行渲染。

路徑(Path)

<Route>接受一個數為string型別的path,該值路由匹配的路徑名的型別。例如:<Route path='/roster'/>會匹配以/roster[注2]開頭的路徑名。在當前path引數與當前location的路徑相匹配時,路由就會開始渲染React元素。若不匹配,路由不會進行任何操作[注3]。

<Route path='/roster'/>
// 當路徑名為'/'時, path不匹配
// 當路徑名為'/roster''/roster/2'時, path匹配
// 當你只想匹配'/roster'時,你需要使用"exact"引數
// 則路由僅匹配'/roster'而不會匹配'/roster/2'
<Route exact path='/roster'/>

注意:在匹配路由時,React Router只關注location的路徑名。當URL如下時:

http://www.example.com/my-projects/one?extra=false

React Router去匹配的只是'/my-projects/one'這一部分。

匹配路徑

path-to-regexp包用來決定route元素的path引數與當前location是否匹配。它將路徑字串編譯成正則表示式,並與當前location的路徑名進行匹配比較。除了上面的例子外,路徑字串有更多高階的選項,詳見[path-to-regexp文件]。
當路由地址匹配成功後,會建立一個含有以下屬性的match物件

  • url :與當前location路徑名所匹配部分

  • path :路由的地址

  • isExact :path 是否等於 pathname

  • params :從path-to-regexp獲取的路徑中取出的值都被包含在這個物件中

使用route tester這款工具來對路由與URL進行檢驗。

注意:本例中路由路徑僅支援絕對路徑[注4]。

建立你的路由

可以在路由器(router)元件中的任意位置建立多個<Route>,但通常我們會把它們放在同一個位置。使用<Switch>元件來包裹一組<Route>。<Switch>會遍歷自身的子元素(即路由)並對第一個匹配當前路徑的元素進行渲染。

對於本網站,我們希望匹配一下路徑:

  • / : 主頁

  • /roster : 團體列表

  • /roster/:number :運動員頁面,使用運動員的編號作為標識

  • /schedule :團隊的賽程表

為了在應用中能匹配路徑,在建立<Route>元素時必須帶有需要匹配的path作為引數。

<Switch>
  <Route exact path='/' component={Home}/>
  {/* both /roster and /roster/:number begin with /roster */}
  <Route path='/roster' component={Roster}/>
  <Route path='/schedule' component={Schedule}/>
</Switch>

<Route>是如何渲染的?

當一個路由的path匹配成功後,路由用來確定渲染結果的引數有三種。只需要提供其中一個即可。

  • component : 一個React元件。當帶有component引數的route匹配成功後,route會返回一個新的元素,其為component引數所對應的React元件(使用React.createElement建立)。

  • render : 一個返回React element的函式[注5]。當匹配成功後呼叫該函式。該過程與傳入component引數類似,並且對於行級渲染與需要向元素傳入額外引數的操作會更有用。

  • children : 一個返回React element的函式。與上述兩個引數不同,無論route是否匹配當前location,其都會被渲染。

<Route path='/page' component={Page} />
const extraProps = { color: 'red' }
<Route path='/page' render={(props) => (
  <Page {...props} data={extraProps}/>
)}/>
<Route path='/page' children={(props) => (
  props.match
    ? <Page {...props}/>
    : <EmptyPage {...props}/>
)}/>

通常component引數與render引數被更經常地使用。children引數偶爾會被使用,它更常用在path無法匹配時呈現的'空'狀態。在本例中並不會有額外的狀態,所以我們將使用<Route>的component引數。

通過<Route>渲染的元素會被傳入一些引數。分別是match物件,當前location物件[注6]以及history物件(由router建立)[注7]。

<Main>

現在我們清楚了根路由的結構,我們需要實際渲染我們的路由。對於這個應用,我們將會在<Main>元件中渲染<Switch>與<Route>,這一過程會將route匹配生成的HTML放在<main>節點中。

import { Switch, Route } from 'react-router-dom'
const Main = () => (
  <main>
    <Switch>
      <Route exact path='/' component={Home}/>
      <Route path='/roster' component={Roster}/>
      <Route path='/schedule' component={Schedule}/>
    </Switch>
  </main>
)

注意:主頁路由包含額外引數。該引數用來保證路由能準確匹配path。

巢狀路由

運動員路由/roster/:number並未包含在上述<Switch>中。它由<Roster>元件負責在路徑包含'/roster'的情形下進行渲染。

在<Roster>元件中,我們將為兩種路徑進行渲染:

  • /roster :對應路徑名僅僅是/roster時,因此需要在exact元素上新增exact引數。

  • /roster/:number : 該路由使用一個路由引數來獲取/roster後的路徑名。

    const Roster = () => (
    <Switch>

    <Route exact path='/roster' component={FullRoster}/>
    <Route path='/roster/:number' component={Player}/>

    </Switch>
    )

    組合在相同元件中分享共同字首的路由是一種有用的方法。這就需要簡化父路由並且提供一個區域來渲染具有相同字首的通用路由。

例如,<Roster>用來渲染所有以/roster開始的全部路由。

const Roster = () => (
  <div>
    <h2>This is a roster page!</h2>
    <Switch>
      <Route exact path='/roster' component={FullRoster}/>
      <Route path='/roster/:number' component={Player}/>
    </Switch>
  </div>
)

路徑引數

有時路徑名中存在我們需要獲取的引數。例如,在運動員介面,我們需要獲取運動員的編號。我們可以向route的路徑字串中新增path引數

如'/roster/:number'中:number這種寫法意味著/roster/後的路徑名將會被獲取並存在match.params.number中。例如,路徑名'/roster/6'會獲取到一個物件:

{ number: '6' } // 注獲取的值是字串型別的

<Player>元件可以使用props.match.params物件來確定需要被渲染的運動員的資料。

// 返回運動員物件的API
import PlayerAPI from './PlayerAPI'
const Player = (props) => {
  const player = PlayerAPI.get(
    parseInt(props.match.params.number, 10)
  )
  if (!player) {
    return <div>Sorry, but the player was not found</div>
  }
  return (
    <div>
      <h1>{player.name} (#{player.number})</h1>
      <h2>{player.position}</h2>
    </div>
)

除了<Player>元件,我們的頁面還包含<FullRoster>, <Schedule>以及 <Home>元件。

const FullRoster = () => (
  <div>
    <ul>
      {
        PlayerAPI.all().map(p => (
          <li key={p.number}>
            <Link to={`/roster/${p.number}`}>{p.name}</Link>
          </li>
        ))
      }
    </ul>
  </div>
)
const Schedule = () => (
  <div>
    <ul>
      <li>6/5 @ Evergreens</li>
      <li>6/8 vs Kickers</li>
      <li>6/14 @ United</li>
    </ul>
  </div>
)
const Home = () => (
  <div>
    <h1>Welcome to the Tornadoes Website!</h1>
  </div>
)

Link

現在,我們應用需要在各個頁面間切換。如果使用錨點元素(就是)實現,在每次點選時頁面將被重新載入。React Router提供了<Link>元件用來避免這種狀況的發生。當你點選<Link>時,URL會更新,元件會被重新渲染,但是頁面不會重新載入。

import { Link } from 'react-router-dom'
const Header = () => (
  <header>
    <nav>
      <ul>
        <li><Link to='/'>Home</Link></li>
        <li><Link to='/roster'>Roster</Link></li>
        <li><Link to='/schedule'>Schedule</Link></li>
      </ul>
    </nav>
  </header>
)

<Link>使用'to'引數來描述需要定位的頁面。它的值即可是字串也可是location物件(包含pathname,search,hash與state屬性)。如果其值為字元床將會被轉換為location物件。

<Link to={{ pathname: '/roster/7' }}>Player #7</Link>

注意:本例的link的pathname屬性只能是絕對路徑[注4]。

例子

一個完整的網站例子

獲取路由

希望當下你已準備好深入構建你自己的網站了。

我們已經瞭解了構建網站所需要的所有必須元件(<BrowserRouter>, <Route>, 以及 <Link>)。當然,還有一些我們沒有涉及的元件。所幸React Router擁有優質的文件,你可以檢視並從中瞭解更多的資訊。文件也提供一系列的例子與原始碼。

註釋:

[1] locations 是一個含有描述URL不同部分屬性的物件:

// 一個基本的location物件
{ pathname: '/', search: '', hash: '', key: 'abc123' state: {} }

[2] 你可以渲染無路

React Router4是一個流行的純React重寫的包。現在的版本中已不需要路由配置,現在一切皆元件。

本文涵蓋了開始使用React Router構建網站所需要的一切知識。我們將會為本地運動隊製作一個網站。

程式碼

想看網站最終效果,檢視demo

安裝

React Router被拆分成三個包:react-router,react-router-domreact-router-nativereact-router提供核心的路由元件與函式。其餘兩個則提供執行環境(即瀏覽器與react-native)所需的特定元件。

進行網站(將會執行在瀏覽器環境中)構建,我們應當安裝react-router-domreact-router-dom暴露出react-router中暴露的物件與方法,因此你只需要安裝並引用react-router-dom即可。

npm install --save react-router-dom

路由器(Router)

在你開始專案前,你需要決定你使用的路由器的型別。對於網頁專案,存在<BrowserRouter><HashRouter>兩種元件。當存在服務區來管理動態請求時,需要使用<BrowserRouter>元件,而<HashRouter>被用於靜態網站。

通常,我們更傾向選擇<BrowserRouter>,但如果你的網站僅用來呈現靜態檔案,那麼<HashRouter>將會是一個好選擇。

對於我們的專案,將設將會有伺服器的動態支援,因此我們選擇<BrowserRouter>作為路由器元件。

歷史(History)

每個路由器都會建立一個history物件並用其保持追蹤當前location[注1]並且在有變化時對網站進行重新渲染。這個history物件保證了React Router提供的其他元件的可用性,所以其他元件必須在router內部渲染。一個React Router元件如果向父級上追溯卻找不到router元件,那麼這個元件將無法正常工作。

渲染<Router>

路由器元件無法接受兩個及以上的子元素。基於這種限制的存在,建立一個<App>元件來渲染應用其餘部分是一個有效的方法(對於服務端渲染,將應用從router元件中分離也是重要的)。

import { BrowserRouter } from 'react-router-dom'
ReactDOM.render((
  <BrowserRouter>
    <App />
  </BrowserRouter>
), document.getElementById('root'))

<App>

應用通過<App>元件定義。簡化一下,我們將應用拆分成兩個部分。<Header>元件包含網站的導航連結。<Main>元件則呈現其餘內容。

// this component will be rendered by our <___Router>
const App = () => (
  <div>
    <Header />
    <Main />
  </div>
)

注意:你可以按你喜歡的方式對應用佈局,但是將路由與導航拆分出來對於這個入門教程會成家簡單。

路由(Route)

<Route>元件是React Router中主要的結構單元。在任意位置只要匹配了URL的路徑名(pathname)你就可以建立<Route>元素進行渲染。

路徑(Path)

<Route>接受一個數為string型別的path,該值路由匹配的路徑名的型別。例如:<Route path='/roster'/>會匹配以/roster[注2]開頭的路徑名。在當前path引數與當前location的路徑相匹配時,路由就會開始渲染React元素。若不匹配,路由不會進行任何操作[注3]。

<Route path='/roster'/>
// 當路徑名為'/'時, path不匹配
// 當路徑名為'/roster''/roster/2'時, path匹配
// 當你只想匹配'/roster'時,你需要使用"exact"引數
// 則路由僅匹配'/roster'而不會匹配'/roster/2'
<Route exact path='/roster'/>

注意:在匹配路由時,React Router只關注location的路徑名。當URL如下時:

http://www.example.com/my-projects/one?extra=false

React Router去匹配的只是'/my-projects/one'這一部分。

匹配路徑

path-to-regexp包用來決定route元素的path引數與當前location是否匹配。它將路徑字串編譯成正則表示式,並與當前location的路徑名進行匹配比較。除了上面的例子外,路徑字串有更多高階的選項,詳見[path-to-regexp文件]。
當路由地址匹配成功後,會建立一個含有以下屬性的match物件

  • url :與當前location路徑名所匹配部分

  • path :路由的地址

  • isExact :path 是否等於 pathname

  • params :從path-to-regexp獲取的路徑中取出的值都被包含在這個物件中

使用route tester這款工具來對路由與URL進行檢驗。

注意:本例中路由路徑僅支援絕對路徑[注4]。

建立你的路由

可以在路由器(router)元件中的任意位置建立多個<Route>,但通常我們會把它們放在同一個位置。使用<Switch>元件來包裹一組<Route>。<Switch>會遍歷自身的子元素(即路由)並對第一個匹配當前路徑的元素進行渲染。

對於本網站,我們希望匹配一下路徑:

  • / : 主頁

  • /roster : 團體列表

  • /roster/:number :運動員頁面,使用運動員的編號作為標識

  • /schedule :團隊的賽程表

為了在應用中能匹配路徑,在建立<Route>元素時必須帶有需要匹配的path作為引數。

<Switch>
  <Route exact path='/' component={Home}/>
  {/* both /roster and /roster/:number begin with /roster */}
  <Route path='/roster' component={Roster}/>
  <Route path='/schedule' component={Schedule}/>
</Switch>

<Route>是如何渲染的?

當一個路由的path匹配成功後,路由用來確定渲染結果的引數有三種。只需要提供其中一個即可。

  • component : 一個React元件。當帶有component引數的route匹配成功後,route會返回一個新的元素,其為component引數所對應的React元件(使用React.createElement建立)。

  • render : 一個返回React element的函式[注5]。當匹配成功後呼叫該函式。該過程與傳入component引數類似,並且對於行級渲染與需要向元素傳入額外引數的操作會更有用。

  • children : 一個返回React element的函式。與上述兩個引數不同,無論route是否匹配當前location,其都會被渲染。

<Route path='/page' component={Page} />
const extraProps = { color: 'red' }
<Route path='/page' render={(props) => (
  <Page {...props} data={extraProps}/>
)}/>
<Route path='/page' children={(props) => (
  props.match
    ? <Page {...props}/>
    : <EmptyPage {...props}/>
)}/>

通常component引數與render引數被更經常地使用。children引數偶爾會被使用,它更常用在path無法匹配時呈現的'空'狀態。在本例中並不會有額外的狀態,所以我們將使用<Route>的component引數。

通過<Route>渲染的元素會被傳入一些引數。分別是match物件,當前location物件[注6]以及history物件(由router建立)[注7]。

<Main>

現在我們清楚了根路由的結構,我們需要實際渲染我們的路由。對於這個應用,我們將會在<Main>元件中渲染<Switch>與<Route>,這一過程會將route匹配生成的HTML放在<main>節點中。

import { Switch, Route } from 'react-router-dom'
const Main = () => (
  <main>
    <Switch>
      <Route exact path='/' component={Home}/>
      <Route path='/roster' component={Roster}/>
      <Route path='/schedule' component={Schedule}/>
    </Switch>
  </main>
)

注意:主頁路由包含額外引數。該引數用來保證路由能準確匹配path。

巢狀路由

運動員路由/roster/:number並未包含在上述<Switch>中。它由<Roster>元件負責在路徑包含'/roster'的情形下進行渲染。

在<Roster>元件中,我們將為兩種路徑進行渲染:

  • /roster :對應路徑名僅僅是/roster時,因此需要在exact元素上新增exact引數。

  • /roster/:number : 該路由使用一個路由引數來獲取/roster後的路徑名。

    const Roster = () => (
    <Switch>

    <Route exact path='/roster' component={FullRoster}/>
    <Route path='/roster/:number' component={Player}/>

    </Switch>
    )

    組合在相同元件中分享共同字首的路由是一種有用的方法。這就需要簡化父路由並且提供一個區域來渲染具有相同字首的通用路由。

例如,<Roster>用來渲染所有以/roster開始的全部路由。

const Roster = () => (
  <div>
    <h2>This is a roster page!</h2>
    <Switch>
      <Route exact path='/roster' component={FullRoster}/>
      <Route path='/roster/:number' component={Player}/>
    </Switch>
  </div>
)

路徑引數

有時路徑名中存在我們需要獲取的引數。例如,在運動員介面,我們需要獲取運動員的編號。我們可以向route的路徑字串中新增path引數

如'/roster/:number'中:number這種寫法意味著/roster/後的路徑名將會被獲取並存在match.params.number中。例如,路徑名'/roster/6'會獲取到一個物件:

{ number: '6' } // 注獲取的值是字串型別的

<Player>元件可以使用props.match.params物件來確定需要被渲染的運動員的資料。

// 返回運動員物件的API
import PlayerAPI from './PlayerAPI'
const Player = (props) => {
  const player = PlayerAPI.get(
    parseInt(props.match.params.number, 10)
  )
  if (!player) {
    return <div>Sorry, but the player was not found</div>
  }
  return (
    <div>
      <h1>{player.name} (#{player.number})</h1>
      <h2>{player.position}</h2>
    </div>
)

除了<Player>元件,我們的頁面還包含<FullRoster>, <Schedule>以及 <Home>元件。

const FullRoster = () => (
  <div>
    <ul>
      {
        PlayerAPI.all().map(p => (
          <li key={p.number}>
            <Link to={`/roster/${p.number}`}>{p.name}</Link>
          </li>
        ))
      }
    </ul>
  </div>
)
const Schedule = () => (
  <div>
    <ul>
      <li>6/5 @ Evergreens</li>
      <li>6/8 vs Kickers</li>
      <li>6/14 @ United</li>
    </ul>
  </div>
)
const Home = () => (
  <div>
    <h1>Welcome to the Tornadoes Website!</h1>
  </div>
)

Link

現在,我們應用需要在各個頁面間切換。如果使用錨點元素(就是)實現,在每次點選時頁面將被重新載入。React Router提供了<Link>元件用來避免這種狀況的發生。當你點選<Link>時,URL會更新,元件會被重新渲染,但是頁面不會重新載入。

import { Link } from 'react-router-dom'
const Header = () => (
  <header>
    <nav>
      <ul>
        <li><Link to='/'>Home</Link></li>
        <li><Link to='/roster'>Roster</Link></li>
        <li><Link to='/schedule'>Schedule</Link></li>
      </ul>
    </nav>
  </header>
)

<Link>使用'to'引數來描述需要定位的頁面。它的值即可是字串也可是location物件(包含pathname,search,hash與state屬性)。如果其值為字元床將會被轉換為location物件。

<Link to={{ pathname: '/roster/7' }}>Player #7</Link>

注意:本例的link的pathname屬性只能是絕對路徑[注4]。

例子

一個完整的網站例子

獲取路由

希望當下你已準備好深入構建你自己的網站了。

我們已經瞭解了構建網站所需要的所有必須元件(<BrowserRouter>, <Route>, 以及 <Link>)。當然,還有一些我們沒有涉及的元件。所幸React Router擁有優質的文件,你可以檢視並從中瞭解更多的資訊。文件也提供一系列的例子與原始碼。

註釋:

[1] locations 是一個含有描述URL不同部分屬性的物件:

// 一個基本的location物件
{ pathname: '/', search: '', hash: '', key: 'abc123' state: {} }

[2] 你可以渲染無路徑的<Route>,其將會匹配所有location。此法用於訪問存在上下文中的變數與方法。

[3] 如果你使用children引數,即便在當前location不匹配時route也將進行渲染。

[4] 當需要支援相對路徑的<Route>與<Link>時,你需要多做一些工作。相對<Link>將會比你之前看到的更為複雜。因其使用了父級的match物件而非當前URL來匹配相對路徑。

[5] 這是一個本質上無狀態的函式元件。內部實現,component引數與render引數的元件是用很大的區別的。使用component引數的元件會使用React.createElement來建立元素,使用render引數的元件則會呼叫render函式。如果我們定義一個行內函數並將其傳給component引數,這將會比使用render引數慢很多。

<Route path='/one' component={One}/>
// React.createElement(props.component)
<Route path='/two' render={() => <Two />}/>
// props.render()

[6] <Route>與<Switch>元件都會帶有location引數。這能讓你使用與實際location不同的location去匹配地址。

[7] 可以傳入staticContext引數,不過這僅在服務端渲染時有用。

徑的<Route>,其將會匹配所有location。此法用於訪問存在上下文中的變數與方法。

[3] 如果你使用children引數,即便在當前location不匹配時route也將進行渲染。

[4] 當需要支援相對路徑的<Route>與<Link>時,你需要多做一些工作。相對<Link>將會比你之前看到的更為複雜。因其使用了父級的match物件而非當前URL來匹配相對路徑。

[5] 這是一個本質上無狀態的函式元件。內部實現,component引數與render引數的元件是用很大的區別的。使用component引數的元件會使用React.createElement來建立元素,使用render引數的元件則會呼叫render函式。如果我們定義一個行內函數並將其傳給component引數,這將會比使用render引數慢很多。

<Route path='/one' component={One}/>
// React.createElement(props.component)
<Route path='/two' render={() => <Two />}/>
// props.render()

[6] <Route>與<Switch>元件都會帶有location引數。這能讓你使用與實際location不同的location去匹配地址。

[7] 可以傳入staticContext引數,不過這僅在服務端渲染時有用。