[React 基础系列] React Router 的基本应用

Posted GoldenaArcher

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[React 基础系列] React Router 的基本应用相关的知识,希望对你有一定的参考价值。

本章讲述 React Router 的基础用法,主要讲解应用,不讲更更基层的东西,同时,这篇也是近期 React 基础系列的最后一篇。

其实 React Router 不是官方提供的库,属于生态圈补充的,但是没有路由基本上就无法完成 SPA(Single Page Application,单页应用),所以就将 React Router 的内容提到这里来学习了。

主要内容由 路由功能的实现页面的跳转 两个部分组成。

案例地址在这里:react router 应用案例

React 基础系列:

React Router Dom

前端有三大框架:Angular,React 和 Vue,其中 Angular 和 Vue 是真正的框架(framework),而 React 对自己的定义其实是库(library),因此,React 的原生生态支持是三者中最差的一个:它没有自己的内置路由功能。

对比 Angular——一个完整的 MVC 结构的框架,以及 Vue——一个完整的 MVVM 框架,都有着自己的路由功能,React 的路由功能是通过社区支持去实现的:react-router-dom。

之所以下载 react-router-dom 而非直接下载 react-router 的原因很简单:前者包含了后者,是基于 react router 开发的对 DOM 的操作;而后者并不提供对于 DOM 的直接操作。

React Router Dom 的安装也很简单,在 CRA 项目创建完之后,直接使用 npm 安装即可:

C:\\starter-kit>npm install --save react-router-dom

传统意义上的路由是将 url 路径与 html 页面进行绑定,通过 URL 去访问 HTML 页面和其相关联的资源。但是 React 是 SPA,也就意味着它只有一个 index.html 作为入口,其他的页面完成都是通过对组件的渲染而实现的。这就代表着对于 react-router-dom 这个插件来说,它需要将 URL 与组件进行绑定,从而实现通过不同的 URL 访问不同的页面这样一个效果。

注*:再重申一遍,react-router-dom 是基于 react-router 实现的,对 DOM 进行操作的库。目前 react router 最新的文档应该是这两个个:web 以及 nativenative 指的就是 react native。

页面分析

注*:这里会使用 BrowserRouter 去进行功能的视线,但是在将页面部署到 Git Page 上时,遇到过使用 BrowserRouter 却白屏的情况,在 写好了功能/项目不知道怎么展示?手把手带你白嫖 Git Pages 部署自己的项目去惊艳面试官 中有通过使用 HashRouter 代替 BrowserRouter 去解决这个问题,不过最后还是在这篇文章找到了可行~~(未实践过)~~的解决方案:使用 BrowserRouter 白屏问题

案例是通过一个导航栏去完成不同页面的跳转,有点像首页这里点击不同的页面能够访问到不同的分区:

案例中会使用两个页面,一个是首页,另一个是博客页面。首页可以通过 http://localhost:3000/http://localhost:3000/home 均可访问,博客页只能通过 http://localhost:3000/blog 进行访问。

页面的路由,大致是这样的:

功能实现

从这以下就看是按步实现路由功能。

实现组件

根据需求总共只有两个页面,所以这里就只需要实现两个组件即可。

鉴于只是一个简单的 Demo,所以组件的实现还是比较简单的:

class Blog extends Component {
  render() {
    return (
      <div>
        <div>GoldenaArcher - Blog</div>
        <div>路由案例,博客页面</div>
        <div>
          可以通过{' '}
          <a href="http://localhost:3000/blog">http://localhost:3000/blog</a>{' '}
          进行访问
        </div>
      </div>
    );
  }
}

页面效果如下:

class Home extends Component {
  render() {
    return (
      <div>
        <div>GoldenaArcher - Home</div>
        <div>路由案例,首页</div>
        <div>
          可以通过 <a href="http://localhost:3000/">http://localhost:3000/</a>{' '}<a href="http://localhost:3000/home">http://localhost:3000/home</a>{' '}
          进行访问
        </div>
      </div>
    );
  }
}

页面效果如下:

实现路由功能

现在,单独的页面已经完成了,要做的就是将 URL 与页面进行绑定。这时候就会用到几个 react-router-dom 已经封装好的,有 react-router 实现的组件:

  • BrowserRouter

    实际上实现于 react-router,文档 中的原文是这么说的:

    A <Router> that uses the HTML5 history API (pushState, replaceState and the popstate event) to keep your UI in sync with the URL.

    也就是说,BrowserRouter 是基于 Router,并使用了 HTML5 的 history API 去保持 UI 与 URL 之间的同步。

  • Switch

    Swith 默认只会渲染第一个匹配的 URL 和组件。

  • Route

    Route 实现了具体的功能:即匹配 URL 和组件的功能。

Route.js 的具体实现如下:

import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';

