React頁面路由


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可以是絕對路徑,也可以是相對路徑。

例如: 

當前urlhttps://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

to 有兩種寫法,表示跳轉到哪個路由

  • 字符串寫法

      <Link to="/a"   />

  • 對象寫法

    <Link to={{

  pathname: '/courses',

  search: '?sort=name',

  hash: '#the-hash',

  state: { fromDashboard: true }

}}/>

replace:就是將push改成replace

innerRef:訪問Link標簽的dom

        

(2)NavLink的api

Link的所有api

activeClassName 路由激活的時候設置的類名

activeStyle 路由激活設置的樣式

exact  參考Route,符合這個條件才會激活active類

strict  參考Route,符合這個條件才會激活active類

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>

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內部只能包含RouteRedirectRouter

  <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事件

  示例1location.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為跨域網址,那么會報錯.這樣設計的目的是防止惡意代碼讓用戶以為他們是在另一個網站上

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM