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可以是绝对路径,也可以是相对路径。

例如: 

当前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

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内部只能包含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事件

  示例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为跨域网址,那么会报错.这样设计的目的是防止恶意代码让用户以为他们是在另一个网站上

 

上一篇:HTML5学习笔记简明版(5):input的type超级类型


下一篇:HTML5学习笔记简明版 目录索引