目錄:
前端路由
react-router 中常見概念
react-router 使用實例
React-Router GitHub地址:https://github.com/ReactTraining/react-router
本文完整例子:https://codesandbox.io/embed/charming-dream-916y1
文章比較長,很大一部分是截圖和示例代碼。
一、前端路由
前端路由原理很簡單:檢測瀏覽器 URL 變化,截獲 URL 地址,然后進行URL 路由匹配。
兩種路由實現(xiàn)方式:hash-mode、html5-mode。
1.1、hash-mode
在html5 標準落地之前,都是通過監(jiān)聽?hashchange?事件,當 URL 變化時,進行頁面跳轉(zhuǎn)。

例子:https://jsbin.com/xexogiy/5/edit?html,js,console,output
<!DOCTYPE html><html><head><metacharset="utf-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><title></title><linkrel="stylesheet"href=""></head><body><buttonid="toggle">提交</button></body><script>const$el=document.getElementById('toggle')$el.addEventListener('click',(e)=>{e.preventDefault()location.hash='#'+Math.floor(Math.random()*1000000)})window.addEventListener('hashchange',(e)=>{const{oldURL,newURL}=econsole.log(oldURL,newURL)})</script></html>
需要注意的是,通過hash方式的,路由地址會有?#:
http://test.com/#/a
http://test.com/#/b
1.2、html5-mode
html5標準中,為 history 添加了pushState()、replaceState()方法,以及?onpopstate?事件。但原理和 hash 方式是相同的。
window.onpopstatedeveloper.mozilla.org/zh-CN/docs/Web/API/Window/onpopstate

通過 html5-mode 實現(xiàn)單頁路由的 URL 沒有?#。但因為沒有?#?,所以當用戶刷新頁面之類的操作時,瀏覽器還是會給服務器發(fā)送請求。
為了避免出現(xiàn)這種情況,所以這個實現(xiàn)需要服務器的支持,需要把所有路由都重定向到根頁面。
通過?html5-mode?方式的,路由地址沒有?#:
http://test.com/a
http://test.com/b
二、react-router 常見概念
主流的前端框架也都有自己的路由,比如 Backbone、Ember、Angular、React 等等,React-Router 是 React 生態(tài)的基礎路由庫,它通過管理 URL,實現(xiàn)組件的切換和狀態(tài)的變化。
React-Router?v5.0.1是目前最新版本。
React Router: Declarative Routing for Reactreacttraining.com/react-router/web/guides/quick-start
ReactTraining/react-routergithub.com/ReactTraining/react-router/releases

我們來看看各版本的差異
2.1、v4 vs pre-v4
v4 是目前大多數(shù)項目中使用的穩(wěn)定版本,不同于 pre-v4,v4屬于動態(tài)路由,而pre-v4 屬于靜態(tài)路由。
在 v4 中 React-Router 倉庫被拆分成了多個包進行發(fā)布:react-router、react-router-dom、react-router-native、react-router-config。
react-router:路由基礎庫
react-router-dom:適用于瀏覽器環(huán)境的再次封裝
react-router-native:適用于react-native環(huán)境的再次封裝
react-router-config:靜態(tài)路由配置助手
2.2、v5 vs v4
原本只是計劃發(fā)布 React Router 4.4 版本,但由于錯誤地使用了托字符 (^) —— 將依賴錯誤地寫成 "react-router": "^4.3.1",導致報錯。最后團隊決定撤銷 4.4 版本,直接改為發(fā)布 React Router v5。
完全兼容老版本v4
支持 React 16 , 兼容 React >= 15
消除在 React.StrictMode?嚴格模式中的警告
使用?create-react-context?實現(xiàn) context API
2.3、靜態(tài)路由
所謂“靜態(tài)路由”,就是說路由規(guī)則是固定的。只要應用程序一啟動,映射關(guān)系就固定不變。無論 express、Angular 還是 Rails 等業(yè)界響當當?shù)目蚣?,都用的是靜態(tài)路由。以 express 為例,路由規(guī)則差不多是這么寫的:
app.get('/',Home);app.get('/product/:id',Product);app.get('/about',About);
特點:
路由集中在一個地方。
布局和頁面嵌套是通過<Route>組件的嵌套而來的。
我們可以:
初始化路由的時候,加點業(yè)務邏輯去動態(tài)生成路由配置
通過路由參數(shù)(query)可以動態(tài)控制頁面的輸出
react-router v3 靜態(tài)路由
import{Router,Route,IndexRoute}from'react-router'constPrimaryLayout=props=>(<divclassName="primary-layout"><header>OurReactRouter3App</header><main>{props.children}</main></div>)constHomePage=()=><div>HomePage</div>constUsersPage=()=><div>UsersPage</div>constApp=()=>(<Routerhistory={browserHistory}><Routepath="/"component={PrimaryLayout}><IndexRoutecomponent={HomePage}/><Routepath="/users"component={UsersPage}/></Route></Router>)render(<App/>,document.getElementById('root'))
路由配置集中在 App 函數(shù)中,<Route> 直接嵌套于 <Router>,頁面組件融于其中作為路由的一部分。這便是整個路由的配置。
2.4、動態(tài)路由
所謂動態(tài)路由,指的是路由規(guī)則不是預先確定的,而是在渲染過程中確定的。
既然 React 組件渲染是動態(tài)發(fā)生的,那么就讓 Route 變成一個 React 組件,和其他組件一樣被渲染,在運行時完全可以決定某個Route渲染還是不渲染,也可以決定渲染這個 Route 的參數(shù) props 是怎樣,如此一來,路由規(guī)則也就完全動態(tài)了。
程墨Morgan:React Router v4 幾乎誤我一生181 贊同 · 76 評論文章

