React-Router路由

725 阅读12分钟

路由

后端路由阶段

  • 页面中需要请求不同的路径内容时, 交给服务器来进行处理, 服务器渲染好整个页面, 并且将页面返回给客户端.
  • 这种情况下渲染好的页面, 不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于**SEO的优化**.
  • 缺点:
    • 性能限制:后端路由需要将请求发送到服务器进行处理,然后再将响应返回给客户端。这涉及到网络延迟和服务器负载,可能会对应用程序的性能产生影响。特别是在大量请求同时到达时,后端路由可能面临性能瓶颈。
    • 扩展性限制:后端路由通常依赖于服务器端的处理和资源,这使得在面对高流量或大规模应用程序时扩展变得更加复杂。需要增加服务器资源或采用负载均衡等技术来应对流量增加,这可能导致高成本和复杂性。
    • 开发效率低下:后端路由要求在服务器端编写和维护路由逻辑,这意味着前后端之间需要进行更多的协作和开发工作。此外,由于请求需要经过网络传输,开发和调试的效率可能会受到一定影响。
    • 限制前端技术选择:后端路由通常会限制前端开发人员使用特定的服务器端技术栈。前端开发人员需要熟悉服务器端路由和服务器框架,这可能需要额外的学习成本和时间。
    • 不适合单页应用程序(SPA):后端路由通常是基于请求-响应模型的,不太适合用于构建单页应用程序(SPA),因为SPA通常需要在客户端进行动态的路由和页面切换。后端路由需要每次路由切换时都向服务器发送请求,这会增加不必要的网络开销和延迟。

前后端分离阶段;

  • 前端渲染的理解:

    • 每次请求涉及到的静态资源都会从静态资源服务器获取,这些资源包括HTML+CSS+JS,然后在前端对这些请求回来的资源进行渲染;
    • 需要注意的是,客户端的每一次请求,都会从静态资源服务器请求文件;
    • 同时可以看到,和之前的后端路由不同,这时后端只是负责提供API了;
  • 单页面富应用(SPA);

    • SPA最主要的特点就是在前后端分离的基础上加了一层前端路由.
    • 也就是前端来维护一套路由规则.
  • 前端路由的核心是什么呢?改变URL,但是页面不进行整体的刷新。

URL的hash

前端路由是如何做到URL和内容进行映射呢?监听URL的改变。

