React頁面路由
前言:
隨着 ajax 的使用越來越廣泛,前端的頁面邏輯開始變得越來越復雜,特別是單頁Web應用(Single Page Web Application,SPA))的興起,前端路由系統隨之開始流行。
1、從用戶的角度看,前端路由主要實現了兩個功能(使用ajax更新頁面狀態的情況下):
記錄當前頁面的狀態(保存或分享當前頁的url,再次打開該url時,網頁還是保存(分享)時的狀態);
可以使用瀏覽器的前進后退功能(如點擊后退按鈕,可以使頁面回到使用ajax更新頁面之前的狀態,url也回到之前的狀態);
2、作為開發者,要實現這兩個功能,我們需要做到:
改變url且不讓瀏覽器向服務器發出請求;
監測 url 的變化;
截獲 url 地址,並解析出需要的信息來匹配路由規則。
我們路由常用的hash模式和history模式實際上就是實現了上面的功能。
一、hash模式
這里的 hash 就是指 url 尾巴后的 # 號以及后面的字符。這里的 # 和 css 里的 # 是一個意思。hash也稱作錨點,本身是用來做頁面定位的,她可以使對應 id 的元素顯示在可視區域內。
由於 hash 值變化不會導致瀏覽器向服務器發出請求,而且 hash 改變會觸發 hashchange 事件,瀏覽器的進后退也能對其進行控制,所以人們在 html5 的 history 出現前,基本都是使用 hash 來實現前端路由的。
使用到的api:
window.location.hash = 'qq' // 設置 url 的 hash,會在當前url后加上 '#qq'
var hash = window.location.hash // '#qq'
window.addEventListener('hashchange', function(){
// 監聽hash變化,點擊瀏覽器的前進后退會觸發
})
二、history模式
已經有 hash 模式了,而且 hash 能兼容到IE8, history 只能兼容到 IE10,為什么還要搞個 history 呢?
首先,hash 本來是拿來做頁面定位的,如果拿來做路由的話,原來的錨點功能就不能用了。
其次,hash 的傳參是基於 url的,如果要傳遞復雜的數據,會有體積的限制,而history 模式不僅可以在url里放參數,還可以將數據存放在一個特定的對象中。
相關API:
window.history.pushState(state, title, url)
// state:需要保存的數據,這個數據在觸發popstate事件時,可以在event.state里獲取
// title:標題,基本沒用,一般傳 null
// url:設定新的歷史記錄的 url。新的 url 與當前 url 的 origin 必須是一樣的,否則會拋出錯誤。url可以是絕對路徑,也可以是相對路徑。
例如:
當前url是 https://www.baidu.com/a/,執行history.pushState(null, null, './qq/'),則變成 https://www.baidu.com/a/qq/,
執行history.pushState(null, null, '/qq/'),則變成 https://www.baidu.com/qq/
window.history.replaceState(state, title, url)
// 與 pushState 基本相同,但她是修改當前歷史記錄,而 pushState 是創建新的歷史記錄
window.addEventListener("popstate", function() {
// 監聽瀏覽器前進后退事件,pushState 與 replaceState 方法不會觸發
});
window.history.back() // 后退
window.history.forward() // 前進
window.history.go(1) // 前進一步,-2為后退兩步,window.history.length可以查看當前歷史堆棧中頁面的數量
三、react-router-dom API
React實現頁面路由的模塊:react-router-dom
1、HashRouter和BrowserRouter
其實就是路由的hash和history兩種模式,並且這兩個組件是路由的容器,必須在最外層
// hash模式
ReactDOM.render(
<HashRouter>
<Route path="/" component={Home}/>
</HashRouter>
)
// history模式
ReactDOM.render(
<BrowserRouter>
<Route path="/" component={Home} />
</BrowserRouter>
)
2、Route
路由的一個原材料,它是控制路徑對應顯示的組件
Route的參數
path:跳轉的路徑
component: 對應路徑顯示的組件
render:可以自己寫render函數返回具體的dom,而不需要去設置component
location: 傳遞route對象,和當前的route對象對比,如果匹配則跳轉
exact: 匹配規則,true的時候則精確匹配。
3、Router
低級路由,適用於任何路由組件,主要和redux深度集成,使用必須配合history對象,使用Router路由的目的是和狀態管理庫如redux中的history同步對接
<Router history={history}>
...
</Router>
4、Link和NavLink
兩者都是跳轉路由,NavLink的參數更多些
(1)Link的api
l to: 有兩種寫法,表示跳轉到哪個路由
- 字符串寫法
<Link to="/a" />
- 對象寫法
<Link to={{
pathname: '/courses',
search: '?sort=name',
hash: '#the-hash',
state: { fromDashboard: true }
}}/>
l replace:就是將push改成replace
l innerRef:訪問Link標簽的dom
(2)NavLink的api
l Link的所有api
l activeClassName 路由激活的時候設置的類名
l activeStyle 路由激活設置的樣式
l exact 參考Route,符合這個條件才會激活active類
l strict 參考Route,符合這個條件才會激活active類
l isActive 接收一個回調函數,active狀態變化的時候回觸發,返回false則中斷跳轉
const oddEvent = (match, location) => {
console.log(match,location)
if (!match) {
return false
}
console.log(match.id)
return true
}
<NavLink isActive={oddEvent} to="/a/123">組件一</NavLink>
l location 接收一個location對象,當url滿足這個對象的條件才會跳轉
<NavLink to="/a/123" location={{ key:"mb5wu3", pathname:"/a/123" }}/>
5、Redirect:頁面重定向
// 基本的重定向
<Redirect to="/somewhere/else" />
// 對象形式
<Redirect
to={{
pathname: "/login",
search: "?utm=your+face",
state: { referrer: currentLocation }
}}
/>
// 采用push生成新的記錄
<Redirect push to="/somewhere/else" />
// 配合Switch組件使用,form表示重定向之前的路徑,如果匹配則重定向,不匹配則不重定向
<Switch>
<Redirect from='/old-path' to='/new-path'/>
<Route path='/new-path' component={Place}/>
</Switch>
6、Switch
路由切換,只會匹配第一個路由,可以想象成tab欄
Switch內部只能包含Route、Redirect、Router
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/:user" component={User}/>
<Route component={NoMatch}/>
</Switch>
7、withRouter
當一個非路由組件也想訪問到當前路由的match,location,history對象,那么withRouter將是一個非常好的選擇,可以理解為將一個組件包裹成路由組件
import { withRouter } from 'react-router-dom'
const MyComponent = (props) => {
const { match, location, history } = this.props
return (
<div>{props.location.pathname}</div>
)
}
const FirstTest = withRouter(MyComponent);
8、Router Hooks
在Router5.x中新增加了Router Hooks用於在函數組件中獲取路由信息。使用規則和React的其他Hooks一致。
(1)useHistory:返回history對象
(2)useLocation:返回location對象
(3)useRouteMatch:返回match對象
(4)useParams:返回match對象中的params,也就是path傳遞的參數
import React from ‘react’;
import { useHistory } from ‘react-router-dom’;
function backBtn(props) {
let history = useHistory;
return <button onClick={ ()=> {
history.goBack();
}>返回上一頁</button>
}
9、history對象
在每個路由組件中我們可以使用this.props.history獲取到history對象,也可以使用withRouter包裹組件獲取,
在history中封裝了push,replace,go等方法,具體內容如下:
History {
length: number;
action: Action;
location: Location;
push(path: Path, state?: LocationState): void; // 調用push前進到一個地址,可以接受一個state對象,就是自定義的路由數據
push(location: LocationDescriptorObject): void; // 接受一個location的描述對象
replace(path: Path, state?: LocationState): void; // 用頁面替換當前的路徑,不可再goBack
replace(location: LocationDescriptorObject): void; // 同上
go(n: number): void; // 往前走多少也頁面
goBack(): void; // 返回一個頁面
goForward(): void; // 前進一個頁面
block(prompt?: boolean | string | TransitionPromptHook): UnregisterCallback;
listen(listener: LocationListener): UnregisterCallback;
createHref(location: LocationDescriptorObject): Href;
}
這樣我們想使用api來操作前進后退就可以調用history中的方法
10、404視圖
既然路由就需要考慮一個問題---404視圖。當用戶訪問一些不存在的URL時就該返回404視圖了,但不存在的地址該如何匹配呢?----使用Switch
Switch組件的作用類似於JS中的switch語句,當一項匹配成功之后,就不再匹配后續內容。這樣的話就可以把要匹配的內容寫在Switch組件中,最后一項寫404視圖,當其他都匹配不成功時就是404視圖。例如:
<Switch>
<Route exact={true} path={"/"} component={Home}/>
<Route path={"/about"} component={About}/>
<Route path={"/topics"} component={Topics}/>
<Route component={View404}/>
</Switch>
四、react-router-dom實現路由
1、基本路由示例:
(1)Home.js
import React, { Component } from "react";
class Home extends Component {
render() {
return (
<div>
<h2>Home頁面</h2>
</div>
)
}
}
export default Home;
(2)About.js
import React, { Component } from "react";
class About extends Component {
render() {
return (
<div>
<h2>About頁面</h2>
</div>
)
}
}
export default About;
(3)Topic.js
import React,{ Component } from "react";
class Topic extends Component {
render() {
console.log(this.props);
return (
<div>
<h2>{ this.props.match.params.topicId}</h2>
</div>
)
}
}
export default Topic;
(4)Topics.js
import React,{ Component } from "react";
import {
Route,
Link
} from "react-router-dom";
import Topic from "./Topic";
class Topics extends Component{
render() {
console.log(this)
return (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${ this.props.match.url }/rendering`}>
Rendering with React
</Link>
</li>
<li>
<Link to={`${this.props.match.url}/components`}>
Components
</Link>
</li>
<li>
<Link to={`${this.props.match.url}/props-v-state`}>
Props v. State
</Link>
</li>
</ul>
<Route path={`${this.props.match.url}/:topicId`} component={Topic}/>
<Route exact path={this.props.match.url} render={()=> (
<h3> Please select a topic.</h3>
)}/>
</div>
)
}
}
export default Topics;
(5)App.js
import logo from './logo.svg';
import './App.css';
import {
BrowserRouter as Router,
Route,
Link
} from "react-router-dom";
import Topics from "./components/Topics";
import Home from "./components/Home";
import About from "./components/About";
function App() {
return (
<Router>
<div>
<ul>
<li>
<Link to={"/"}>Home</Link>
</li>
<li>
<Link to={"/about"}>About</Link>
</li>
<li>
<Link to={"/topics"}>Topics</Link>
</li>
</ul>
<hr/>
<Route exact={true} path={"/"} component={Home}/>
<Route path={"/about"} component={About}/>
<Route path={"/topics"} component={Topics}/>
</div>
</Router>
)
}
export default App;
2、嵌套路由示例
(1)運行結果
(2)目錄結構
(3)Header.js
import React,{ Component } from "react";
import { NavLink } from "react-router-dom";
import '../css/header.css';
class Header extends Component {
render() {
return (
<header>
<nav>
<ul>
<li>
<NavLink exact to={"/"}>首頁</NavLink>
</li>
<li>
<NavLink to={"/news"}>新聞</NavLink>
</li>
<li>
<NavLink to={"/course"}>課程</NavLink>
</li>
<li>
<NavLink to={"/joinUs"}>加入我們</NavLink>
</li>
</ul>
</nav>
</header>
)
}
}
export default Header;
(4)header.css
body{
font-size: 16px;
margin: 0;
padding: 0;
}
ul{
text-align: right;
background-color: #eee;
margin: 0;
}
ul li{
display: inline-block;
list-style: none;
text-align: center;
border-left: 1px solid #ccc;
}
a {
text-decoration: none;
color: #666;
font-size: 1.5rem;
padding: 0.8em 2em;
display: block;
}
a:hover{
color: #000;
}
a.active {
background-color: #666;
color: #fff;
}
(5)Home.js
import React,{ Component } from "react";
import Header from "../components/Header";
import '../css/home.css';
import logo from '../images/react.png';
class Home extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
add = ()=> {
this.setState((preState)=>{
return{
count: preState.count+1
}
})
}
sub = ()=> {
this.setState((preState)=> {
return{
count: preState.count-1
}
})
}
async asyncAdd() {
await setTimeout(()=> {
this.setState((preState)=> {
return{
count: preState.count+1
}
})
},1000);
}
render() {
return(
<div className={"home"}>
<Header/>
<div>
<img className="logo" src={logo} alt={"logo"}/>
</div>
<h1>Count的值:{ this.state.count }</h1>
<div className="flexContainer">
<button onClick={ ()=> this.asyncAdd() }>等待1s再執行count+1</button>
<button onClick={ this.add }>count+1</button>
<button onClick={ ()=> this.sub() }>count-1</button>
</div>
</div>
)
}
}
export default Home;
(6)home.css
@keyframes rotate {
0% {
transform: rotate(0deg); left: 0px;
}
100% {
transform: rotate(360deg); left: 0px;
}
}
.home{
text-align: center;
}
.logo{
animation: rotate 10s linear 0s infinite;
}
button{
background: #237889;
font-size: calc(1.5*1rem);
color: #fff;
padding: 0.3em 1em;
border-radius: 1em;
margin: 1em;
}
(7)NewDetail.js
import React,{ Component } from "react";
import Header from "../components/Header";
class NewDetail extends Component{
constructor(props) {
super(props);
console.log(props)
this.data = props.location.state? props.location.state.data:null;
}
render() {
if (this.data !=null){
let title = this.data.title;
let content = this.data.content;
return (
<div>
<Header />
<h1>{ title }</h1>
<p> { content }</p>
</div>
)
}
}
}
export default NewDetail;
(8)News.js
import React,{ Component } from "react";
import {
Route,
NavLink
} from "react-router-dom";
import Header from "../components/Header";
import NewDetail from "./NewDetail";
const data = [
{
id: 1001,
title: '西安新增6例新冠病例',
content: '上海-西安-張掖-額濟納-西安'
},
{
id: 1002,
title: '寒潮來襲,你...凍成狗了嗎?',
content: '被子是我的親人,我不想離開它'
}
]
class NewsPage extends Component{
render() {
return(
<div>
<Header />
<h1>請選擇一條新聞:</h1>
{
data.map((item)=>{
return(
<div key={item.id}>
<NavLink to={{
pathname: `${this.props.match.url}/${item.id}`,
state: { data: item }
}}>
{ item.title}
</NavLink>
</div>
)
})
}
</div>
)
}
}
const News = ({ match })=> {
return(
<div>
<Route exact path={match.path} render={(props)=><NewsPage {...props}/>}/>
<Route path={`${match.path}/:id`} component={NewDetail} />
</div>
)
}
export default News;
(9)Course.js
import React,{ Component } from "react";
import Header from "../components/Header";
import { NavLink } from "react-router-dom";
class Course extends Component{
render() {
let { match } = this.props;
console.log(this.props)
return(
<div>
<Header />
<p>
<NavLink to={`${match.url}/front-end`}>前端技術</NavLink>
</p>
<p>
<NavLink to={`${match.url}/big-data`}>大數據</NavLink>
</p>
<p>
<NavLink to={`${match.url}/algorithm`}>算法</NavLink>
</p>
</div>
)
}
}
export default Course;
(10)App.js
import './App.css';
import {
BrowserRouter as Router,
Route,
Switch,
} from "react-router-dom";
import Home from "./pages/Home";
import Course from "./pages/Course";
import News from './pages/News';
function App() {
return (
<Router>
<Switch>
<Route exact path={"/"} component={ Home }/>
<Route path={"/course"} component={Course}/>
<Route path={"/news"} component={News}/>
</Switch>
</Router>
);
}
export default App;
五、關於路由的面試題:
1、實現前端路由的兩種方式及其差異?
前端路由的本質是監聽url變化,然后匹配路由規則,無需刷新就可以顯示相應的頁面,目前單頁面路由主要有兩種方式
(1) hash 模式
(2) history 模式
2、hash模式實現路由
為什么要使用hash模式:頁面使用Ajax發送異步請求可以實現無縫刷新,但這種方式也存在使得瀏覽器的url不發生任何變化的時候就完成了請求,使得用戶體驗不佳,也導致了用戶下次使用相同的url訪問上次的頁面時內容無法重新呈現的問題。hash模式是解決這個問題的途徑之一。
主要通過location.hash設置hash Url,也就是url的符號#后面的值。當哈希值發生變化時,不會向服務器請求發送數據,可以通過hashchange事件來監聽hash的變化,實現相應的功能
(1) ocation.hash 設置/獲取hash
(2) hashchange事件監聽url變化,解析url實現頁面路由跳轉
hash模式需要注意的幾個點
- 散列值不會隨請求發送到服務器
- 散列值會反映在瀏覽器url上
- 只修改瀏覽器的哈希部分,按下回車,瀏覽器不會發送任何請求給服務器,只會觸發hashchange事件並且修改location.hash的值
- html a標簽,設置id錨點,點擊觸發時,瀏覽器會自動設置location.hash值,同時觸發hashchange事件,url上也會反映這一變化
- hash模式下,手動刷新頁面不會向瀏覽器發送請求,不會觸發hashchange事件,但是會觸發load事件
示例1:location.hash觸發hashchange事件
效果:
示例2:錨點跳轉設置hash觸發hashchange事件
關於錨點跳轉:
- a標簽可以跳轉到指定了name或者id的a標簽
- a標簽可以跳轉到指定了id的非a標簽,非a標簽如果沒有指定id則不可以被跳轉
3、history模式實現路由
主要通過history.pushState/replceState向當前歷史記錄中插入狀態對象state,在瀏覽器前進、回退、跳轉等動作發生時觸發popState事件,此時可以通過解析popState事件回調函數的event參數中的state對象,或者解析當前頁面url來實現路由。
建議解析url方式實現路由。如果沒有在頁面首次加載的時候設置pushState/replaceState,那么首頁一般是沒有state對象的,在執行瀏覽器動作時,如果回退到首頁,那么當前history對象的state屬性不存在,導致解析state報錯
示例:
兩種路由方式的差異以及需要注意的點
(1)方式的異同
- 頁面手動刷新,hash模式不會向服務器發送請求,history模式會
- hash模式下url中的哈希值不會發送到服務器,history模式url全部會發送至服務器
- 設置location.hash和pushState都不會導致瀏覽器刷新
- 設置location.hash的時候會觸發hashchange事件和popstate事件
- 僅當pushState函數設置url參數的值為hash格式時,瀏覽器動作發生會觸發hashchange事件,盡管location.hash值為空
- a標簽錨點跳轉可以設置hash,觸發hashchange事件
(2)注意的問題
如果pushState的url為跨域網址,那么會報錯.這樣設計的目的是防止惡意代碼讓用戶以為他們是在另一個網站上