Skip to content

5 路由

约 857 个字 267 行代码 预计阅读时间 6 分钟

SPA(Single Page Application)

  • 整个应用只有一个完整的页面
  • 点击页面中的链接「不会刷新」页面(只会局部更新)
  • 数据需要通过 Ajax 获取,在前端异步展示

所有不存在的资源都返回 index.html 兜底...

Intro

React-Router 在 npm 上共发布了三个不同的版本

  • react-router:核心库
  • react-router-dom:添加了DOM组件,比如 BrowserRouter
  • react-router-native:添加了用于 ReactNative 的 API

1 React-Router-Dom@5

  • 一般把普通组件放在 components 下,路由组件放在 pages
  • 路由组件默认会收到 history, location, match 三个 props

Set Up

npm i react-router-dom@5 # 此处学习 Version 5

Basic Usage

import { Link, Route, BrowserRouter } from 'resct-router-dom'
// import 所有要用到的路由组件
export default class App extends Component {
    render() {
        return (
            {/* 由同一对「BrowserRouter」包裹的内容视为独立的路由规则
                所以这里干脆把 App 全包上了 */}
            <BrowserRouter>
                <h1>This is APP!!!</h1>
                {/* 编写路由链接,相当于 a 标签 */}
                <Link to="/home">Home</Link>
                <Link to="/shop">Shop</Link>
                {/* 注册路由,相当于 route + router-view */}
                <Route path="/home" component={Home}/>
                <Route path="/shop" component={Shop}/>
            </BrowserRouter>
        )
    }
}
// 还有另外一种完全包裹法 @ index.js
ReactDOM.render(
    <BrowserRouter>
        <App/>
    </BrowserRouter>,
    document.getElementById('root')
)
// 其实用 HashRouter 也行(会多一个'#',认为后面的都是前台资源)

1.1 标签式路由

<Link> 标签不支持动态高亮效果

export default class App extends Component {
    render() {
        return (
            <BrowserRouter>
                {/* NavLink 会默认为当前选中的 tag 追加 active作为类名
                    也可以用 activeClassName 进行指定捏 */}
                <NavLink activeClassName="demo" to="/home">Home</Link>
                <NavLink to="/shop">Shop</Link>
            </BrowserRouter>
        )
    }
}

下面对 NavLink 进行一些小小的封装(一直写一样的 tag 真的很烦诶!)

// MyNavLink 是一般组件,只是封装了 activeClassName & className 部分
// App 中的使用(使用 props 和 slot 传递个性数据)
<MyNavLink to="/home">Home</MyNavLink>
// 组件定义
export default class MyNavLink extends Component {
    render() {
        return (
            {/* 把所有接受的 prop 都塞进 tag 的属性里了 */}
            <NavLink {...this.props} activeClassName="active" className="demo">
                {this.props.children}
            </NavLink>
        )
    }
}
// 鉴于 NavLink 的标签体内容可以直接用 children 属性配置
<MyNavLink to="/home" children="Home"/>
return (
    <NavLink {...this.props} activeClassName="active" className="demo" />
)

Switch 标签

实现 First Match,提高匹配效率

// 用 switch 包裹 route 就可以了
// 默认:Last Match -> /shop 显示 ABC
// Switch:First Match -> /shop 显示 Shop
<Switch>
    <Route path="/home" component={Home}/>
    <Route path="/shop" component={Shop}/>
    <Route path="/shop" component={ABC}/>
</Switch>

样式丢失

刷新后通过 ./index.css 请求资源,cur_path 其实已经不是根目录了,实际返回的是 index.html