URL的hash

  • URL的hash也就是锚点(#),本质上是改变window.location的href属性;
  • 可以通过直接赋值location.hash来改变href, 但是页面不发生刷新;
  • hash的优势就是兼容性更好,在老版IE中都可以运行,但是缺陷是有一个#,显得不像一个真实的路径。
<div id="app">
    <a href="#/home">home</a>
    <a href="#/about">about</a>
    <div class="router-view"></div>
</div>

以下是给提供的代码添加了注释:

// 1. 获取 router-view 元素
const routerViewEl = document.querySelector(".router-view");

// 2. 监听 hashchange 事件
window.addEventListener("hashchange", () => {
    switch (location.hash) { // 根据当前 hash 值设置页面内容
        case "#/home":
            routerViewEl.innerHTML = "home";
            break;
        case "#/about":
            routerViewEl.innerHTML = "about";
            break;
        default:
            routerViewEl.innerHTML = "default";
    }
});
  1. 获取名为 "router-view" 的元素,用于显示路由视图。
  2. 监听浏览器的 hashchange 事件,当 hash 值发生变化时,执行回调函数。根据当前的 hash 值,设置 routerViewEl 元素的内容,以显示对应的页面内容。如果 hash 为 "#/home",显示 "home";如果 hash 为 "#/about",显示 "about";否则显示 "default"。

注意:该代码是使用 hash 值来实现简单的路由功能,根据不同的 hash 值显示不同的页面内容。然而,现代的前端路由通常使用 HTML5 History API 中的 pushState 或 replaceState 方法来修改 URL,而不是使用 hash。使用 hash 的路由在 URL 上会带有 # 符号,并且可能不支持一些现代的前端路由功能和服务器配置。

HTML5的History

  • history接口是HTML5新增的, 它有六种模式改变URL而不刷新页面:
    • replaceState :替换原来的路径;
    • pushState :使用新的路径;
    • popState :路径的回退;
    • go :向前或向后改变路径;
    • forward :向前改变路径;
    • back :向后改变路径;
<div id="app">
    <a href="#/home">home</a>
    <a href="#/about">about</a>
    <div class="router-view"></div>
</div>
// 1. 获取 router-view 元素
const routerViewEl = document.querySelector(".router-view");

// 2. 监听所有的 a 元素
const aEls = document.getElementsByTagName("a"); 
for (let aEl of aEls) {
    aEl.addEventListener("click", (e) => { 
        e.preventDefault(); // 阻止默认行为,不进行页面跳转
        const href = aEl.getAttribute("href"); // 获取链接的 href 属性
        console.log(href); // 打印 href
        history.pushState({}, "", href); // 将链接添加到浏览器历史记录中
        historyChange(); // 执行页面设置操作
    })
}

// 3. 监听 popstate 和 go 操作
window.addEventListener("popstate", historyChange); 
window.addEventListener("go", historyChange);

// 4. 执行设置页面操作
function historyChange() { 
    switch(location.pathname) { // 根据当前路径设置页面内容
        case "/home":
            routerViewEl.innerHTML = "home"; 
            break;
        case "/about":
            routerViewEl.innerHTML = "about"; 
            break;
        default:
            routerViewEl.innerHTML = "default"; 
    }
}
  1. 获取名为 "router-view" 的元素,用于显示路由视图。
  2. 遍历所有的 <a> 元素,并为每个元素添加点击事件监听器。当点击链接时,阻止默认行为,获取链接的 href 属性,并将其添加到浏览器历史记录中。然后执行 historyChange 函数,用于根据当前路径设置页面内容。
  3. 监听浏览器的 popstate 和自定义的 go 事件。当浏览器的前进/后退按钮被点击或通过 JavaScript 的 go 方法调用时,执行 historyChange 函数,用于根据当前路径设置页面内容。
  4. historyChange 函数根据当前路径的不同,设置 routerViewEl 元素的内容,以显示对应的页面内容。如果路径为 "/home",显示 "home";如果路径为 "/about",显示 "about";否则显示 "default"。

React-Router

  • 安装react-router-dom -- npm install react-router-dom

Router的基本使用

  • 选择路由模式 BrowserRouter使用history模式 / HashRouter使用hash模式
<HashRouter>
    <App />
</HashRouter>

路由映射配置

  • Routes:包裹所有的Route,在其中匹配一个路由
    • Router5.x使用的是Switch组件
  • Route:Route用于路径的匹配;
    • path 属性:用于设置匹配到的路径;
    • element 属性:设置匹配到路径后,渲染的组件;
      • Router5.x使用的是 component 属性
    • exact :精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;
      • Router6.x不再支持该属性
   {/* 映射关系: path => Component */}
        <Routes>
          <Route path='/' element={<Home/>}/>
          <Route path='/about' element={<About/>}/>
          <Route path='/login' element={<Login/>}/>
          <Route path='*' element={<NotFound/>}/>
        </Routes>

路由配置和跳转

  • 路由跳转 -- Link组件 / NavLink组件
    • 通常路径的跳转是使用Link组件,最终会被渲染成 a 元素;
    • NavLink是在Link基础之上增加了一些样式属性
    • to 属性 -- 用于设置跳转到的路径
     <div className='nav'>
          <Link to="/home">首页</Link>
          <Link to="/about">关于</Link>
          <Link to="/login">登录</Link>
          <Link to="/user?name=why&age=18">用户</Link>
     </div>
  • NavLink的使用
    • 需求:路径选中时,对应的a元素变为红色
    • 这个时候,我们要使用NavLink组件来替代Link组件:
      • style:传入函数,函数接受一个对象,包含isActive属性
      • className:传入函数,函数接受一个对象,包含isActive属性
      • 事实上在默认匹配成功时,NavLink就会添加上一个动态的active class;
        • 可能出现样式的层叠,也可以自定义class
  <NavLink to="/home" 
  style={({isActive}) => ({color: isActive ? "red": ""})}>首页</NavLink>
  <NavLink to="/about" 
  style={({isActive}) => ({color:  isActive ? "red": ""})}>关于</NavLink>
  //或者
  <NavLink to="home" className={this.getActiveClass}>首页</NavLink>
  getActiveClass({isActive}){
      return classNames({"link-active": isActive})
  }
  • 路由重定向 -- Navigate
    • Navigate用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中:
 <Route path='/' element={<Navigate to="/home"/>}/>
  • Not Found页面配置 -- path="\*"
    • 用户随意输入一个地址,该地址无法匹配,那么在路由匹配的位置显示Not Found页面
 <Route path='*' element={<NotFound/>}/>

路由的嵌套

  • 路由之间是存在嵌套关系的。
  • <Outlet> 组件用于在父路由元素中作为子路由的占位元素。
  <Route path='/' element={<Navigate to="/home"/>}/>
    <Route path='/home' element={<Home/>}>
      <Route path='/home' element={<Navigate to="/home/recommend"/>}/>
      <Route path='/home/recommend' element={<HomeRecommend/>}/>
      <Route path='/home/ranking' element={<HomeRanking/>}/>
    </Route>
  </Routes>
  
   <div>
        <h1>Home Page</h1>
        <div className='home-nav'>
          <Link to="/home/recommend">推荐</Link>
          <Link to="/home/ranking">排行榜</Link>
          <button onClick={e => this.navigateTo("/home/songmenu")}>歌单</button>
        </div>

        {/* 占位的组件 */}
        <Outlet/>
  </div>

手动路由的跳转

  • 通过JavaScript代码逻辑进行跳转(比如点击了一个button),那么就需要获取到navigate对象。
  • 在Router6.x版本之后,代码类的API都迁移到了hooks的写法:
    • 代码跳转,需要通过useNavigate的Hook获取到navigate对象进行操作
 const navigate = useNavigate()
  
  function navigateTo(path) {
    navigate(path)
  }

  return (
    <div className='app'>
      <div className='header'>
        <span>header</span>
        <div className='nav'>
          {/* 点击按钮时,导航到 "/category" 路径 */}
          <button onClick={e => navigateTo("/category")}>分类</button>
          {/* 点击按钮时,导航到 "/order" 路径 */}
          <span onClick={e => navigateTo("/order")}>订单</span>

          <Link to="/user?name=why&age=18">用户</Link>
        </div>
        <hr />
      </div>
      <div className='content'>
        {/* 映射关系: path => Component */}
        <Routes>
          <Route path='/category' element={<Category/>}/>
          <Route path='/order' element={<Order/>}/>
          <Route path='/detail/:id' element={<Detail/>}/>
          <Route path='/user' element={<User/>}/>
          <Route path='*' element={<NotFound/>}/>
        </Routes>
        {useRoutes(routes)}
      </div>
    </div>
  )
  • 类组件
// 定义一个高阶组件 (HOC),它接收一个组件(WrapperComponent)作为参数
function withRouter(WrapperComponent) {
  // 返回一个新的函数组件,接收原始组件的 props 作为参数
  return function(props) {
    // 使用 React Router 的 useNavigate hook 获取导航函数,
    // 它可以用来进行页面跳转
    const navigate = useNavigate();

    // 使用 React Router 的 useParams hook 获取动态路由参数,
    // 例如,在路由 /detail/:id 中,id 就是一个动态路由参数
    const params = useParams();

    // 使用 React Router 的 useLocation hook 获取 location 对象,
    // 它包含了当前URL的信息
    const location = useLocation();

    // 使用 React Router 的 useSearchParams hook 获取 URL 查询参数,
    // 返回一个包含所有查询参数的 URLSearchParams 实例
    const [searchParams] = useSearchParams();

    // 将查询参数的 entries 转化为一个对象,方便使用
    const query = Object.fromEntries(searchParams);

    // 将获取到的路由相关的数据组合到一个对象中
    const router = { navigate, params, location, query };

    // 返回一个新的组件,传递原始的 props 并添加 router prop
    return <WrapperComponent {...props} router={router}/>;
  }
}
这个代码是一个使用类组件的React Router例子。`Home` 是一个 `PureComponent`,它包含了一些导航链接和一个 `Outlet` 组件。

下面是对代码的解释:

```jsx
// 定义一个类组件 Home,它继承自 React.PureComponent
export class Home extends PureComponent {
  // 定义一个方法 navigateTo,用于导航到指定路径
  navigateTo(path) {
    // 从 props 中获取 router 对象,然后获取其中的 navigate 方法
    const { navigate } = this.props.router;
    // 调用 navigate 方法,导航到指定路径
    navigate(path);
  }

  // 定义渲染方法
  render() {
    // 渲染一些导航链接和一个 Outlet 组件
    return (
      <div>
        <h1>Home Page</h1>
        <div className='home-nav'>
          {/* 使用 Link 组件导航到 "/home/recommend" 路径 */}
          <Link to="/home/recommend">推荐</Link>

          {/* 使用 Link 组件导航到 "/home/ranking" 路径 */}
          <Link to="/home/ranking">排行榜</Link>

          {/* 点击按钮时,使用 navigateTo 方法导航到 "/home/songmenu" 路径 */}
          <button onClick={e => this.navigateTo("/home/songmenu")}>歌单</button>
        </div>

        {/* Outlet 组件,它用于显示当前路径对应的子路由组件 */}
        <Outlet/>
      </div>
    )
  }
}

在这个组件中,Link 组件用于创建导航链接,Outlet 组件用于渲染子路由。点击按钮时,会调用 navigateTo 方法,该方法会使用 navigate 函数导航到指定路径。

路由参数传递

  • 动态路由的方式;
    <Link to="/user/9978">用户</Link>
    <Route path="/user/:id" element={<User />}></Route>
    
    // 获取动态路由参数 -- 需要通过 useParams 只能在函数式组件中使用
    import { useParams } from "react-router-dom";
    export function User() {
      const params = useParams()
      return (
        <div>
          <h4>User Page</h4>
          <h4>id: {params.id}</h4>
        </div>
      )
    }
    
  • search传递参数;
 {/* 创建一个链接,点击后会导航到 "/user" 路径,并带有查询参数 "name=coder" 和 "age=19" */}
    <Link to="/user?name=coder&age=19">用户</Link>
    
function MyComponent() {
  // 获取 URL 查询参数
  const [searchParams] = useSearchParams();

  // 将查询参数转换为 JavaScript 对象
  const query = Object.fromEntries(searchParams);

  // 现在你可以使用 query.name 和 query.age 获取查询参数了

  return (
    <div>
      {/* 显示查询参数 */}
      <p>名字: {query.name}</p>
      <p>年龄: {query.age}</p>
    </div>
  );
}

路由的配置文件

  • 早期,Router并且没有提供相关的API,我们需要借助于react-router-config完成;
  • 在Router6.x中,为我们提供了useRoutes API可以完成相关的配置
image.png
    <div>  {useRoutes(routes)} </div>
  • 对某些组件进行了异步加载(懒加载),那么需要使用Suspense进行包裹:
// 在单独的router/index.js文件中

// 路由懒加载/按需加载/异步加载 ? 
// 这样暂时不会显示,因为是异步的要单独下载,需要加载一个loading动画React提供的Suspense组件
const Order = React.lazy(() => import("../page/Order"));
const User = React.lazy(() => import("../page/User"));

const router = [
    {
        path: '/',
        element: <Navigate to="/home" />,
    },
    {
        path: "/home",
        element: <Home />,
        children: []
    }
]
export default router
<HashRouter>
  <Suspense fallback={<h4>Loading~~~~</h4>}>
    <App />
  </Suspense>
</HashRouter>

类组件无法直接使用navigate、location等参数,应该如何进行操作?

  • 封装

这是一个React中的高阶组件(HOC),它包装另一个组件,并为其提供与路由相关的属性。

import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";
// 这是一个接收组件(WrapperComponent)作为参数的函数
function withRouter(WrapperComponent) {
  // 它返回另一个接收props作为参数的函数
  return function(props){
    // 这个钩子函数允许你以编程的方式进行导航。它相当于react-router v5中的history.push。
    const navigate = useNavigate()

    // 这个钩子函数允许你访问当前动态路由的参数
    const params = useParams()

    // 这个钩子函数返回当前的位置对象,该对象包含了当前URL的信息
    const location = useLocation();

    // 这个钩子函数返回一个新的URLSearchParams对象,该对象表示当前URL的查询参数
    const [searchParams] = useSearchParams();

    // 将URLSearchParams对象转换成普通的JavaScript对象,方便访问
    const query = Object.fromEntries(searchParams);

    // 将所有这些与路由相关的对象聚合到一个对象中
    const router = {navigate, params, location, query}

    // 返回WrapperComponent,扩展原始的props,并添加新创建的router对象作为一个属性
    return <WrapperComponent {...props} router={router} />
  }
}
export default withRouter

这个函数旨在为组件提供方便访问路由相关功能和数据的方式,如导航、URL参数和查询参数。你可以使用这个函数来包装任何需要使用这些功能的组件。

  • 使用

这个代码首先从 'react' 中导入了 React 和 PureComponent,以及从一个名为 "../hoc/with_router" 的文件中导入了 withRouter 高阶组件。

然后定义了一个名为 About 的组件,它继承自 PureComponent。这个组件有一个名为 navigateTo 的方法,它使用了 router 属性中的 navigate 方法来进行导航。在 render 方法中,它从 router 属性中获取了 query 对象,并在页面上展示了 query.name 和 query.age 的值。

最后,它使用 withRouter 高阶组件包装了 About 组件,并将结果作为默认导出。这样,About 组件就可以使用 router 属性中的 navigate 方法和 query 对象了。

// 从'react'中导入React和PureComponent
import React, { PureComponent } from 'react'
// 从"../hoc/with_router"中导入withRouter高阶组件
import withRouter from "../hoc/with_router"

// 定义一个名为About的PureComponent组件
export class About extends PureComponent {
  // 定义一个导航方法,接收一个路径作为参数
  navigateTo(path) {
    // 从props中解构出router对象,然后从router中解构出navigate方法
    const { navigate } = this.props.router
    // 调用navigate方法,进行页面导航
    navigate(path)
  }
  // 定义render方法,返回组件的JSX结构
  render() {
    // 从props中解构出router对象,然后从router中解构出query对象
    const { query } = this.props.router
    // 返回一个div元素,其中包含一个标题和一个显示query.name和query.age的文本
    return (
      <div>
        <h3>About Page</h3>
        <h4>name: {query.name}-age: {query.age}</h4>
      </div>
    )
  }
}
// 使用withRouter高阶组件包装About组件,并导出结果
export default withRouter(About)

这个 About 组件通过 withRouter 高阶组件得到了 router 属性,可以通过它进行导航以及获取查询参数。