React页面路由 react-router-dom

1 背景

随着ajax的使用越来越广泛,前端的页面逻辑开始变得越来越复杂,特别是单页Web应用(Single Page Web Application,SPA)的兴起,前端路由系统随之开始流行。

所谓单页Web应用,就是只有一张Web页面的应用,是加载单个HTML页面并在用户与应用程序交互时动态更新该页面的Web应用程序。浏览器一开始会加载必须的HTMLCSSJavascript,所有的操作都在这张页面上完成,都由Javascript来控制。

  • 从用户的角度看,前端路由主要实现了两个功能(使用ajax更新页面状态的情况下):
    • 记录当前页面的状态(保存或分享当前页的url,再次打开该url时,网页还是保存(分享)时的状态);
    • 可以使用浏览器的前进后退功能(如点击后退按钮,可以使页面回到使用ajax更新页面之前的状态,url也回到之前的状态);
  • 作为开发者,要实现这两个功能,我们需要做到:
    • 改变url且不让浏览器向服务器发出请求;
    • 监测url的变化;
    • 截获url地址,并解析出需要的信息来匹配路由规则。

我们路由常用的hash模式和history模式实际上就是实现了上面的功能。

2 hash模式

hash模式是一种把前端路由的路径用井号#拼接在真实url后面的模式。当井号#后面的路径发生变化时,浏览器并不会重新发起请求,而是会触发onhashchange事件。

这里的hash就是指 url 尾巴后的#号以及后面的字符。这里的#css里的#是一个意思。hash也称作锚点,本身是用来做页面定位的,它可以使对应id的元素显示在可视区域内。

由于hash值变化不会导致浏览器向服务器发出请求,而且hash改变会触发hashchange事件,浏览器的进后退也能对其进行控制,所以人们在html5history出现前,基本都是使用hash来实现前端路由的。

缺点:传递参数的方式是在url后拼接,会有体积的限制

使用到的api:

window.location.hash = 'qq' // 设置 url 的 hash,会在当前url后加上 '#qq'
var hash = window.location.hash // '#qq'
window.addEventListener('hashchange', function () {
    // 监听hash变化,点击浏览器的前进后退会触发
})

3 history模式

history APIH5提供的新特性,允许开发者直接更改前端路由,即更新浏览器URL地址而不重新发起请求,它通过监听windowpopstate来实现的。