// 导入组件
import Home from './components/Home';
import Blog from './components/Blog';

const routes = () => (
  <Router>
    <Switch>
      {/* Switch 只会匹配第一个 URL 与 组件 匹配的内容,但是所有的路径都始于 / */}
      {/* 这里的关键字 exact 会匹配与 路径 完全一致的 URL */}
      <Route path={'/'} exact component={Home} />
      <Route path={'/home'} component={Home} />
      <Route path={'/blog'} component={Blog} />
    </Switch>
  </Router>
);

export default routes;

随后在页面中导入 routes,这里是在 app.js 中导入的:

import React from 'react';
import Routes from './Routes';

const App = () => {
  return (
    <div className="App">
      <Routes />
    </div>
  );
};

export default App;

就能产生切换 URL 时导入不同组件的效果了:

创建超链接

传统 HTML 中使用 a 标签对页面进行跳转,上面也是这么做的。但是在 React 中使用 a 标签有一个问题:使用 a 标签进行页面跳转会触发页面的重新加载,并清除当前所有的状态。

对此,解决方案可以用 react-router 提供的 Link 去实现,下面就用 Link 实现一个导航栏,基础结构依旧是 nav > ul > li 这样的格式,只不过 li 下面不是 a 标签,而是 Link:

import React, { Component } from 'react';

import { Link } from 'react-router-dom';

class Nav extends Component {
  // 输出测试,证明没有重复渲染
  componentDidMount() {
    console.log('Nav componentDidMount', new Date().toString());
  }

  // 输出测试,证明没有重复更新
  componentDidUpdate(prevProps, prevState) {
    console.log('Nav componentDidUpdate', new Date().toString());
  }

  render() {
    return (
      <nav>
        <ul>
          <li>
            <Link to="/home">Home</Link>
          </li>
          <li>
            <Link to="/blog">Blog</Link>
          </li>
        </ul>
      </nav>
    );
  }
}

而在 Home 和 Blog 中都需要加入 Nav 组件,所以这里可以选择在 Routes.js 中添加 Nav。一来这样可以少写一些代码;二来 react-router 有对渲染进行优化,即,只 重新渲染有修改的内容,所以一旦 Nav 被渲染了之后,在当前实现内 Nav 不会被重复更新和渲染。实现如下:

import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';

// 导入组件
import Home from './components/Home';
import Blog from './components/Blog';
import Nav from './components/Nav';

const routes = () => (
  <Router>
    {/* Nav 不受 Switch 的空值,所以所有页面都会有 Nav */}
    <Nav />
    <Switch>
      {/* Switch 只会匹配第一个 URL 与 组件 匹配的内容,但是所有的路径都始于 / */}
      {/* 这里的关键字 exact 会匹配与 路径 完全一致的 URL */}
      <Route path={'/'} exact component={Home} />
      <Route path={'/home'} component={Home} />
      <Route path={'/blog'} component={Blog} />
    </Switch>
  </Router>
);

export default routes;

实现效果如下:

能够看到,页面跳转之间非常的流畅,没有出现使用由页面重新加载而造成的白屏现象。同时,componentDidMount 只触发了一次,componentDidUpdate 根本没有触发,证明这个 Nav 的 UI 也没有被继续更新。

至于状态丢失现象,修改一下一部分代码,再去查看效果。这里假设通过导航栏点击导航到 Home 页面,会有一个 { testedValue: true } 这样的属性,实现效果如下:

nav.js 的修改:

<Link
  to={{
    pathname: '/home',
    state: { testedValue: true },
  }}
>
  Home
</Link>

也顺便需要一下 Home.js,看一下输出效果:

class Home extends Component {
  // componentDidMount 会在页面第一次加载时实现
  componentDidMount() {
    console.log(this.state, this.props);
  }

  // 其余不变
}

再看一下效果:

可以看到页面刷新之后,再通过超链接 http://localhost:3000/home 打开页面后,命令行输出的内容重载了。重载后,命令行中输出的对象中可以看到,this.props > location > state 的值是 undefined

而通过导航栏点进去的页面,则是有结果的:

注*:图片均没有抽帧。

模拟登录案例演示状态丢失问题

从现在的情况来看,用 a 标签还是 Link 的确对于应用来说,没有特别大的区别,那下面就模拟一个注册案例来掩饰一下状态丢失的问题。

重写 App.js

这一步主要是为了加上登录事件的处理,以及登陆状态。因为有事件处理,并且需要绑定事件,所以状态会在构造函数之内申明 state。

具体的可以查看:状态 & 状态更新 & 生命周期方法事件处理 两篇笔记。

登陆的状态是要传给所有的组件,所以需要在最高层进行处理,而 App 级的最高层就在 App.js 了,这也是为什么会选择 App.js 进行修改的原因。

