React系列十九 - 掌握react-router
一. 認識react-router
1.1. 認識前端路由
路由其實是網絡工程中的一個術語:在架構一個網絡時,非常重要的兩個設備就是路由器和交換機。
當然,目前在我們生產中路由器也是越來越被大家所熟知,因爲我們生活中都會用到路由器:
-
事實上,路由器主要維護的是一個映射表;
-
映射表會決定數據的流向;
路由的概念出現最早是在後端路由中實現的,原因是web的發展主要經歷了這樣一些階段:
-
後端路由階段;
-
前後端分離階段;
-
單頁面富應用(SPA);
階段一:後端路由階段
早期的網站開發整個HTML頁面是由服務器來渲染的.
-
服務器直接生產渲染好對應的HTML頁面, 返回給客戶端進行展示.
但是, 一個網站, 這麼多頁面服務器如何處理呢?
-
一個頁面有自己對應的網址, 也就是URL.
-
URL會發送到服務器, 服務器會通過正則對該URL進行匹配, 並且最後交給一個Controller進行處理.
-
Controller進行各種處理, 最終生成HTML或者數據, 返回給前端.
-
這就完成了一個IO操作.
上面的這種操作, 就是後端路由.
-
當我們頁面中需要請求不同的 路徑 內容時, 交給服務器來進行處理, 服務器渲染好整個頁面, 並且將頁面返回給客戶端.
-
這種情況下渲染好的頁面, 不需要單獨加載任何的js和css, 可以直接交給瀏覽器展示, 這樣也有利於SEO的優化.
後端路由的缺點:
-
一種情況是整個頁面的模塊由後端人員來編寫和維護的.
-
另一種情況是前端開發人員如果要開發頁面, 需要通過PHP和Java等語言來編寫頁面代碼.
-
而且通常情況下HTML代碼和數據以及對應的邏輯會混在一起, 編寫和維護都是非常糟糕的事情.
階段二:前後端分離階段
前端渲染的理解:
-
每次請求涉及到的靜態資源都會從靜態資源服務器獲取,這些資源包括HTML+CSS+JS,然後在前端對這些請求回來的資源進行渲染;
-
需要注意的是,客戶端的每一次請求,都會從靜態資源服務器請求文件;
-
同時可以看到,和之前的後端路由不同,這時後端只是負責提供API了;
前後端分離階段:
-
隨着Ajax的出現, 有了前後端分離的開發模式;
-
後端只提供API來返回數據,前端通過Ajax獲取數據,並且可以通過JavaScript將數據渲染到頁面中;
-
這樣做最大的優點就是前後端責任的清晰,後端專注於數據上,前端專注於交互和可視化上;
-
並且當移動端(iOS/Android)出現後,後端不需要進行任何處理,依然使用之前的一套API即可;
-
目前很多的網站依然採用這種模式開發(jQuery開發模式);
階段三:單頁面富應用(SPA)
單頁面富應用的理解:
-
單頁面富應用的英文是single-page application,簡稱SPA;
-
整個Web應用只有實際上只有一個頁面,當URL發生改變時,並不會從服務器請求新的靜態資源;
-
而是通過JavaScript監聽URL的改變,並且根據URL的不同去渲染新的頁面;
如何可以應用URL和渲染的頁面呢?前端路由
-
前端路由維護着URL和渲染頁面的映射關係;
-
路由可以根據不同的URL,最終讓我們的框架(比如Vue、React、Angular)去渲染不同的組件;
-
最終我們在頁面上看到的實際就是渲染的一個個組件頁面;
1.2. 前端路由原理
前端路由是如何做到URL和內容進行映射呢?監聽URL的改變。
URL的hash
-
URL的hash也就是錨點(#), 本質上是改變window.location的href屬性;
-
我們可以通過直接賦值location.hash來改變href, 但是頁面不發生刷新;
<div id="app"> <a href="#/home">home</a> <a href="#/about">about</a> <div class="router-view"></div> </div> <script> // 1.獲取router-view const routerViewEl = document.querySelector(".router-view"); // 2.監聽hashchange window.addEventListener("hashchange", () => { switch(location.hash) { case "#/home": routerViewEl.innerHTML = "home"; break; case "#/about": routerViewEl.innerHTML = "about"; break; default: routerViewEl.innerHTML = "default"; } }) </script>
hash的優勢就是兼容性更好,在老版IE中都可以運行,但是缺陷是有一個#,顯得不像一個真實的路徑。
HTML5的History
history接口是HTML5新增的, 它有l六種模式改變URL而不刷新頁面:
-
replaceState:替換原來的路徑;
-
pushState:使用新的路徑;
-
popState:路徑的回退;
-
go:向前或向後改變路徑;
-
forword:向前改變路徑;
-
back:向後改變路徑;
我們這裏來簡單演示幾個方法:
<div id="app"> <a href="/home">home</a> <a href="/about">about</a> <div class="router-view"></div> </div> <script> // 1.獲取router-view const routerViewEl = document.querySelector(".router-view"); // 2.監聽所有的a元素 const aEls = document.getElementsByTagName("a"); for (let aEl of aEls) { aEl.addEventListener("click", (e) => { e.preventDefault(); const href = aEl.getAttribute("href"); console.log(href); history.pushState({}, "", href); historyChange(); }) } // 3.監聽popstate和go操作 window.addEventListener("popstate", historyChange); window.addEventListener("go", historyChange); // 4.執行設置頁面操作 function historyChange() { switch(location.pathname) { case "/home": routerViewEl.innerHTML = "home"; break; case "/about": routerViewEl.innerHTML = "about"; break; default: routerViewEl.innerHTML = "default"; } } </script>
1.3. react-router
目前前端流行的三大框架, 都有自己的路由實現:
-
Angular的ngRouter
-
React的ReactRouter
-
Vue的vue-router
React Router的版本4開始,路由不再集中在一個包中進行管理了:
-
react-router是router的核心部分代碼;
-
react-router-dom是用於瀏覽器的;
-
react-router-native是用於原生應用的;
目前我們使用最新的React Router版本是v5的版本:
-
實際上v4的版本和v5的版本差異並不大;
安裝react-router:
-
安裝react-router-dom會自動幫助我們安裝react-router的依賴;
yarn add react-router-dom
二. react-router基本使用
2.1. Router基本使用
react-router最主要的API是給我們提供的一些組件:
-
BrowserRouter或HashRouter
-
Router中包含了對路徑改變的監聽,並且會將相應的路徑傳遞給子組件;
-
BrowserRouter使用history模式;
-
HashRouter使用hash模式;
-
Link和NavLink:
-
通常路徑的跳轉是使用Link組件,最終會被渲染成a元素;
-
NavLink是在Link基礎之上增加了一些樣式屬性(後續學習);
-
to屬性:Link中最重要的屬性,用於設置跳轉到的路徑;
-
Route:
-
Route用於路徑的匹配;
-
path屬性:用於設置匹配到的路徑;
-
component屬性:設置匹配到路徑後,渲染的組件;
-
exact:精準匹配,只有精準匹配到完全一致的路徑,纔會渲染對應的組件;
在App中進行如下演練:
import React, { PureComponent } from 'react'; import { BrowserRouter, Route, Link } from 'react-router-dom'; import Home from './pages/home'; import About from './pages/about'; import Profile from './pages/profile'; export default class App extends PureComponent { render() { return ( <BrowserRouter> <Link to="/">首頁</Link> <Link to="/about">關於</Link> <Link to="/profile">我的</Link> <Route exact path="/" component={Home} /> <Route path="/about" component={About} /> <Route path="/profile" component={Profile} /> </BrowserRouter> ) } }
2.2. NavLink的使用
路徑選中時,對應的a元素變爲紅色
這個時候,我們要使用NavLink組件來替代Link組件:
-
activeStyle:活躍時(匹配時)的樣式;
-
activeClassName:活躍時添加的class;
-
exact:是否精準匹配;
先演示activeStyle:
<NavLink to="/" activeStyle={{color: "red"}}>首頁</NavLink> <NavLink to="/about" activeStyle={{color: "red"}}>關於</NavLink> <NavLink to="/profile" activeStyle={{color: "red"}}>我的</NavLink>
但是,我們會發現在選中about或profile時,第一個也會變成紅色:
-
原因是/路徑也匹配到了/about或/profile;
-
這個時候,我們可以在第一個NavLink中添加上exact屬性;
<NavLink exact to="/" activeStyle={{color: "red"}}>首頁</NavLink>
默認的activeClassName:
-
事實上在默認匹配成功時,NavLink就會添加上一個動態的active class;
-
所以我們也可以直接編寫樣式
a.active { color: red; }
當然,如果你擔心這個class在其他地方被使用了,出現樣式的層疊,也可以自定義class
<NavLink exact to="/" activeClassName="link-active">首頁</NavLink> <NavLink to="/about" activeClassName="link-active">關於</NavLink> <NavLink to="/profile" activeClassName="link-active">我的</NavLink>
2.3. Switch的作用
我們來看下面的路由規則:
-
當我們匹配到某一個路徑時,我們會發現有一些問題;
-
比如/about路徑匹配到的同時,
/:userid
也被匹配到了,並且最後的一個NoMatch組件總是被匹配到;
<Route exact path="/" component={Home} /> <Route path="/about" component={About} /> <Route path="/profile" component={Profile} /> <Route path="/:userid" component={User}/> <Route component={NoMatch}/>
原因是什麼呢?默認情況下,react-router中只要是路徑被匹配到的Route對應的組件都會被渲染;
但是實際開發中,我們往往希望有一種排他的思想:
-
只要匹配到了第一個,那麼後面的就不應該繼續匹配了;
-
這個時候我們可以使用Switch來將所有的Route進行包裹即可;
<Switch> <Route exact path="/" component={Home} /> <Route path="/about" component={About} /> <Route path="/profile" component={Profile} /> <Route path="/:userid" component={User} /> <Route component={NoMatch} /> </Switch>
2.3. Redirect的使用
Redirect用於路由的重定向,當這個組件出現時,就會執行跳轉到對應的to路徑中:
我們這裏使用這個的一個案例:
-
用戶跳轉到User界面;
-
但是在User界面有一個isLogin用於記錄用戶是否登錄:
-
true:那麼顯示用戶的名稱;
-
false:直接重定向到登錄界面;
App.js中提前定義好Login頁面對應的Route:
<Switch> ...其他Route <Route path="/login" component={Login} /> <Route component={NoMatch} /> </Switch>
在User.js中寫上對應的邏輯代碼:
import React, { PureComponent } from 'react' import { Redirect } from 'react-router-dom'; export default class User extends PureComponent { constructor(props) { super(props); this.state = { isLogin: false } } render() { return this.state.isLogin ? ( <div> <h2>User</h2> <h2>用戶名: coderwhy</h2> </div> ): <Redirect to="/login"/> } }
三. react-router高級使用
3.1. 路由嵌套
在開發中,路由之間是存在嵌套關係的。
這裏我們假設about頁面中有兩個頁面內容:
-
商品列表和消息列表;
-
點擊不同的鏈接可以跳轉到不同的地方,顯示不同的內容;
import React, { PureComponent } from 'react'; import { Route, Switch, Link } from 'react-router-dom'; function AboutProduct(props) { return ( <ul> <li>商品列表1</li> <li>商品列表2</li> <li>商品列表3</li> </ul> ) } function AboutMessage(props) { return ( <ul> <li>消息列表1</li> <li>消息列表2</li> <li>消息列表3</li> </ul> ) } export default class About extends PureComponent { render() { return ( <div> <Link to="/about">商品</Link> <Link to="/about/message">消息</Link> <Switch> <Route exact path="/about" component={AboutProduct} /> <Route path="/about/message" component={AboutMessage} /> </Switch> </div> ) } }
3.2. 手動跳轉
目前我們實現的跳轉主要是通過Link或者NavLink進行跳轉的,實際上我們也可以通過 JavaScript代碼
進行跳轉。
但是通過 JavaScript代碼
進行跳轉有一個前提:必須獲取到history對象。
如何可以獲取到history的對象呢?兩種方式
-
方式一:如果該組件是通過路由直接跳轉過來的,那麼可以直接獲取history、location、match對象;
-
方式二:如果該組件是一個普通渲染的組件,那麼不可以直接獲取history、location、match對象;
那麼如果普通的組件也希望獲取對應的對象屬性應該怎麼做呢?
-
前面我們學習過高階組件,可以在組件中添加想要的屬性;
-
react-router也是通過高階組件爲我們的組件添加相關的屬性的;
如果我們希望在App組件中獲取到history對象,必須滿足以下兩個條件:
-
App組件必須包裹在Router組件之內;
-
App組件使用withRouter高階組件包裹;
index.js代碼修改如下:
ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById('root') );
App.js代碼修改如下:
import { Route, Switch, NavLink, withRouter } from 'react-router-dom'; ...省略其他的導入代碼 class App extends PureComponent { render() { console.log(this.props.history); return ( <div> ...其他代碼 <button onClick={e => this.pushToProfile()}>我的</button> <Switch> <Route exact path="/" component={Home} /> <Route path="/about" component={About} /> <Route path="/profile" component={Profile} /> <Route path="/:userid" component={User} /> <Route component={NoMatch} /> </Switch> </div> ) } pushToProfile() { this.props.history.push("/profile"); } } export default withRouter(App);
源碼選讀:這裏的history來自哪裏呢?是否和之前使用的window.history一樣呢?
我們發現withRouter的高階函數來自react-router-dom:
-
實際上來自react-router的包;
withRouter函數:
history對象來自哪裏呢?
-
實際來自上面代碼的context;
這個context的值來自哪裏呢?
-
來自於context.Consumer的value中;
this.props.history來自哪裏呢?
-
來自BrowserRouter或者HashRouter在創建時,傳入的值;
-
又傳遞給了Router,Router的子組件可以通過該context獲取到這個值;
createBrowserHistory來自哪裏呢?
執行push操作的本質:
2.5. 傳遞參數
傳遞參數有三種方式:
-
動態路由的方式;
-
search傳遞參數;
-
to傳入對象;
動態路由的方式
動態路由的概念指的是路由中的路徑並不會固定:
-
比如
/detail
的path對應一個組件Detail; -
/detail/:id /detail/abc /detail/123
-
這個匹配規則,我們就稱之爲動態路由;
通常情況下,使用動態路由可以爲路由傳遞參數。
<div> ...其他Link <NavLink to="/detail/abc123">詳情</NavLink> <Switch> ... 其他Route <Route path="/detail/:id" component={Detail}/> <Route component={NoMatch} /> </Switch> </div>
detail.js的代碼如下:
-
我們可以直接通過match對象中獲取id;
-
這裏我們沒有使用withRouter,原因是因爲Detail本身就是通過路由進行的跳轉;
import React, { PureComponent } from 'react' export default class Detail extends PureComponent { render() { console.log(this.props.match.params.id); return ( <div> <h2>Detail: {this.props.match.params.id}</h2> </div> ) } }
search傳遞參數
NavLink寫法:
-
我們在跳轉的路徑中添加了一些query參數;
<NavLink to="/detail2?name=why&age=18">詳情2</NavLink> <Switch> <Route path="/detail2" component={Detail2}/> </Switch>
Detail2中如何獲取呢?
-
Detail2中是需要在location中獲取search的;
-
注意:這個search沒有被解析,需要我們自己來解析;
import React, { PureComponent } from 'react' export default class Detail2 extends PureComponent { render() { console.log(this.props.location.search); // ?name=why&age=18 return ( <div> <h2>Detail2:</h2> </div> ) } }
to傳入對象
to可以直接傳入一個對象
<NavLink to={{ pathname: "/detail2", query: {name: "kobe", age: 30}, state: {height: 1.98, address: "洛杉磯"}, search: "?apikey=123" }}> 詳情2 </NavLink>
獲取參數:
import React, { PureComponent } from 'react' export default class Detail2 extends PureComponent { render() { console.log(this.props.location); return ( <div> <h2>Detail2:</h2> </div> ) } }
四. react-router-config
目前我們所有的路由定義都是直接使用Route組件,並且添加屬性來完成的。
但是這樣的方式會讓路由變得非常混亂,我們希望將所有的路由配置放到一個地方進行集中管理:
-
這個時候可以使用react-router-config來完成;
安裝react-router-config:
yarn add react-router-config
常見router/index.js文件:
import Home from "../pages/home"; import About, { AboutMessage, AboutProduct } from "../pages/about"; import Profile from "../pages/profile"; import Login from "../pages/login"; import User from "../pages/user"; import Detail from "../pages/detail"; import Detail2 from "../pages/detail2"; import NoMatch from "../pages/nomatch"; const routes = [ { path: "/", exact: true, component: Home }, { path: "/about", component: About, routes: [ { path: "/about", exact: true, component: AboutProduct }, { path: "/about/message", component: AboutMessage }, ] }, { path: "/profile", component: Profile }, { path: "/login", component: Login }, { path: "/user", component: User }, { path: "/detail/:id", component: Detail }, { path: "/detail2", component: Detail2 }, { component: NoMatch } ]; export default routes;
將之前的Switch配置,換成react-router-config中提供的renderRoutes函數:
{renderRoutes(routes)} {/* <Switch> <Route exact path="/" component={Home} /> <Route path="/about" component={About} /> <Route path="/profile" component={Profile} /> <Route path="/user" component={User} /> <Route path="/login" component={Login} /> <Route path="/detail/:id" component={Detail}/> <Route path="/detail2" component={Detail2}/> <Route component={NoMatch} /> </Switch> */}
如果是子組件中,需要路由跳轉,那麼需要在子組件中使用renderRoutes函數:
-
在跳轉到的路由組件中會多一個
this.props.route
屬性; -
該
route
屬性代表當前跳轉到的路由對象,可以通過該屬性獲取到routes
;
export default class About extends PureComponent { render() { return ( <div> <Link to="/about">商品</Link> <Link to="/about/message">消息</Link> {renderRoutes(this.props.route.routes)} </div> ) } }
實際上react-router-config中還提供了一個 matchRoutes
輔助函數:
-
matchRoutes(routes, pathname)
傳入一個路由對象數組,獲取所有匹配的路徑;
const routes = matchRoutes(this.props.route.routes, "/about"); console.log(routes);
查看renderRoutes的源碼也是非常簡單的: