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。