history路由特点:

  • 新的url可以是与当前url同源的任意url,也可以是与当前url一样的地址,但是这样会导致的一个问题是,会把重复的这一次操作记录到栈当中。
  • 通过history.state,添加任意类型的数据到记录中。
  • 可以额外设置title属性,以便后续使用。
  • 通过pushStatereplaceState来实现无刷新跳转的功能。
  • 可以传递复杂的参数
  • 可以监听浏览器的前进、后退事件(backforwardgo

对于history来说,确实解决了不少hash存在的问题,但是也带来了新的问题。具体如下:

  • 使用history模式时,在对当前的页面进行刷新时,此时浏览器会重新发起请求。如果nginx没有匹配得到当前的url,就会出现404的页面。
  • 而对于hash模式来说, 它虽然看着是改变了url,但不会被包括在http请求中。所以,它算是被用来指导浏览器的动作,并不影响服务器端。因此,改变hash并没有真正地改变url ,所以页面路径还是之前的路径,nginx也就不会拦截。
  • 因此,在使用history模式时,需要通过服务端来允许地址可访问,如果没有设置,就很容易导致出现404的局面。

history模式与hash模式的比较:
1、hash本来是拿来做页面定位的,如果拿来做路由的话,原来的锚点功能就不能用了。
2、hash的传参是基于url的,如果要传递复杂的数据,会有体积的限制,而history模式不仅可以在url里放参数,还可以将数据存放在一个特定的对象中。

相关API如下表:

API说明
window.history.pushState(state, title, url)pushState主要用于往历史记录堆栈顶部添加一条记录。
state:需要保存的数据,这个数据在触发popstate事件时作为参数传递过去
title:页面标题,基本没用,一般传null,当前所有浏览器都会忽略此参数
url:设定新的历史记录的url,缺少时表示为当前页地址。新的url与当前urlorigin必须是一样的,否则会抛出错误。url可以是绝对路径,也可以是相对路径。
window.history.replaceState(state, title, url)更改当前的历史记录,与pushState参数相同
window.history.state用于存储以上方法的data数据,不同的浏览器读写权限不一样
window.onpopstate监听浏览器前进后退事件,pushStatereplaceState方法不会触发
window.history.back()后退
window.history.forward()前进
window.history.go(1)前进一步,-2为后退两步
window.history.length查看当前历史堆栈中页面的数量

示例:当前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/

3 React路由使用:react-router-dom

3.1 安装

npm install react-router-dom
yarn add react-router-dom 

以上方式默认安装最新版本,react-router-dom的两个版本,5和6的使用方式不同,根据需要安装。

3.2 HashRouter和BrowserRouter:路由的容器

HashRouterBrowserRouter决定了路由模式分别是hash模式和history模式,并且这两个组件是路由的容器,必须放在最外层。

1、hash模式

ReactDOM.render(
  <HashRouter>
      <Route path="/" component={Home}/>
  </HashRouter>
)

2、history模式

ReactDOM.render(
  <BrowserRouter>
      <Route path="/" component={Home}/>
  </BrowserRouter>
)

3.3 Route:路由与组件之间的映射

Route实现了路径和显示组件之间的映射。

V5写法:

<Route path="/路径" component={组件} render={返回dom} location="route对象" exact="匹配规则"/>

它的参数说明如下:

参数说明
path跳转的路径
component对应路径显示的组件
render可以自己写render函数返回具体的dom,而不需要去设置component
location传递route对象,和当前的route对象对比,如果匹配则跳转
exact匹配规则,默认值为false,true的时候则精确匹配。

V6写法:

<Route path="/路径" element={组件} render={返回dom} location="route对象"/>

示例:

<Routes>
  <Route path={"/"} element={<home/>} />
  <Route path={"/about"} element={<About/>} />
  <Route path={"/topics"} element={<Topics/>} />
</Routes>

注意:V6中不需要exact属性,它默认就是匹配完整路径。

3.4 Router:管理路由的状态

Router组件是底层路由,可以管理路由的状态,可以代替HashRouterBrowserRouter

下面与<BrowserRouter></BrowserRouter>作用相同:

<Router history={history}>
  // ...
</Router>

下面与<HashRouter></HashRouter>作用相同:

<Router history={createHashHistory()}>
  // ...
</Router>

3.5 Link和NavLink:跳转链接

LinkNavLink组件类似于<a>标签.

1、Link组件的api
(1)to:目标页面的路径,两种写法,表示跳转到哪个路由

// 字符串写法
<Link to="/a" />

// 对象写法
<Link to={{
    pathname: '/courses',
    search: '?sort=name',
    hash: '#the-hash',
    state: {fromDashboard: true}
}}/>

(2)replace:就是将push改成replace
(3)innerRef:访问Link标签的dom

2、NavLink组件的api属性:包含了Link组件的所有api,在Link组件的基础上进行了扩展
(1)Link的所有api
(2)activeClassName:路由激活的时候设置的类名
(3)activeStyle:路由激活设置的样式
(4)exact:参考Route,符合这个条件才会激活active
(5)strict:参考Route,符合这个条件才会激活active
(6)isActive:接收一个回调函数,active状态变化的时候回触发,返回false则中断跳转
示例代码:

const oddEvent = (match, location) => {
  if (!match) {
      return false
  }
  console.log(match.id)
  return true
}
<NavLink isActive={oddEvent} to="/a/123">组件一</NavLink>

(7)location:接收一个location对象,当url满足这个对象的条件才会跳转

<NavLink to="/a/123" location={{ key:"mb5wu3", pathname:"/a/123" }}/>

3.6 Redirect与Navigate:页面重定向

Redirect组件是V5中的页面重定向,属性和Link相同,用法如下:

// 基本的重定向
<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>

注:
①页面重定向:客户端向服务器端发送了两次请求
②请求转发:客户端向服务器发送了一次请求

Navigate是V6中的页面重定向,使用方式如下:

<Route path="/" element={<Navigate to="/home" />}></Route>

3.7 Switch和Routes:路由切换

Switch组件是V5中进行路由切换的方式,类似Tab标签。Switch内部只能包含RouteRedirectRouter,示例代码如下:

<Switch>
  <Route exact path="/" component={Home} />
  <Route path="/about" component={About} />
  <Route path="/:user" component={User} />
  <Route component={NoMatch} />
</Switch>

Routes组件是V6中进行路由切换的方式,示例代码如下:

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
</Routes>

3.8 withRouter:包装器

withRouter组件:包装器,将普通的组件包装成路由组件。包装后普通组件就可以访问路由信息(如:historylocationmatch

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);

3.9 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>
}

