React 学习之路由(React Router)

3,166 阅读9分钟

React Router

react-router:路由核心库,包含诸多和路由功能相关的核心代码

react-router-dom:利用核心库 (它依赖 react-router),结合实际的页面,实现跟页面路由相关的功能

所以我们一般都安装使用 react-router-dom

路由的两种模式

我们都知道,url 地址 (如:https://www.suressk.com:443/article?uid=sures&id=1008#hash) 由以下几部分组成:

  1. 网络协议 (schema):https (http / https / file等)
  2. 主机名 (host):www.suressk.com (一般有 ip 地址域名预设值(如 localhost)局域网中的电脑名等)
  3. 端口号 (port):443 (http 默认 80https 默认 443)
  4. 访问路径 (path):/article
  5. 查询参数 (query / search):?uid=sures&id=1008
  6. 哈希 (hash / 锚点):#hash

Hash Router

根据 url 地址中的 hash 值来确定显示的组件

因为 hash 值变化不会导致页面刷新
它的兼容性比较好,老版浏览器也支持

Browser History Router

HTML5 出现后,新增 History API,从而使浏览器拥有了改变路径而不刷新页面的能力。History 表示浏览器的历史记录,它使用栈的方式存储,当我们每访问一个路径,它会将这个栈中加入一条路径记录

window.history

  1. history.length:获取当前 tab页 历史记录条数

  2. history.pushState(data, title, url):向当前历史记录栈中加入一条新记录

    • data:附加的数据信息
    • title:页面标题 (大部分浏览器不支持)
    • url:新的路径地址
  3. history.replaceState(data, title, url):替换历史记录栈当前的历史记录

从而,我们可以根据路径来决定渲染哪个组件

路由组件

react-router 为我们提供了两个重要组件:RouterRoute 以及一些其他的组件

Router 组件

它本身不做任何展示,仅提供路由模式配置;此组件会产生一个上下文,上下文会提供一些实用的对象和方法,react-router-dom 提供下面两个组件:

  1. HashRouter:使用 hash 模式 匹配

  2. BrowserRouter:使用 BrowserHistory 模式 匹配

  3. MemoryRouter:将 url 的历史记录保存在内存中 (不读取或写入地址栏),一般用于 非浏览器环境 (如:React Native) (这里不讨论)

通常情况下,仅使用一个 Router 组件,而且用它来包裹整个页面

Route 组件

根据不同的地址,展示不同的组件,它有两个重要属性:pathcomponent

  1. path:匹配的路径规则 (涉及后面说到的 "动态路径"),它也可以是一个 路径正则数组

    • 默认是不区分大小写的 (可以配合设置 sensitive 属性为 true,使路径匹配 区分大小写)
    • 默认是模糊匹配,只要路径存在,则认为匹配成功 (可以配合设置 exact 属性为 true 来精确匹配)
    • 如果不设置 path,则匹配任意路径
  2. component:路径匹配成功后需显示的组件

  3. Routechildren (子元素)

    • 传递 React 元素,无论路径是否成功匹配,只要经过对此 Route 组件 的匹配,那么一定会显示 children,且会 忽略 Route 组件的 component 属性

    • 传递一个函数,该函数有多个参数 (来自 Router 组件产生的上下文),改函数返回 React 元素,则一定会显示返回的元素,且会 忽略 Route 组件的 component 属性

  4. exact 属性:精确匹配

  5. sensitive 属性:路径匹配 区分大小写

  6. strict 属性:Boolean 是否严格匹配路径的最后一个斜杠 (为 true 时,to="/a",而访问路径为 "/a/" 则匹配失败)

import React from 'react'
import {BrowserRouter as Router, Route} from 'react-router-dom'
function CompA() {
    return <div>CompA</div>
}
function CompB() {
    return <div>CompB</div>
}
function CompC() {
    return <div>CompC</div>
}
export default function App() {
    return (<Router>
        <Route path='/a' component={CompA} />
        <Route path='/b' component={CompB}>
            <div style={{ color: "#f40" }}>Route Children Prop</div>
        </Route>
        <Route component={CompC} />
    </Router>)
}

Route 组件可以写在任意位置,只要保证它是 Router 组件的后代元素即可

withRouter 高阶函数

比如说有这么个使用场景,CompB 组件并不是 Route 组件匹配显示的组件,而是其匹配组件的后代组件,但也需要使用 historylocationmatch 对象信息:

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

function CompA() {
    return <div>
        CompA
        <CompB />
    </div>
}
function CompB() {
    /* 这里是拿不到 Route 组件注入的 history, location, match 对象信息的 */
    return <div>CompB</div>
}
export default function App() {
    return (<Router>
        <Route path='/a' component={CompA} />
    </Router>)
}

所以,我们需要使用 withRouter 这个高阶函数来包装 CompB 组件,它内部会拿到 Router 组件创建的上下文,并将其作为属性传递给返回的新组件:const RouteCompB = withRouter(CompB),这样在 CompB 组件内部就能使用 historylocationmatch 对象的信息了

Switch 组件

它会依次进行路径匹配,若路径匹配成功,则不会继续往后匹配 (不加此组件的默认情况下会将 Route 组件的路径全部进行匹配渲染)

由于 Switch 组件会循环所有的子元素 (按照书写 Route 组件 的顺序去依次匹配),若匹配成功,则渲染对应的组件,停止循环;因此,Switch 子组件不能是 RouteRedirect 组件以外的其他组件

// 比如,用法如下:
import React, { memo } from 'react'
import {BrowserRouter as Router, Route, Switch} from 'react-router-dom'

function CompA() {
    return <div>CompA</div>
}
function CompB() {
    return <div>CompB</div>
}
function CompC() {
    return <div>CompC</div>
}

function Task() {
    return (<Router>
        <Switch>
            <Route path="/a/b">
                <CompB />
            </Route>
            <Route path="/a" component={CompA}>
                <div style={{ color: "#f40" }}>Route Children Prop</div>
            </Route>
            <Route component={CompC} />
        </Switch>
    </Router>)
}
export default memo(Task)

这个示例,当我们访问 本地 server 地址 /a/b 路径时,你会发现页面渲染的内容是:CompB,虽然 /a 路径展示 红色 Route Children Prop 也满足路径匹配,但 Switch组件 匹配显示 CompB组件 后就停止了

Link 组件

生成一个无刷新作用的 a 元素

  1. to 属性:

    • 可以是一个字符串:<Link to="/about" /> / <Link to="/courses?sort=name" />

    • 可以是一个包含 pathnamesearchhashstate 四个属性的对象:

      <Link to={{
          pathname: "/courses",
          search: "?sort=name",
          hash: "#heading",
          state: { key: "value" }
      }} />
      
    • 可以是一个函数,内部返回字符串或对象

  2. replace 属性:Boolean,表明当前 Link 组件 跳转是否采用替换模式 (默认 push 方式跳转)

  3. innerRef 属性:用于获得 Link 组件实际渲染元素的 DOM 元素

    • 函数:会将这个渲染元素的 DOM 元素作为这个函数的参数
    • 对象:通过 React.createRef() 创建的 ref 对象

NavLink 组件

它具备 Link 组件 的所有功能,还可以根据渲染元素的链接地址与当前浏览器访问路径进行匹配,为匹配上的元素添加一个 active 类名 (默认匹配上路径的类名)

  1. activeClassName 属性:路径匹配成功时的选中类名 (替换默认的 .active)
  2. activeStyle 属性:路径匹配成功的内联样式
  3. exact 属性:Boolean,是否精确匹配
  4. sensitive 属性:Boolean,路径匹配是否区分大小写
  5. strict 属性:Boolean,是否严格匹配路径的最后一个斜杠

Redirect 组件

重定向组件,当加载到该组件时,会无刷新地跳转页面

  1. to 属性:重定向的路径 (字符串对象)
  2. push 属性:Boolean,默认 false (即按 replace 的模式重定向)
  3. from 属性:当匹配到 from 的地址规则时,才进行重定向跳转
  4. exact 属性:Booleanfrom 是否精确匹配
  5. sensitive 属性:Booleanfrom 路径匹配是否区分大小写
  6. strict 属性:Booleanfrom 路径是否严格匹配的最后一个斜杠

路由信息

Router 组件会创建一个 Context,并且会向 Context 中注入一些信息。这个 Context 对开发者是隐藏的,Route 组件匹配到了 pathRoute 组件会将这些 Context 中的信息作为属性传递给对应的组件:

history

  • 非 window.history 对象,我们可以利用该对象进行无刷新跳转等操作

  • push(relativePath, data?):将某个新地址入栈 (历史记录栈),第一个参数为跳转的相对路径;第二个参数即为跳转过去附加的状态数据 (后面可通过 props.history.location.state 获取,但这个状态数据依赖于跳转,若直接访问这个跳转的路径,状态数据就为空)

  • replace(relativePath, data?):将某个新地址替换历史记录栈的当前记录

  • go() / forward() / back():用法与 window.history 中的同名方法一样

location

  • props.history.location 是同一个对象,它里面记录了当前地址的相关信息 (search / hash / pathname / state),我们通常会使用第三方库 query-string 来解析参数数据 (parse):

  • location.search?a=1&b=2&c=3 解析为 {a: 1, b: 2, c: 3}

  • location.hash#d=4&e=5 解析为 {d: 4, e: 5}

match

  • 保存路由匹配的相关信息

  • isExact:指当前路由路径与 Route 组件 配置的路径是否精确匹配,与 Route 组件 是否设置 exact 属性无关

  • params:会根据 Route 组件 配置的动态参数,将地址栏参数对应位置的数据收集到这个对象中,如:

    • <Route path="/news/:year/:month?/:day?" component={News} />,访问路径是:/news/2021/7,那么 params = {year: '2021', month: '7', day: undefined}

    • <Route path="/news/:year(\d+)/:month?/:day?" component={News} />,加入正则表达式 (年份是数字),若访问路径是:/news/2021/7,那么 params = {year: '2021', month: '7', day: undefined};若访问路径是:/news/suressk/7,则此路径匹配失败

  • path:匹配上的路由路径使用的路径正则匹配规则 (如上面的示例:"/news/:year(\d+)/:month?/:day?")

  • url:实际匹配上的路由路径

react-router 使用了 path-to-regexp 来解析路径正则字符串,将它解析转换成一个真正的正则表达式

通常,向页面传递数据的方式有:

  1. 使用 state:依赖于手动使用 history 对象 跳转时传递数据

  2. 使用 search:在地址栏通过查询参数携带 /news?year=2021&month=7&day=21

  3. 使用 hash:将数据加到地址栏的 hash 值后 /news#year=2021&month=7&day=21

  4. 使用 params:将数据填写到 路径/news/2021/7/21

Hooks

如上面说到 withRouter 高阶函数时,我们需要在某些 Route 匹配到的组件内部使用相关对象或信息,我们就需要使用高阶组件去包一层来通过属性获取,但我们可能只需要使用这其中的某一个对象,那么就可以 在函数组件中 使用以下 Hooks

useHistory

import { useHistory } from "react-router-dom"

function HomeButton() {
    const history = useHistory() // 获取到 history 对象
    const handleClick = useCallback(() => {
         history.push("/")
    }, [history])
    return (
        <button type="button" onClick={handleClick}>
            Go home
        </button>
    )
}

useLocation

import { useLocation } from "react-router-dom"

function HomeButton() {
    const location = useLocation() // 获取到 location 对象
    // other code...
    return (<>
       {/* anything... */} 
    </>)
}

useParams

import {BrowserRouter as Router, Route, Switch, useParams} from "react-router-dom"

function ArticleDetail(){
    const {uid} = useParams() // 获取到路径匹配的 params 对象 => 路径参数 uid
    return <div>The article uid is "{uid}"</div>
}

function App() {
    // other code...
    return (<Router>
       <Switch>
           <Route path="/" exact component={HomePage} />
           <Route path="/article/:uid" exact component={ArticleDetail} />
           <Route component={NotFoundPage} />
       </Switch>
    </Router>)
}