下面是三种解决方案:

  1. index.html 中的 link 路径改成 /index.css (去掉最前面的 .
  2. index.html 中的 link 路径改成 %PUBLIC_URL%/index.css (去掉最前面的 .
  3. 使用 HashRouter# 后作为 hash 值,不影响 cur_path

匹配模式

  • 模糊匹配:前缀包含就算匹配上
  • 严格匹配:
    • 全等匹配,需要配置 <Route exact={true}/> 或者简写 <Route exact/>
    • 可能会影响嵌套路由的正常显示

重定向

// 放在路由注册的「最下方」进行兜底 - 全都不符合就跑到 redirect 去
<Switch>
    <Route path="/home" component={Home}/>
    <Route path="/shop" component={Shop}/>
    <Redirect to="/home" />
</Switch>

嵌套路由

开启严格匹配就直接寄了

// 我们在 Home 下进行二级路由,那么就要在 Home.jsx 中进行配置
// NavLink.to && Route.path 中都要带有一级路由 /home
<BrowserRouter>
    <NavLink activeClassName="active" to="/home/message">Message</NavLink><br/>
    <NavLink activeClassName="active" to="/home/price">Price</NavLink>
    {/* 本级别的重定向也要包含上一级路由 */}
    <Redirect to="/home/message"/>
    <Route path="/home/message" component={hMsg}></Route>
    <Route path="/home/price" component={hPrc}></Route>
</BrowserRouter>

路由穿参

// 1. params - 不能直接用模版字符串捏
    // 发送
    <Link to={`url/${this.state.p1}/${this.state.p2}`}/>
    // 接收
    // 1. 首先在 Route 中占位
    <Route path="/url/:p1/:p2" component={Comp}/>
    // 2. 在 Comp 的 props.match.params 中接收
    // const {p1, p2} = this.props.match.params

// 2. query/search
    // 发送 - 不同参数间使用 & 连接
    <Link to={`url/?p1=${this.state.p1}&p2=${this.state.p2}`}/>
    // 接收(不需要声明)
    // pre:先安装,然后 import qs from 'querystring'
        // 1. 初始的 search 是字符串,使用 qs.parse() 转 Obj
        // const {search} = this.props.location
        // const {p1, p2} = qs.parse(search.slice(1)) => 否则带?

// 3. state 参数(不是组件状态)=> 刷新后保持
    // 发送 - 必须把 to 配置为对象
    <Link to={{
        pathname: '/url',
        state: {
            s1: 'val1',
            s2: 'val2'
        }
    }}/>
    // 在 this.props.location.state 中接收(不需要声明)
    // 使用空对象适配清空缓存的情况
    // const {s1,s2} = this.props.location.state || {}

针对历史记录的操作

  • push(默认)
  • replace(在标签中配置)<Link replace to="url"/>

1.2 编程式路由

路由组件显示还是用 <Route> 配置

// replace 模式
replaceShow = (id, title) => {
    // query 参数 - 要用 qs 解析
    this.props.history.replace(`url/?id=${id}&t=${title}`)
    // params 参数 - Route 要声明接收
    this.props.history.replace(`url/${id}/${title}`)
    // state 参数 - 第二个参数是 state 对象
    this.props.history('url', {
        id: 1,
        title: 'tt'
    })
}
// 绑 click 的时候使用匿名函数调用
<button onClick={()=>{this.replaceShow(id,title)}}/>

// push 模式
this.props.history.push('url')

// 操作浏览历史栈
    // 前进 forward
    this.props.history.goFoward()
    // 后退 back
    this.props.history.goBack()
    // go - 看参数:+int 前进, -int 后退
    this.props.history.go(-3)

withRouter

只有「路由组件」持有 this.props.history,那么一般组件呢?

通过 withRouter 进行加工,使其具备路由组件所持有的 API

import {withRouter} from'react-router-dom'
class Header extends Component {
    // 这时候就能用 props.history 下的 API 啦
}
// 注意暴露内容的包裹
export default withRouter(Header)

Browser vs Hash

  • 底层原理不同
    • BrowserRouter 基于 H5 的 history API 实现,不兼容 IE9 以下版本
    • HashRouter 使用 URL 的 hash 值
  • path 表现形式不同
    • BrowserRouter 路径中不存在 #
    • HashRouter 路径包含 #(后面的参数不会发送给服务器)
  • 刷新对 State 参数的影响
    • 对 BrowserRouter 无影响,state 参数存储在 history 对象中
    • 会导致 HashRouter 的 state 参数丢失
  • HashRouter 可以解决样式丢失的问题捏

2 React-Router 6

一级路由

// 基本解构:使用 xxxRouter 包裹整个路由空间
<BrowserRouter>
    // 使用 Link/NavLink 指定跳转 tag
    <NavLink to="/home">Home</NavLink>
    <NavLink to="/about">About</NavLink>
    // 使用 Routes 包裹 route-view,达到 match FIRST
    <Routes>
        // 配置组件的形式变为 element={<Tag/>}
        <Route element={<Home/>}path="/home"/>
        <Route element={<About/>}path="/about"/>
        // 新的重定向规则,将 / 定向至 /home
        // Navigate只要「渲染」就等价于页面跳转,默认为 push 模式
        <Route path="/" element={<Navigate to="/home"/>}/>
    </Routes>
</BrowserRouter>
  • 我们可以配置 <Route caseSensitive> 使得路径严格区分大小写
  • <NavLink> 的动态类名建议使用以下形式对其进行配置:

    默认情况下会加 .active 一样的话就不用改了

    <NavLink 
      className={({isActive})=>{return isActive ? 'base active': 'base'}}
      to="url" children="URLNAME"
    />
    // 可以封装函数进行切换
    function clacClassName(isActive) {
        return isActive ? 'base active': 'base'
    }
    <NavLink 
     className={({isActive})=>this.calcClassName(isActive)}
    />
    

路由表

useRoute 将生成 <Routes> 包裹的全部内容

import {useRoutes} from 'react-router-dom'
// 逐渐 Vue 化
const element = useRoutes([
    {
        path: '/home',
        element: <Home/>
    },
    {
        path: '/about',
        element: <About/>
    },
    {
        path: '/',
        element: <Navigate to="/home"/>
    }
])
// 只要在原来的地方使用 {element} 就行

「坑」

  1. useRoutes() may be used only in the context of a <Router> component.

    最后在 index.js 里用 <BrowserRouter> 把 App 包起来了

  2. useRoutes() 似乎只能在函数式组件里使用

小小的优化

  • 将路由表独立
    // @ router.js
    // import components & Navigate
    const router = [
        { path:'path', element:<Home/>},
        { path:'/'   , element:<Navigate to="/home"/>}
    ];
    export default router;
    
  • 简化 rfc
    import router;
    export default function App {
        const element = userRoutes(router)
        return (
            <Link to="/home" children="Home"/><br/>
            <Link to="/about" children="About"/>
            {elements}
        )
    }
    

嵌套路由

逐渐 Vue 化,我们继续改造路由表

首层路由使用 {element} 进行挂在,二级开始使用 <Outlet> 占位

// @ router.js
const router = [
    {
        path: '/home',
        element: <Home/>,
        children: [
            {
                path: 'news', // 前面不用加斜杠
                element: <News/>
            },
            {
                path: 'message',
                element: <Message/>
            }
        ]
    }
]
// 不用带父级路由&斜杠,用 /news 就会被当成一级路由
<NavLink to="news">News</NavLink>
// 为了指定路由组件呈现的位置,引入 Outlet(就是 router-view 啦)
<Outlet/>
// 默认情况下会给匹配到的「父级」路由也加上 active
// 如果不希望父级高亮,则配置 <NavLink end/>

路由传参

params

// 发送
<Link to=`news/${id}/${title}`>{title}</Link>
// 需要在路由表中注册
{
    path: 'news/:id/:title',
    element: <News/>
}
import {useParams} from 'react-router-dom';
const {id, title} = useParams(); // 然后就能用哩
// 有一个更麻烦的东西
const {id, title} = useMatch('/home/nesw/:id:/:title').params;
<Link to=`news/?id=${id}&t=${title}`>{title}</Link>
// 不需要占位捏
import {useSearchParams} from 'react-router-dom';
const [search, setSearch] = userSearchParams();
// 最后用 search.get('key') 获取对应参数
// 也可以 setSearch('id=00&title=haha') 重置参数

// 更麻烦的东西
const {id, title} = useLocation().search;
 ```

#### state
```js
<Link to="news" state={{
    id: item.id,
    title: item.title
}}>
    {title}
</Link>
// 也不需要占位捏
import {useLocation} from 'react-router-dom';
const {state:{id, title}} = useLocation();

编程式路由

import {useNavigate} from 'react-router-dom';
const navigate = useNavigate();
// 去子路由就不用加斜杠了
function goShop() { navigate('/shop', {
    replace: false,
    // 其他参数只能在路径里拼接
    state: {
        id: item.id,
        title: item.title
    }
});} 
// 前进后退均通过 navigate(n) 来实现

Hooks

  • useInRouterContext 判断是否组件处在 <Router> 的上下文中

    被包裹的所有组件(包括后代组件)均返回 true

    App 要在 index.js 中包裹才判定有效(在组件 tag 外面)

  • useNavigationType 返回当前导航类型

    返回值有:POP / PUSH / REPLACE(POP = 直接打开了该组件 / 刷新页面)

  • useOutlet

    • 没有挂载当前组件中应当渲染的嵌套路由,返回 null
    • 反之,返回展示的嵌套路由对象
  • useResolvedPath 给定 URL,解析 path、search、hash

    该 URL 不一定是本项目中的