3.10 history对象

在每个路由组件中我们可以使用this.props.history获取到history对象,也可以使用withRouter包裹组件获取,在history中封装了pushreplacego等方法,具体内容如下:

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;
}

3.11 404视图:请求资源不存在

404视图:404错误表示客户端请求的资源不存在。在react中请求的路径不存在(404),路由采用Switch组件进行切换

<Switch>
    <Route exact={true} path={"/"} component={Home}/>
    <Route path={"/about"} component={About}/>
    <Route path={"/topics"} component={Topics}/>
    <Route component={View404}/>
</Switch>

4 react-router-dom实现路由案例

4.1 【案例1】实现简单的路由跳转

4.1.1 效果展示

启动程序,默认是Home界面,这时的地址是:localhost:3000
在这里插入图片描述
点击About界面,跳转到About界面,这时地址是:localhost:3000/about
在这里插入图片描述
点击Topics界面,跳转到Topics界面,这时地址是:localhost:3000/topics
在这里插入图片描述
点击话题1,页面跳转,这时地址是:localhost:3000/topics/话题1
在这里插入图片描述
点击话题2,地址是:localhost:3000/topics/话题2
在这里插入图片描述
点击话题3,地址是:localhost:3000/topics/话题3
在这里插入图片描述

4.1.2 实现步骤

1、用WebStorm创建一个React项目react-demo,首先安装react-router-dom模块,这里安装5.2.0版本(最新版本中的Route组件用法与本版本不同,具体在上面已经说明):

npm install react-router-dom@5.2.0

2、在src文件夹下新建文件components,用来编写不同的组件。在components文件夹下新建文件home.js,编写主页文件:

import React from "react";

class Home extends React.Component {
    render() {
        return (
            <div>
                <h2>Home页面</h2>
            </div>
        );
    }
}

export default Home;

3、在components文件夹下新建about.js文件,代码如下:

import React from "react";

class About extends React.Component {
    render() {
        return (
            <div>
                <h2>About页面</h2>
            </div>
        )
    }
}

export default About;

4、在components文件夹下新建topic.js文件,具体代码如下:

import React from "react";

class Topic extends React.Component {
    render() {
        return (
            <div>
                <h2>
                    {/*使用大括号表示这里的参数不止一条,需要从别的地方获取*/}
                    {/*this.props.match包含了url的信息,其中params包含了我们给定的参数*/}
                    {this.props.match.params.topicId}
                </h2>
            </div>
        )
    }
}

export default Topic;

5、在components文件夹下新建文件topics.js,代码如下:

import React from "react";
import {Link, Route} from "react-router-dom";
import Topic from "./topic";

class Topics extends React.Component {
    render() {
        return (
            <div>
                <h2>今日话题</h2>
                <ul>
                    <li>
                        {/*Link表示要跳转到哪个页面,但并没有实现跳转,this.props.match.url获取到了当前页面的url*/}
                        <Link to={`${this.props.match.url}/话题1`}>
                            话题1:今天吃了什么
                        </Link>
                    </li>
                    <li>
                        <Link to={`${this.props.match.url}/话题2`}>
                            话题2:今天做核酸了吗
                        </Link>
                    </li>
                    <li>
                        <Link to={`${this.props.match.url}/话题3`}>
                            话题3:今天天气怎么样
                        </Link>
                    </li>
                </ul>
                {/*真正实现页面功能的是Route组件,exact表示默认显示当前信息*/}
                {/*这里写render函数表示在页面加载出来之后首先显示该信息*/}
                <Route exact path={this.props.match.url} render={() => (<h3>请选择今日话题</h3>)}/>
                {/*当点击上面的话题时,跳转到topic界面,:topicId表示请求参数*/}
                <Route path={`${this.props.match.url}/:topicId`} component={Topic}/>
            </div>
        );
    }
}

export default Topics;

6、最后一步:修改App.js部分代码,将组件显示在网页上,App.js代码如下:

import {BrowserRouter, Link, Route} from "react-router-dom";
import Home from "./components/home";
import About from "./components/about";
import Topics from "./components/topics";

function App() {
    return (
        <BrowserRouter>
            {/*编写Home、About、Topics页面的跳转*/}
            <div>
                <ul>
                    <li>
                        <Link to={"/"}>Home界面</Link>
                    </li>
                    <li>
                        <Link to={"/about"}>About界面</Link>
                    </li>
                    <li>
                        <Link to={"/topics"}>Topics界面</Link>
                    </li>
                </ul>
                {/*使用Route实现真正的页面跳转*/}
                {/*当前默认主页面是Home界面*/}
                <Route exact path={"/"} component={Home}/>
                <Route path={"/about"} component={About}/>
                <Route path={"/topics"} component={Topics}/>
            </div>
        </BrowserRouter>
    )
}

