React Router 事實上是React官方的標準路由庫。當你在一個多檢視的React應用中來回切換,你需要一個路由來管理那些URL。React Router 專注於此,同步保持你應用的UI和URL。
這個教程主要給你介紹React Router 的v4版本,以及你使用它可以做的大部分事情。
開場白
React 是一個很流行的庫,用於在客戶端渲染建立的單頁應用(SPAs)。 一個SPA會有很多檢視(也可以稱為頁面),不像傳統的多頁應用,檢視之間的跳轉不應該導致整個頁面被重新載入。相反,我們希望檢視就在當前頁面裡渲染。那些習慣於多頁應用的終端使用者,期望在一個SPA中應該包含以下特性:
- 應用中每個檢視都應該有對應的唯一URL用來區分檢視。以便使用者可以在之後通過書籤收藏的URL指向引用資源 – 例如:
www.example.com/products
。 - 瀏覽器的前進後退按鈕應該正常工作。
- 動態生成的巢狀檢視更應該有成對應的URL – 例如:
example.com/products/shoes/101
,101是產品id。
路由跳轉是指在同步保持瀏覽器URL的過程中渲染頁面中的檢視。React Router 讓你宣告式的操作路由跳轉。宣告式路由方法,通過說“路由應該是這樣的”,允許你控制應用中的資料流:
1 |
`<Route path="/about" component={About}/>` |
你可以把<Router>元件放在任意你想要路由渲染的地方。由於我們所需要接觸的<Router>
,<Link>
以及其他React Router的API都只是元件,所以你可以非常方便的在React裡使用路由。
寫在開頭。有一個常見的誤區,大家都認為React Router是由facebook官方開發的一個路由解決方案。實際上,它是一個因其設計和簡易性而流行的第三方庫。如果你的需求只侷限於路由的跳轉,你可以無需太多麻煩,就可以從頭開始實現一個自定義的路由。但是,瞭解React Router的基礎知識可以讓你更清楚的認識一個路由是怎麼工作的。
概述
本次教程分為幾個部分。首先,我們使用npm安裝好React和React Router,然後我們就開始React Router的基礎部分。你將會看到React Router不同的程式碼示例的效果。本次教程涉及的例子包含:
- 基本路由跳轉
- 巢狀路由
- 帶路徑引數的巢狀路由
- 保護式路由
主要圍繞構建這些路由所涉及的概念進行討論。這個專案的全部程式碼在這個Github倉庫可以看到。當你進入一個單獨的demo目錄,執行npm install
來安裝依賴。要在本地伺服器上執行這個應用,執行npm start
,然後在瀏覽器開啟http://localhost:3000/
可以看到執行的demo。
讓我們開始吧!
安裝 React Router
假設你已經有一個React開發環境並已經執行了。如果沒有,可以跳轉到React和JSX入門。或者,你可以使用Create React App來生成建立一個基本的React專案所需要的檔案。這是Create React App生成的預設目錄結構:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
react-routing-demo-v4 ├── .gitignore ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── README.md ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ └── registerServiceWorker.js └── yarn.lock |
React Router庫包含三個包: react-router
, react-router-dom
, 和 react-router-native
。react-router
是路由的核心包,而其他兩個是基於特定環境的。如果你在開發一個網站,你應該使用react-router-dom
,如果你在移動應用的開發環境使用React Native,你應該使用react-router-native
。
使用npm安裝react-router-dom
:
1 |
`npm install --save react-router-dom` |
React Router 基礎
下面是路由的例子:
1 2 3 4 5 6 7 8 9 |
<router> <route exact="" path="/" component="{Home}/"> <route path="/category" component="{Category}/"> <route path="/login" component="{Login}/"> <route path="/products" component="{Products}/"/> </route> </route> </route> </router> |
Router
像上面的例子,你需要一個元件和一些
元件來建立一個基本的路由。由於我們建立的是一個基於瀏覽器的應用,我們可以從React Router API中使用這兩種型別的路由:
<BrowserRouter>
<HashRouter>
它們之間主要的區別,可以在它們所建立的URL明顯看出:
1 2 3 4 5 |
// http://example.com/about // http://example.com/#/about |
<BrowserRouter>
在兩者中更為常用,原因是它使用了HTML5的history API來記錄你的路由歷史。而<HashRouter>
則使用URL(
window.location.hash
)的hash部分來記錄。如果你想相容老式瀏覽器,你應該使用<HashRouter>
。
使用<BrowserRouter>
元件包裹App元件。
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/* Import statements */ import React from 'react'; import ReactDOM from 'react-dom'; /* App is the entry point to the React code.*/ import App from './App'; /* import BrowserRouter from 'react-router-dom' */ import { BrowserRouter } from 'react-router-dom'; ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter> , document.getElementById('root')); |
注意:Router元件只能有一個子元素。子元素可以是HTML – 例如div – 也可以是一個react元件。
要讓React Router工作,你需要從react-router-dom
庫引入相關的API。這裡,我在index.js
引入了BrowserRouter
,也從App.js
引入了App
元件。App.js
,如你所猜想的,是React元件的入口。
上述程式碼給我們整個App元件建立了一個history例項。接下來正式介紹下history。
history
history
是一個讓你輕鬆管理所有Javascript執行的會話記錄的Javascript庫。history提供了簡潔的API,讓你可以管理history堆疊,跳轉,確認跳轉,以及保持會話之間的狀態。 – 來自React 培訓文件
每個router元件建立了一個history物件,用來記錄當前路徑(history.location
),上一步路徑也儲存在堆疊中。當前路徑改變時,檢視會重新渲染,給你一種跳轉的感覺。當前路徑又是如何改變的呢?history物件有history.push()
和history.replace()
這些方法來實現。當你點選元件會觸發
history.push()
,使用則會呼叫
history.replace()
。其他方法 – 例如history.goBack()
和history.goForward()
– 用來根據頁面的後退和前進來跳轉history堆疊。
接下來,我們談談Links和Routes
Links and Routes
是React Router裡最重要的元件。若當前路徑匹配route的路徑,它會渲染對應的UI。理想來說,
應該有一個叫
path
的prop,當路徑名跟當前路徑匹配才會渲染。
另一方面,用來跳轉頁面。可以類比HTML的錨元素。然而,使用錨連結會導致瀏覽器的重新整理,這不是我們想要的。所以,我們可以使用
來跳轉至具體的URL,並且檢視重新渲染不會導致瀏覽器重新整理。
我們已經介紹了建立一個基本的路由需要的所有東西。讓我們試一個吧。
Demo 1: 基礎路由
src/App.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
/* Import statements */ import React, { Component } from 'react'; import { Link, Route, Switch } from 'react-router-dom'; /* Home component */ const Home = () => ( <div> <h2>Home</h2> </div> ) /* Category component */ const Category = () => ( <div> <h2>Category</h2> </div> ) /* Products component */ const Products = () => ( <div> <h2>Products</h2> </div> ) /* App component */ class App extends React.Component { render() { return ( <div> <nav className="navbar navbar-light"> <ul className="nav navbar-nav"> /* Link components are used for linking to other views */ <li><Link to="/">Homes</Link></li> <li><Link to="/category">Category</Link></li> <li><Link to="/products">Products</Link></li> </ul> </nav> /* Route components are rendered if the path prop matches the current URL */ <Route path="/" component={Home}/> <Route path="/category" component={Category}/> <Route path="/products" component={Products}/> </div> ) } } |
我們在App.js
裡定義了 Home,Category,和Products元件。儘管目前看起來沒問題,當元件變得越來越臃腫,最好將每個元件分成單獨的檔案。根據經驗,如果元件程式碼超過了10行,我通常會給它建立一個新的檔案。從第二個demo開始,我會將App.js
裡面越來越多的元件分成單獨的檔案。
在App元件中,我們寫了路由跳轉的邏輯。 的路徑與當前路徑匹配,對應元件就會被渲染。對應渲染的元件傳給了第二個prop–
component
。
在這裡,/
同時匹配/
和/category
。因此,所有路由都匹配並被渲染。我們該如何避免呢?應該給 path='/'
的路由傳遞exact= {true}
props:
1 |
`<Route exact={true} path="/" component={Home}/>` |
若只想要路由在路徑完全相同時渲染,你就可以使用exact
props。
巢狀路由
建立巢狀路由之前,我們需要更深入的理解如何執行。開始吧。
<Route>
有三個可以用來定義要渲染內容的props:
- component.在上面我們已經看到了。當URL匹配時,router會將傳遞的元件使用
React.createElement
來生成一個React元素。 - render. 適合行內渲染。在當前路徑匹配路由路徑時,
render
prop期望一個函式返回一個元素。 - children.
children
prop跟render
很類似,也期望一個函式返回一個React元素。然而,不管路徑是否匹配,children都會渲染。
Path and match
path用來標識路由匹配的URL部分。React Router使用了Path-to-RegExp庫將路徑字串轉為正規表示式。然後正規表示式會匹配當前路徑。
當路由路徑和當前路徑成功匹配,會生成一個物件,我們叫它match。match物件有更多關於URL和path的資訊。這些資訊可以通過它的屬性獲取,如下所示:
match.url
.返回URL匹配部分的字串。對於建立巢狀的很有用。
match.path
.返回路由路徑字串 – 就是。將用來建立巢狀的
。
match.isExact
.返回布林值,如果準確(沒有任何多餘字元)匹配則返回true。match.params
.返回一個物件包含Path-to-RegExp包從URL解析的鍵值對。
現在我們完全瞭解了,開始建立一個巢狀路由吧。
Switch元件
在我們開始示例程式碼籤,我想給你介紹下元件。當一起使用多個
時,所有匹配的routes都會被渲染。根據demo1的程式碼,我新增一個新的route來驗證為什麼
很有用。
1 2 3 4 |
<Route exact path="/" component={Home}/> <Route path="/products" component={Products}/> <Route path="/category" component={Category}/> <Route path="/:id" render = {()=> (<p> I want this text to show up for all routes other than '/', '/products' and '/category' </p>)}/> |
當URL為/products
,所有匹配/products
路徑的route都會被渲染。所以,那個path為:id
的<Route>會跟著
Products
元件一塊渲染。設計就是如此。但是,若這不是你想要的結果,你應該給你的routes新增<Switch>元件。有
<Switch>
元件的話,只有第一個匹配路徑的子
<Route>
會渲染。
Demo 2: 巢狀路由
之前,我們給/
, /category
and /products
建立了路由。但如果我們想要/category/shoes
這種形式的URL呢?
src/App.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import React, { Component } from 'react'; import { Link, Route, Switch } from 'react-router-dom'; import Category from './Category'; class App extends Component { render() { return ( <div> <nav className="navbar navbar-light"> <ul className="nav navbar-nav"> <li><Link to="/">Homes</Link></li> <li><Link to="/category">Category</Link></li> <li><Link to="/products">Products</Link></li> </ul> </nav> <Switch> <Route exact path="/" component={Home}/> <Route path="/category" component={Category}/> <Route path="/products" component={Products}/> </Switch> </div> ); } } export default App; /* Code for Home and Products component omitted for brevity */ |
不像React Router之前的版本,在版本4中,巢狀的最好放在父元素裡面。所以,Category元件就是這裡的父元件,我們將在父元件中定義
category/:name
路由。
src/Category.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import React from 'react'; import { Link, Route } from 'react-router-dom'; const Category = ({ match }) => { return( <div> <ul> <li><Link to={`${match.url}/shoes`}>Shoes</Link></li> <li><Link to={`${match.url}/boots`}>Boots</Link></li> <li><Link to={`${match.url}/footwear`}>Footwear</Link></li> </ul> <Route path={`${match.path}/:name`} render= {({match}) =>( <div> <h3> {match.params.name} </h3></div>)}/> </div>) } export default Category; |
首先,我們給巢狀路由定義了一些Link。之前提到過,match.url
用來構建巢狀連結,match.path
用來構建巢狀路由。如果你對match有不理解的概念,console.log(match)
會提供一些有用的資訊來幫助你瞭解它。
1 2 |
<Route path={`${match.path}/:name`} render= {({match}) =>( <div> <h3> {match.params.name} </h3></div>)}/> |
這是我們首次嘗試動態路由。不同於硬編碼路由,我們給pathname使用了變數。:name
是路徑引數,獲取category/
之後到下一條斜槓之間的所有內容。所以,類似products/running-shoes
的路徑名會生成如下的一個params
物件:
1 2 3 |
{ name: 'running-shoes' } |
引數可以通過match.params
或props.match.params
來獲取,取決於傳遞哪種props。另外有趣的是我們使用了render
prop。render
props非常適合行內函式,這樣不需要單獨拆分元件。
Demo 3: 帶Path引數的巢狀路由
我們讓事情變得再複雜一些,可以嗎?一個真實的路由應該是根據資料,然後動態展示。假設我們獲取了從服務端API返回的product資料,如下所示。
src/Products.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
const productData = [ { id: 1, name: 'NIKE Liteforce Blue Sneakers', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin molestie.', status: 'Available' }, { id: 2, name: 'Stylised Flip Flops and Slippers', description: 'Mauris finibus, massa eu tempor volutpat, magna dolor euismod dolor.', status: 'Out of Stock' }, { id: 3, name: 'ADIDAS Adispree Running Shoes', description: 'Maecenas condimentum porttitor auctor. Maecenas viverra fringilla felis, eu pretium.', status: 'Available' }, { id: 4, name: 'ADIDAS Mid Sneakers', description: 'Ut hendrerit venenatis lacus, vel lacinia ipsum fermentum vel. Cras.', status: 'Out of Stock' }, ]; |
我們需要根據下面這些路徑建立路由:
/products
. 這個路徑應該展示產品列表。/products/:productId
.如果產品有:productId
,這個頁面應該展示該產品的資料,如果沒有,就該展示一個錯誤資訊。
src/Products.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
/* Import statements have been left out for code brevity */ const Products = ({ match }) => { const productsData = [ { id: 1, name: 'NIKE Liteforce Blue Sneakers', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin molestie.', status: 'Available' }, //Rest of the data has been left out for code brevity ]; /* Create an array of `<li>` items for each product var linkList = productsData.map( (product) => { return( <li> <Link to={`${match.url}/${product.id}`}> {product.name} </Link> </li> ) }) return( <div> <div> <div> <h3> Products</h3> <ul> {linkList} </ul> </div> </div> <Route path={`${match.url}/:productId`} render={ (props) => <Product data= {productsData} {...props} />}/> <Route exact path={match.url} render={() => ( <div>Please select a product.</div> )} /> </div> ) } |
首先,我們通過productsData.id
建立一列,並把它儲存在
linkList
。路由從路徑字串根據匹配的對應產品id獲取引數。
1 2 |
<Route path={`${match.url}/:productId`} render={ (props) => <Product data= {productsData} {...props} />}/> |
你可能期望使用component = { Product }
來替代行內render函式。問題是,我們不僅需要productsData
,並順帶把剩餘prop也傳給Product元件。儘管你還有其他方法,不過我覺的這是最簡單的方法了。{...props}
使用ES6的擴充套件運算子 將所有prop傳給元件。
這是Product元件的程式碼。
src/Product.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/* Import statements have been left out for code brevity */ const Product = ({match,data}) => { var product= data.find(p => p.id == match.params.productId); var productData; if(product) productData = <div> <h3> {product.name} </h3> <p>{product.description}</p> <hr/> <h4>{product.status}</h4> </div>; else productData = <h2> Sorry. Product doesnt exist </h2>; return ( <div> <div> {productData} </div> </div> ) } |
find
方法用來查詢陣列中物件的id屬性等於match.params.productId
。如果product存在,productData
就會展示,如果不存在,“Product不存在”的資訊就會被渲染。
保護式路由
最後一個demo,我們將圍繞保護式路由的技術進行討論。那麼,如果有人想進入/admin
頁面,他們會被要求先登入。然而,在我們保護路由之前還需要考慮一些事情。
重定向
類似服務端重定向,會將history堆疊的當前路徑替換為新路徑。新路徑通過
to
prop傳遞。這是如何使用:
1 |
`<Redirect to={{pathname: '/login', state: {from: props.location}}}` |
如果有人已經登出了賬戶,想進入/admin
頁面,他們會被重定向到/login
頁面。當前路徑的資訊是通過state傳遞的,若使用者資訊驗證成功,使用者會被重定向回初始路徑。在子元件中,你可以通過this.props.location.state
獲取state的資訊。
自定義路由
自定義路由最適合描述元件裡巢狀的路由。如果我們需要確定一個路由是否應該渲染,最好的方法是寫個自定義元件。下面是通過其他路由來定義自定義路由。
src/App.js
1 2 3 4 5 6 7 |
/* Add the PrivateRoute component to the existing Routes */ <Switch> <Route exact path="/" component={Home} data={data}/> <Route path="/category" component={Category}/> <Route path="/login" component={Login}/> <PrivateRoute authed={fakeAuth.isAuthenticated} path='/products' component = {Products} /> </Switch> |
若使用者已登入,fakeAuth.isAuthenticated
返回true,反之亦然。
這是PrivateRoute的定義。
src/App.js
1 2 3 4 5 6 7 8 9 10 |
/* PrivateRoute component definition */ const PrivateRoute = ({component: Component, authed, ...rest}) => { return ( <Route {...rest} render={(props) => authed === true ? <Component {...props} /> : <Redirect to={{pathname: '/login', state: {from: props.location}}} />} /> ) } |
如果使用者已登入,路由將渲染Admin元件。否則,使用者將重定義到 /login
登入頁面。這樣做的好處是,定義更明確,而且PrivateRoute
可以複用。
最後,下面是Login元件的程式碼:
src/Login.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
import React from 'react'; import { Redirect } from 'react-router-dom'; class Login extends React.Component { constructor() { super(); this.state = { redirectToReferrer: false } // binding 'this' this.login = this.login.bind(this); } login() { fakeAuth.authenticate(() => { this.setState({ redirectToReferrer: true }) }) } render() { const { from } = this.props.location.state || { from: { pathname: '/' } } const { redirectToReferrer } = this.state; if (redirectToReferrer) { return ( <Redirect to={from} /> ) } return ( <div> <p>You must log in to view the page at {from.pathname}</p> <button onClick={this.login}>Log in</button> </div> ) } } /* A fake authentication function */ export const fakeAuth = { isAuthenticated: false, authenticate(cb) { this.isAuthenticated = true setTimeout(cb, 100) }, } |
下面這行是物件的解構賦值的示例,es6的特性之一。
1 |
`const { from } = this.props.location.state || { from: { pathname: '/' } }` |
讓我們把所有片段拼湊到一塊,好嗎?這是我們使用React Router建立的應用最終效果:
Demo 4: 保護式路由
總結
如你在本文中所看到的,React Router是一個幫助React構建更完美,更宣告式的路由庫。不像React Router之前的版本,在v4中,一切就“只是元件”。而且,新的設計模式也更完美的使用React的構建方式來實現。
在本次教程中,我們學到了:
- 如何配置和安裝React Router
- 基礎版路由,和一些基礎元件,例如
,
和
- 如何構建一個有導航功能的極簡路由和巢狀路由
- 如何根據路徑引數構建動態路由
最後,我們還學習了一些高階路由技巧,用來建立保護式路由的最終demo。
感謝作者: Manjunath M