07-06-React-Router的单页应用

课程目标

  • 理解前端路由的作用;
  • 掌握 React-Router 各 API 使用细节;
  • 可根据项目需求,在 React 项目中,组织合理的路由方案。
  • 路由:根据不同的 url 规则,给用户展示不同的视图(页面);
  • 当应用变得复杂的时候,就需要分块的进行处理和展示,传统模式下,我们是把整个应用分成了多个页面,然后通过 url 进行连接。但是这种方式也有一些问题,每次切换页面都需要重新发送所有请求和渲染整个页面,不止性能上会有影响,同时也会导致整个 JavaScript 重新执行,丢失状态。
  • Single Page Application:单页面应用,整个应用只加载一个页面(入口页面),后续在与用户的交互过程中,通过 DOM 操作在这个单页上动态生成结构与内容。
  • 有更好的用户体验(减少请求、渲染和页面跳转产生的等待与空白),页面切换快;
  • 重前端,数据和页面内容由异步请求(AJAX)+ DOM 操作来完成,前端处理更多的业务逻辑。
  • 首次进入慢;
  • 不利于 SEO。
  • SPA 的页面切换机制

  • 虽然 SPA 的内容都是在一个页面通过 JavaScript 动态处理的,但是还是需要根据需求在不同的情况下区分内容展示,如果仅仅只是依靠 JavaScript 内部机制去判断,逻辑会变得过于复杂,通过把 JavaScript 与 URL 进行结合的方式:JavaScript 根据 URL 的变化,来处理不同的逻辑,交互过程中只需要改变 URL 即可。这样把不同的 URL 与 JavaScript 对应的逻辑进行关联的方式就是路由,其本质上与后端路由的思想是一样的。
  • 前端路由只是改变了 URL 或 URL 中的某一部分,但一定不会直接发送请求,可以认为仅仅只是改变了浏览器地址栏上的 URL 而已,JavaScript 通过各种手段处理这种 URL 的变化,然后通过 DOM 操作来动态的改变当前页面的结构;
  • URL 的变化不会直接发送 HTTP 请求;
  • 业务逻辑由前端 JavaScript 来完成。
  • 目前前端路由的主要模式
  • 基于 URL Hash 的路由;
  • 基于 HTML5 History API 的路由。
  • React Router

  • 理解了路由的基本机制以后,也不需要重复造轮子,我们可以直接使用 React Router 库;
  • React Router 提供了多种不同环境下的路由库:web、native;
  • 官网: https://reactrouter.com/
  • 基于 Web 的 React Router

  • 基于 web 的 React Router 为:react-router-dom;
  • 安装:npm i -S react-router-dom。
  • BrowserRouter 组件 -- history
  • 基于 HTML5 History API 的路由组件。
  • HashRouter 组件 -- hash
  • 基于 URL Hash 的路由组件。
  • Route 组件:
  • 匹配规则(v6 之前):
  • 默认模糊匹配,当前 URL 以该 path 为开始时,则匹配成功;
  • exact:精确匹配,URL===path || URL===path/;
  • strict:严格匹配,URL===path,要注意 strict 必须与精确匹配一起使用才生效;
  • 多路径匹配:通过数组实现;
  • 动态路由,见最下面。
  • 渲染视图:
  • component;
  • render。
  • import { Route } from 'react-router-dom';
    import About from './views/About';
    import Hire from './views/Hire';
    import Home from './views/Home';
     * 匹配规则:
     * 1、默认情况-——模糊匹配,当前 URL 以该 path 为开始时,就能匹配:/index、/index/、/index/xxx;
     * 2、exact——精确匹配,URL===path || URL===path/;
     * 3、strict——严格匹配,URL===path,但是需要与 exact 一起使用才生效;
     * 4、多路径匹配——通过数组匹配。
    function App() {
      // 当有值需要传递给对应路由组件时,可以通过 render 属性实现。
      const user = {
        name: 'yjw',
        age: 18
      return (  
          <div>React Router Page</div>
          <Route exact path={['/', '/home', '/index']} component={Home} /> 
          <Route exact strict path='/about' component={About} />
          <Route 
            path='/hire' 
            render={() => {
              return <Hire user={user} />
    export default App;
    
    // react-router 6 之后的使用方式
    import { Routes, Route } from 'react-router-dom';
    import About from './views/About';
    import Hire from './views/Hire';
    import Home from './views/Home';
    function App() {
      return (  
          <Routes>
            {/* 默认 精确匹配,严格匹配 */}
            <Route path='home' element={<Home />} /> 
            <Route path='/about' element={<About />} />
            <Route path='/hire' element={<Hire />} />
          </Routes>
    export default App;
    
  • 链接组件:
  • Link;
  • NavLink:
  • activeClassName;
  • activeStyle;
  • isActive: function。
  • // 可以使用 a 标签实现,但是页面每次都会刷新
    export default () => {
      return <>
        <a href='/home' >首页</a>
        <span> | </span>
        <a href='/about' >关于</a>
        <span> | </span>
        <a href='/hire' >加入</a>
    
    import { Link, NavLink } from "react-router-dom"
     * 应用内链接:Link 或者 NavLink
     * 应用外链接:a 标签
     * NavLink 用于导航的链接制作
     * - 当当前的 url 和 NavLink 的 to 属性匹配后,则会给当前的标签加一个选中状态,注:NavLink 默认情况下也是模糊匹配;
     * - activeClassName:当前项被选中后的 className,默认为 active;
     * - activeStyle:当前项被选中后的 style;
     * - isActive:function,返回一个 Boolean 值,表示该标签的 class、style 始终是 active 状态或者 非active 状态。
    export default () => {
      return <div>
        <NavLink to='/' activeClassName='homeActive'>首页</NavLink>
        <span> | </span>
        <NavLink to='/about' activeStyle={{color: 'red'}}>关于</NavLink>
        <span> | </span>
        <NavLink to='/hire' isActive={()=>{return true}} activeStyle={{color: 'yellow'}}>加入</NavLink>
        <span> | </span>
        <a href='https://www.baidu.com' >百度</a>
    
  • Switch 组件:只匹配一个路径;
  • Redirect 组件:当输入的 url 不合法时,可以重定向到 404:
  • form 属性;
  • to 属性。
  • import { Route, Switch, Redirect } from 'react-router-dom';
    import Nav from './Nav';
    import About from './views/About';
    import Hire from './views/Hire';
    import Home from './views/Home';
    import View404 from './views/404.js';
    function App() {
      // 当有值需要传递给对应路由组件时,可以通过 render 属性实现。
      const user = {
        name: 'yjw',
        age: 18
      return (  
          <div>React Router Page</div>
          <Nav />
          <Switch>
            <Route exact path={['/', '/home', '/index']} component={Home} /> 
            <Route exact path='/about' component={About} />
            <Route 
              path='/about/join' 
              render={() => {
                return <Hire user={user} />
            <Route path='/404' component={View404} />
            {/* 当路径找不到时,显示404页面,并且将 url 显示为404 */}
            <Redirect to='/404' />
          </Switch>
    export default App;
    
  • 通过 props,可以获取 history、location、match 和 staticContext。
  • history
  • action:“PUSH” || “POP” || “REPLACE”;
  • “PUSH”:应用内通过连接跳转到当前视图的,或者通过 push 方法跳转到当前视图;
  • “POP”:直接输入地址跳转到当前应用,或者从外部链接跳转进来的;
  • “REPLACE”:通过重定向跳转或者通过 replace 方法跳转;
  • go:function,go(n)——跳转历史记录n步;
  • goBack:function,goBack()——返回历史记录上一步;
  • goForward:function,goForward()——前进历史记录下一步;
  • length:当前源在历史记录中记录的条目数;
  • push:function,push(path[,state])——跳转视图,向历史记录中,添加新的一条记录,从而影响视图;state 的值其实就是修改 location 下面的 state 的值,同 replace 方法中的 state;
  • replace:function,replace(path[,state])——跳转视图,替换掉历史记录中当前这条;
  • block:当离开当前组件时,会弹窗提示;全局函数,如果只需要在当前组件进行弹窗提示,在当前组件即将卸载时,调用该方法的返回值进行移除;
  • createHref:function,createHref(location),当 url 比较复杂时,比如含有参数和hash,这时可以使用该方法,返回一个 url 地址,但是需要再调用 push、replace 来进行跳转;
  • listen:监听 url 跳转,如果跳转了会打印 location和action,同样是全局函数。
  • location:
  • hash:hash 值,url 中 # 后面的内容;
  • pathname:当前的 url,不包含参数和hash;
  • search:当前的 search 值,? 后面的内容;
  • state:undefined、push 或 replace 传递的信息。
  • match:匹配信息
  • isExact:boolean,和 Route 中配置没有关系,取决于当前 path 和 url 是否能精确匹配;
  • params:{} 动态路由的参数;
  • path:和 pathname 不是一个概念,这里的 path 是当前 route 的 path 值;
  • url:当前 url 中,被当前 path 匹配成功的部分。
  • 路由信息获取

    高阶路由 - withRouter
  • 非路由组件获取路由信息
    import { NavLink, withRouter } from "react-router-dom";
    function SubNav(props) {
      console.log('SubNav: ', props);
      return (  
        <div className='sub-nav'>
          <NavLink 
            isActive={(...args) => {
              console.log('arg: ', args)
              return true;
            to='/list/all'
          >全部</NavLink>
          <NavLink to='/list/good'>精华</NavLink>
          <NavLink to='/list/share'>分享</NavLink>
          <NavLink to='/list/ask'>问答</NavLink>
    const Nav = withRouter(SubNav);
    export default Nav;
    
    路由 Hooks,v5 版本之后引入了 Hooks。
  • useLocation();
  • useHistory();
  • useRouteMatch();
  • useParams():获取动态路由的参数信息。
  • 以下为完整练习代码:
    // App.js
    import { Route, Switch, Redirect } from 'react-router-dom';
    import Nav from './Nav';
    import About from './views/About';
    import Hire from './views/Hire';
    import Home from './views/Home';
    import View404 from './views/404.js';
    import List from './views/list/List';
    import './index.css';
    const types = ['good', 'good', 'share', 'ask'];
    function App() {
      // 当有值需要传递给对应路由组件时,可以通过 render 属性实现。
      const user = {
        name: 'yjw',
        age: 18
      return (  
        <div className="wrap">
          <div>React Router Page</div>
          <Nav />
          <Switch>
            <Route path={['/home', '/index']} component={Home} /> 
            <Route path='/about' component={About} />
            <Route 
              path='/join' 
              render={() => {
                return <Hire user={user} />
              component 自带路由参数,通过 render 传递路由参数时需要手动将参数传递过去 
              - 动态路由: :代表动态路由,: 后面代表的是名字
            {/* <Route 
              // path={['/list', '/list/:type', '/list/:type/:page']}
              path='/list/:type?/:page?'
              render={ (props) => {
                return <List {...props}/>
              exact
            /> */}
            <Route 
              path='/list/:type?/:page?'
              exact
              render={({match}) => {
                console.log('route-> ', match)
                const {type='good', page='1'} = match.params;
                if(types.includes(type) && String(parseInt(page))===page && parseInt(page)>0) {
                  return <List />
                } else {
                  return <Redirect to="/404" />
            <Route path='/404' component={View404} />
            <Redirect to='/404' />
          </Switch>
     * list 的路径问题:
     * - /list
     * - /list/分类
     * - list/分类/页码
    export default App;
    
    // Nav.js
    import { Link, NavLink } from "react-router-dom"
     * 应用内链接:Link 或者 NavLink
     * 应用外链接:a 标签
     * NavLink 用于导航的链接制作
     * - 当当前的 url 和 NavLink 的 to 属性匹配后,则会给当前的标签加一个选中状态,注:NavLink 默认情况下也是模糊匹配;
     * - activeClassName:当前项被选中后的 className,默认为 active;
     * - activeStyle:当前项被选中后的 style;
     * - isActive:function,返回一个 Boolean 值,表示该标签的 class、style 始终是 active 状态或者 非active 状态。
    export default () => {
      return <div className='nav'>
        <NavLink to='/home' >首页</NavLink>
        <span> | </span>
        <NavLink to='/about' >关于</NavLink>
        <span> | </span>
        <NavLink to='/join' >加入</NavLink>
        <span> | </span>
        <NavLink to='/list' >产品列表</NavLink>
    
    // List.js
    import ListList from "./ListList";
    import Pagination from "./Pagination";
    import SubNav from "./SubNav";
    function List(props) {
      return (  
          <h3>List</h3>
          <SubNav />
          <ListList />
          <Pagination />
    export default List;
    
    // SubNav.js
    import { NavLink, useParams } from "react-router-dom";
    function SubNav(props) {
      const { type='good' } = useParams();
      return (  
        <div className='sub-nav'>
          <NavLink to='/list/good'>精华</NavLink>
          <NavLink to='/list/share'>分享</NavLink>
          <NavLink to='/list/ask'>问答</NavLink>
    export default SubNav;
    
    // Pagination.js
    import { useParams } from "react-router";
    import { Link } from "react-router-dom";
    import data from './data';
    const limit = 6;
    function Pagination() {
      const { type='good', page='1' } = useParams();
      const nowData = data[type];
      const pageLen = Math.ceil(nowData.length/limit);
      const renderPage = () => {
        const inner = []
        for (let i = 1; i <= pageLen; i++) {
          if(pageLen === Number(page)) {
            inner.push(<span key={i}>{i}</span>)
          } else {
            inner.push(<Link to={`/list/${type}/${i}`} key={i}>{i}</Link>)
        return inner;
      return (  
          {renderPage()}
    export default Pagination;
    
    // ListList.js
    import { useParams } from "react-router";
    import data from './data';
    const limit = 6;
     * 求页数
     * 每页的第一条:(page-1) * 6
    function ListList() {
      const { type='good', page='1' } = useParams();
      const nowPage = Number(page);
      const start = (nowPage - 1) * limit;
      const end = nowPage * limit;
      const nowData = data[type]?.filter((item, index)=>(index>=start && index<=end));
      return ( 
            {nowData?.map(d => {
              return <li key={d.id}>{d.title}</li>