export default App;

4.2 【案例2】实现计数器

4.2.1 效果展示

首页,localhost:3000:
在这里插入图片描述
新闻,localhost:3000/news:
在这里插入图片描述
点击新闻2,localhost:3000/news/1002:
在这里插入图片描述
课程,localhost:3000/course:
在这里插入图片描述
点击大数据,localhost:3000/big-data
在这里插入图片描述
加入我们,该页面没有编写,因此只能看到地址的改变,localhost:3000/joinUs:
在这里插入图片描述

4.2.2 实现步骤

1、用WebStorm创建一个React项目demo,首先安装react-router-dom模块,这里安装5.2.0版本(最新版本中的Route组件用法与本版本不同,具体在上面已经说明):

npm install react-router-dom@5.2.0

2、在src文件夹下新建文件夹components和css,components用来存放编写的组件,css用来存放样式,在components文件夹下新建文件Headers.js,编写网页的头部,代码如下:

import React, {Component} from "react";
import {NavLink} from "react-router-dom";
import "../css/header.css";

class Headers extends Component {
    render() {
        <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 Headers;

在这里插入图片描述


3、在css文件夹下新建header.css文件,编写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;
}

4、在src文件夹下新建文件夹pages,主要存放页面组件,在该文件夹下新建Home.js文件,用来显示主页。主页是一个计数器,代码如下:

import React, {Component} from "react";
import Headers from "../components/Headers";
import "../css/home.css";

class Home extends Component {
    constructor(props) {
        super(props);
        // 设置状态机
        this.state = {
            count: 0
        }
    }

    // 编写增加函数
    add = () => {
        // preState表示当前状态state
        this.setState((preState) => {
            return {
                count: preState.count + 1
            }
        })
    }
    // 编写减函数
    sub = () => {
        this.setState((preState) => {
            return {
                count: preState.count - 1
            }
        })
    }

    // 异步函数
    async asyncAdd() {
        // 表示1秒后再加1
        await setTimeout(() => {
            this.setState((preState) => {
                return {
                    count: preState.count + 1
                }
            })
        }, 1000);
    }

    render() {
        return (
            <div className={"home"}>
                <Headers/>
                <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;

在这里插入图片描述


5、在css文件夹下新建文件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.3rem 1rem;
    border-radius: 1em;
    margin: 1em;
}

6、在pages文件夹下新建文件NewDetails.js,编写新闻详情页,代码如下:

import React, {Component} from "react";
import Headers from "../components/Headers";

class NewDetails extends Component {
    constructor(props) {
        super(props);
        // props.location.state存放了新闻的具体的数据id、content以及title
        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>
                    <Headers/>
                    <h1>{title}</h1>
                    <p>{content}</p>
                </div>
            )
        }
    }
}

export default NewDetails;

在这里插入图片描述


7、在pages文件夹下新建News.js,编写新闻主页,代码如下:

import React, {Component} from "react";
import {Route, NavLink} from "react-router-dom";
import Headers from "../components/Headers";
import NewDetails from "./NewDetails";

const data = [ // 定义新闻的内容,在实际开发中这些数据来源于后台
    {
        id: 1001,
        title: "新闻1",
        content: "北京"
    }, {
        id: 1002,
        title: "新闻2",
        content: "上海"
    }
]

class NewsPage extends Component {
    render() {
        return (
            <div>
                <Headers/>
                <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>
        )
    }
}

// Route组件实现真正的跳转
const News = ({match}) => {
    return (
        <div>
            <Route exact path={match.path} render={(props) => <NewsPage {...props}/>}/>
            <Route path={`${match.path}/:id`} component={NewDetails}/>
        </div>
    )
}

export default News;

在这里插入图片描述


8、在pages文件夹下新建文件Course.js,代码如下:

import React, {Component} from "react";
import Headers from "../components/Headers";
import {NavLink} from "react-router-dom";

class Course extends Component {
    render() {
        let {match} = this.props; // this.props.match存放了path和url
        return (
            <div>
                <Headers/>
                {/*浏览器的路径改变了,但是没有发生页面跳转*/}
                <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;

在这里插入图片描述


9、最后一步,在App.js文件中修改部分代码,具体代码如下:

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;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>