react-router v4 例子
import{BrowserRouter,Route}from'react-router-dom'constPrimaryLayout=()=>(<divclassName="primary-layout"><header>OurReactRouter4App</header><main><Routepath="/"exactcomponent={HomePage}/><Routepath="/users"component={UsersPage}/></main></div>)constHomePage=()=><div>HomePage</div>constUsersPage=()=><div>UsersPage</div>constApp=()=>(<BrowserRouter><PrimaryLayout/></BrowserRouter>)render(<App/>,document.getElementById('root'))
路由穿插在了各組件中,在嵌套路由(Nested Routes)的場景尤為明顯。
2.5、Router 組件
react-router 的工作方式,是在組件樹頂層放一個 Router 組件,然后在組件樹中散落著很多 Route 組件(注意比 Router 少一個“r”),頂層的 Router 組件負責分析監(jiān)聽 URL 的變化,在它之下的 Route 組件可以直接讀取這些信息。Router 是“提供者”,Route是“消費者”。
BrowserRouter:使用于現(xiàn)代瀏覽器,支持H5 history API
HashRouter:常用于舊款瀏覽器。根據(jù)不同的hashType
MemoryHistory:常用于非DOM環(huán)境,例如React Native或者測試,History存于內(nèi)存。
BrowserRouter 方式的路由:
https://xxx.com/ahttps://xxx.com/b
HashRouter 方式的路由:
https://xxx.com/#/ahttps://xxx.com/#/b
引入方式:
import{Route,BrowserRouterasRouter}from"react-router-dom";
說明:
對于瀏覽器項目我們通常選用: <BrowserRouter 和 <HashRouter> 組件來實現(xiàn) Router
react-native 項目我們通常選用:<MemoryHistory>
2.6、Route 組件
Route只是一個具有渲染方法的普通 React 組件,路由匹配成功則渲染該組件。
三個常用屬性:
path:路由的匹配規(guī)則(可以省略)
exact:用于精確匹配路由(可以省略)
component:需要渲染的組件
所以一個完整的路由:
import{Route,BrowserRouterasRouter}from"react-router-dom";importAfrom'A'importBfrom'B'constrouting=(<Router><Routecomponent={Home}><Routeexactpath="/a"component={A}/><Routeexactpath="/b"component={B}/></Router>)ReactDOM.render(routing,document.getElementById("root"));
2.7 Link 與 NavLink 組件
對于單頁應用,需要在不同頁面之間切換,往往需要一個導航欄,我們在這里也實現(xiàn)一個簡單的導航欄。
在App.js中,我們讓網(wǎng)頁由兩個組件?Navigation?和?Content?組成,?Navigation?就是導航欄,而?Content?是具體內(nèi)容。
classAppextendsComponent{render(){return(<divclassName="App"><Navigation/><Content/></div>);}}
NavLink?組件,是基于?Link?組件,它有一個?activeClassName?屬性,目的在于如果路由匹配成功,則為當前導航添加選中樣式。
Navigation 組件:
importReactfrom'react';classNavigationextendsComponent{render(){<div><NavLinkactiveClassName="active"to="/a">A</NavLink><NavLinkactiveClassName="active"to="/b">B</NavLink></div>}}
Content組件:
importReactfrom"react";importAfrom'./A';importBfrom'./B';import404from'./404'constrouting=(<Router><Switch><Routeexactpath="/a"component={A}/><Routeexactpath="/b"component={B}/><Routecomponent={404}/></Switch></Router>);
2.8 history對象
每個 router 都會創(chuàng)建一個 history 對象,用來保持對當前位置的追蹤還有在頁面發(fā)生變化的時候重新渲染頁面。
React Router 提供的其他組件依賴在 context 上儲存的 history 對象,所以他們必須在 router 對象的內(nèi)部渲染。
一個沒有 router 祖先元素的 React Router 對象將無法正常工作。
三、React-Router 實例
React Router: Declarative Routing for Reactreacttraining.com/react-router/web/guides/quick-start
前面提到,v4 之前是靜態(tài)路由,v4之后是動態(tài)路由。
在單頁面應用程序中,只有一個html頁面,我們重復使用相同的 html 頁面,根據(jù)導航渲染不同的組件。但是在多頁面應用程序中,當您導航時,您將從服務器獲得一個全新的頁面。
3.1、安裝
為了方便我們使用?create-react-app?來創(chuàng)建應用程序
npxcreate-react-approutingcdrouting
安裝 package
npm install react-router-dom
npm start? ? // dev環(huán)境 運行
創(chuàng)建如下目錄結(jié)構(gòu):
public/index.html
<!DOCTYPE html><htmllang="en"><head><metacharset="utf-8"><metaname="viewport"content="width=device-width, initial-scale=1, shrink-to-fit=no"><metaname="theme-color"content="#000000"><linkrel="manifest"href="%PUBLIC_URL%/manifest.json"><linkrel="shortcut icon"href="%PUBLIC_URL%/favicon.ico"><title>React App</title></head><body><divid="root"></div></body></html>
src/index.js
importReactfrom"react";importReactDOMfrom"react-dom";import"./index.css";importAppfrom"./App";importUsersfrom"./Users";importContactfrom"./Contact";ReactDOM.render(<App/>,document.getElementById("root"));
src/App.js
importReactfrom'react'classAppextendsReact.Component{render(){return(<div><h1>Home</h1></div>)}}exportdefaultApp
src/Users.js
importReactfrom"react";classUsersextendsReact.Component{render(){return<h1>Users</h1>;}}exportdefaultUsers;
src/Contact.js
importReactfrom"react";classContactextendsReact.Component{render(){return<h1>Contact</h1>;}}exportdefaultContact;
此時我們有三個 組件:APP、Users、Contact
3.2、Route 組件:路由控制
打開入口文件 index.js, 并導入 三個組件(App、Users、Concat),然后導入?react-router-dom,react-router?給了我們?nèi)齻€組件(Route、Link、BrowserRouter),幫助我們實現(xiàn)路由。
src/index.js
importReactfrom'react'importReactDOMfrom'react-dom'import'./index.css'import{Route,Link,BrowserRouterasRouter}from'react-router-dom'importAppfrom'./App'importUsersfrom'./Users'importContactfrom'./Contact'constrouting=(<Router><div><Routepath="/"component={App}/><Routepath="/users"component={Users}/><Routepath="/contact"component={Contact}/></div></Router>)ReactDOM.render(routing,document.getElementById('root'))
在右側(cè)是瀏覽器地址欄訪問/urser/?運行效果:
但是,此時你會發(fā)現(xiàn)一個問題,/?與?/users?所對應的兩個組件 Home、Users 都被渲染。這是因為?/users?既能夠被?/匹配 也能夠被?/users?匹配。我們需要使用?exact?屬性進行精確匹配。
src/index.js
importReactfrom"react";importReactDOMfrom"react-dom";import"./index.css";import{Route,Link,BrowserRouterasRouter}from"react-router-dom";importAppfrom"./App";importUsersfrom"./Users";importContactfrom"./Contact";constrouting=(<Router><div><Routeexactpath="/"component={App}/><Routepath="/users"component={Users}/><Routepath="/contact"component={Contact}/></div></Router>);ReactDOM.render(routing,document.getElementById("root"));
再次運行,此時只會渲染 Users 組件。
3.3、Link 組件:實現(xiàn)導航
src/index.js
importReactfrom'react'importReactDOMfrom'react-dom'import'./index.css'import{Route,Link,BrowserRouterasRouter}from'react-router-dom'importAppfrom'./App'importUsersfrom'./Users'importContactfrom'./Contact'constrouting=(<Router><div><ul><li><Linkto="/">Home</Link></li><li><Linkto="/users">Users</Link></li><li><Linkto="/contact">Contact</Link></li></ul><Routeexactpath="/"component={App}/><Routepath="/users"component={Users}/><Routepath="/contact"component={Contact}/></div></Router>)ReactDOM.render(routing,document.getElementById('root'))
在頁面上能夠看到導航欄的渲染結(jié)果,點擊導航會渲染對應的 組件
3.4、Switch組件:實現(xiàn)404頁面
404 代表頁面不存在(not found),也就是導航路徑不存在的時候,我們需要讓用戶感知到提示信息,此時就需要添加404頁面。
react-router 提供了一個Switch?組件,用于支持當路由匹配成功,渲染對應組件。若未能匹配路由,則渲染404組件。
創(chuàng)建 404 頁面?Notfound.js
importReactfrom'react'constNotfound=()=><h1>Notfound</h1>exportdefaultNotfound
index.js
importReactfrom"react";importReactDOMfrom"react-dom";import"./index.css";import{Route,Link,BrowserRouterasRouter,Switch}from"react-router-dom";importAppfrom"./App";importUsersfrom"./Users";importContactfrom"./Contact";importNotfoundfrom"./Notfound";constrouting=(<Router><div><ul><li><Linkto="/">Home</Link></li><li><Linkto="/users">Users</Link></li><li><Linkto="/contact">Contact</Link></li></ul><Switch><Routeexactpath="/"component={App}/><Routepath="/users"component={Users}/><Routepath="/contact"component={Contact}/><Routecomponent={Notfound}/></Switch></div></Router>);ReactDOM.render(routing,document.getElementById("root"));
此時我們訪問一個不存在的路由地址:/notfound
3.5、Path 屬性的 URL 參數(shù)
url params 能夠讓我們實現(xiàn)動態(tài)渲染同一組件,比如:/users/1、/users/2、/users/3?分別對應3個不同的用戶。語法如下:
<Route path="users/:id" component={Users} />
index.js
importReactfrom"react";importReactDOMfrom"react-dom";import"./index.css";import{Route,Link,BrowserRouterasRouter,Switch}from"react-router-dom";importAppfrom"./App";importUsersfrom"./Users";importContactfrom"./Contact";importNotfoundfrom"./Notfound";constrouting=(<Router><div><ul><li><Linkto="/">Home</Link></li><li><Linkto="/users">Users</Link></li><li><Linkto="/contact">Contact</Link></li></ul><Switch><Routeexactpath="/"component={App}/><Routepath="/users/:id"component={Users}/><Routepath="/contact"component={Contact}/><Routecomponent={Notfound}/></Switch></div></Router>);ReactDOM.render(routing,document.getElementById("root"));
在 Users.js 中?console.log,打印傳遞的 URL 參數(shù)
Users.js
import React from 'react'
class Users extends React.Component {
? render() {
? ? console.log(this.props)
? ? return <h1>Users</h1>
? }
}
export default Users
再次訪問:/users/1?你會得到如下結(jié)果:
我們將?console.log?的信息渲染到頁面上,對 Users.js 進行簡單改造:
Users.js
importReactfrom"react";classUsersextendsReact.Component{render(){const{params}=this.props.match;return(<div><h1>Users</h1><p>{params.id}</p></div>);}}exportdefaultUsers;
3.6、嵌套路由
嵌套路由,能夠幫助我們實現(xiàn)子路由渲染。比如:/users/1,/users/2。
importReactfrom"react";importReactDOMfrom"react-dom";import"./index.css";import{Route,Link,BrowserRouterasRouter,Switch}from"react-router-dom";importAppfrom"./App";importUsersfrom"./Users";importContactfrom"./Contact";importNotfoundfrom"./Notfound";constrouting=(<Router><div><ul><li><Linkto="/">Home</Link></li><li><Linkto="/users">Users</Link></li><li><Linkto="/contact">Contact</Link></li></ul><hr/><Switch><Routeexactpath="/"component={App}/><Routepath="/users"component={Users}/><Routepath="/contact"component={Contact}/><Routecomponent={Notfound}/></Switch> </div></Router>);ReactDOM.render(routing,document.getElementById("root"));
在 Users.js 文件中,需要導入?react-router-dom?package 中的?Route、Link組件,用于在Users組件中實現(xiàn)子路由。
Users.js
importReactfrom"react";import{Route,Link}from"react-router-dom";constUser=({match})=><p>{match.params.id}</p>;classUsersextendsReact.Component{render(){const{url}=this.props.match;return(<div><h1>Users</h1><strong>selectauser</strong><ul><li><Linkto="/users/1">User1</Link></li><li><Linkto="/users/2">User2</Link></li><li><Linkto="/users/3">User3</Link></li></ul><Routepath="/users/:id"component={User}/></div>);}}exportdefaultUsers;
運行效果:
3.7、NavLink組件: 實現(xiàn)選中樣式
NavLink它用于根據(jù)不同路由設置不同的樣式,以便用戶知道在網(wǎng)站上瀏覽的頁面。
NavLink 與 Link 的區(qū)別:
Link 用于導航站點上的不同路由。
NavLink是一種特殊的Link,用于將樣式屬性添加到當前路由。
在我們的路由應用程序中,我們有三個路徑(home,/ users,/ contact)讓我們使用NavLink設置它們的樣式。
activeClassName 屬性
我們需要向NavLink組件添加一個名為activeClassName 的新屬性,以便路由匹配成功時,能夠添加選中class狀態(tài)。
index.css
.active{color:red;}
index.js
importReactfrom"react";importReactDOMfrom"react-dom";import"./index.css";import{Route,NavLink,BrowserRouterasRouter,Switch}from"react-router-dom";importAppfrom"./App";importUsersfrom"./Users";importContactfrom"./Contact";importNotfoundfrom"./Notfound";constrouting=(<Router><div><ul><li><NavLinkexactactiveClassName="active"to="/">Home</NavLink></li><li><NavLinkactiveClassName="active"to="/users">Users</NavLink></li><li><NavLinkactiveClassName="active"to="/contact">Contact</NavLink></li></ul><hr/><Switch><Routeexactpath="/"component={App}/><Routepath="/users"component={Users}/><Routepath="/contact"component={Contact}/><Routecomponent={Notfound}/></Switch></div></Router>);ReactDOM.render(routing,document.getElementById("root"));
運行后,可以看到?red 樣式?應用于當前路由:
3.8、history對象: 實現(xiàn)程序化導航
什么是程序化導航?
我們需要在當前路由狀態(tài)下發(fā)生路由改變時,重定向用戶到新的路由。例如,當用戶成功登錄時,用戶被重定向到個人主頁。
如何在?react-router?中以編程方式導航?
要以編程方式導航,我們需要獲取由 react 路由器傳遞的歷史對象的幫助。讓我們在?Contact?組件中添加一個聯(lián)系表單。
history 對象中有一個 push 方法,使用push方法,只要用戶提交表單,我們就會將用戶重定向到 Home page。
concat.js
importReactfrom"react";classContactextendsReact.Component{onSubmit=()=>{this.props.history.push("/");};render(){return(<form><inputplaceholder="name"type="name"/><inputplaceholder="email"type="email"/><buttononClick={this.onSubmit}>Submit</button></form>);}}exportdefaultContact;