import React, { Component } from 'react';

import Routes from './Routes';

class App extends Component {
  constructor() {
    super();
    this.state = {
      loggedin: false,
    };

    this.loginHandler = this.loginHandler.bind(this);
  }

  loginHandler = () => {
    setTimeout(
      () =>
        this.setState((prevState) => ({
          loggedin: !prevState.loggedin,
        })),
      3000
    );
  };

  render() {
    return (
      <div className="App">
        <Routes
          isLoggedin={this.state.loggedin}
          loginHandler={this.loginHandler}
        />
      </div>
    );
  }
}

export default App;

重写 Route.js

这里主要是实现 2 个功能:

  1. 将值传给 Nav 组件,在 Nav 组件中实现基础的登陆功能

  2. 通过状态来管理显示的内容:

    如果已经登陆,即 isLoggedin=true,则显示内容;不然显示 Not Authorized

具体实现如下:

import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';

// 导入组件
import Home from './components/Home';
import Blog from './components/Blog';
import Nav from './components/Nav';

const routes = (props) => (
  <Router>
    {/* Nav 不受 Switch 的空值,所以所有页面都会有 Nav */}
    <Nav isLoggedin={props.isLoggedin} loginHandler={props.loginHandler} />
    {props.isLoggedin ? (
      <Switch>
        {/* Switch 只会匹配第一个 URL 与 组件 匹配的内容,但是所有的路径都始于 / */}
        {/* 这里的关键字 exact 会匹配与 路径 完全一致的 URL */}
        <Route path={'/'} exact component={Home} />
        <Route path={'/home'} component={Home} />
        <Route path={'/blog'} component={Blog} />
      </Switch>
    ) : (
      'Not Authorized'
    )}
  </Router>
);

export default routes;

重写 Nav.js

这一步主要也是为了根据状态显示内容,并且实现登录功能。

代码实现如下:

class Nav extends Component {
  componentDidMount() {
    console.log('Nav componentDidMount', new Date().toString());
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('Nav componentDidUpdate', new Date().toString());
  }

  render() {
    const { isLoggedin, loginHandler } = this.props;
    const statusButton = isLoggedin ? (
      'Logged In'
    ) : (
      <button onClick={loginHandler}>Log in</button>
    );
    return (
      <header>
        <nav>
          <ul>
            <li>
              <Link
                to={{
                  pathname: '/home',
                  state: { testedValue: true },
                }}
              >
                Home
              </Link>
            </li>
            <li>
              <Link to="/blog">Blog</Link>
            </li>
          </ul>
        </nav>
        {statusButton}
      </header>
    );
  }
}

最终实现效果有两个动图。

刷新页面的效果:

和登录后再通过 URL 直接访问的效果:

可以看到刚开始在没有登陆的时候,页面显示的是 Not Authorized(这点其实可以优化下,不过是个简单的 demo 就不再创建一个首页了),在点击了登录按钮之后,约 3 秒之后状态,App.js 中 loggedin 的值更新成了 true,代表着登录成功。因此,Log in 的按钮变成了 Logged In,而 Home 和 Blog 两个组件也可以正常渲染。

这时候就能看到,通过 Link 访问没有任何的问题,可是一旦刷新页面,或者是通过 URL 直接访问,那么 App 的状态就会丢失,在这个案例的情况下,就会被自动登出。

总结

最后总结一下,本章主要内容就是关于 路由功能的实现,以及 页面的跳转

  • 路由功能的实现

    这里主要通过这样的模式去实现:

    <Router>
      <Switch>
        <Route path={'/'} exact component={Home} />
        <Route path={'/home'} component={Home} />
        <Route path={'/blog'} component={Blog} />
      </Switch>
    </Router>
    
  • 页面的跳转

    通过 Link 去实现,使用 Link 能够有效的传递和保留状态,而使用 a 标签会强制重载页面,导致失去整个应用的状态。

    尤其如果某个应用是使用状态去管理登陆信息的,那么使用 a 标签就会导致一直登出,无法正常使用应用,这也通过一个具体的模拟登陆案例来进行了更加详细的演示二者之间的差异。

    在以使用状态为主的应用程序之中,如果使用 a 标签去进行重定向的话,那么影响相对而言会挺大的——包括 redux 在没有使用 redux-persist 持久化之前,一单页面被刷新,也会丢失所有的状态。所以在使用 a 标签之前一定要清楚有清楚的意识:是否想要抹去所有的状态。

以上是关于[React 基础系列] React Router 的基本应用的主要内容,如果未能解决你的问题,请参考以下文章

Vue/React实现路由鉴权/导航守卫/路由拦截(react-router v6)

React路由基础

react-native-router-flux(基础内容)

React Router 基础组件一览

使用react-router+hooks搭建基础框架

为啥react-router中的link用不了