React 学习系列(七): react router 深入学习

2,318 阅读14分钟

概述

使用 react 构建 单页面应用 时,离不开 react-router 的配合。通过 react-router, 我们可以建立 路由和组件(页面)之间的映射关系。当 切换路由 时,将匹配的组件(页面) 渲染到对应的位置。

官网 提供了 react-router 的 两个版本:react-router-domreact-router-native。其中,react-router-dom 应用于 web 应用react-router-native 应用于 原生应用即 react-native app

本文主要梳理 react-router-dom 的具体应用,下文中讲到的所有 react-router,实际为 react-router-dom

使用 react-router

react-router 的使用说明,官网上已经提供了大量的示例, 这里就不再做一一介绍了。如果想了解 react-router 的使用, 可以去 官网 了解。

工作原理

HTML5 中引入了 window.history.pushStatewindow.history.replaceState 方法, 它们可以分别 添加和修改浏览器的历史记录不需要重新加载页面

pushStatereplaceState 方法需配合 window.onpopstate 使用。我们可以通过 浏览器的前进、回退按钮 或者 window.history.backwindow.history.gowindow.history.forward 方法, 激活浏览器的某个历史记录。 如果 激活的历史记录 是通过 pushState方法添加 或者 被replaceState方法修改,会触发注册的 popstate 事件。

pushState、replaceState 方法不会触发 popstate 事件。

如果浏览器不支持 pushStatereplaceState 方法, 我们也可以通过 window.location.hash = 'xxx' 或者 window.location.replace 的方式 添加和修改浏览器的历史记录, 而 不需要重新加载页面

window.location.hashwindow.location.replace 需配合 window.onhashchange 使用。只要 激活的历史记录hash 值和 当前 url 的 hash 值 不同,就会触发注册的 onhashchange 事件。

使用 window.location.replace 时, 如果只修改 hash,不会重新刷新页面

只要修改 hash,就会触发 hashchange 事件

使用 react-router 进行 单页面应用页面跳转 是基于 上述原理 实现的。

当需要 跳转页面 时,通过 pushState(replaceState、window.location.hash、 window.location.replace) 方法 添加或修改历史记录, 然后 重新渲染页面。 当通过 浏览器的前进、回退按钮 或者 gobackforward 等方法 激活某个历史记录时, 触发注册的 popstate(hashchange) 事件, 然后 重新渲染页面

使用 react-router 进行 单页面跳转 的基本流程如下:

  1. 启动 react 应用

    每一个 react 应用 的启动都是以 ReactDOM.render 方法执行开始的。

  2. 渲染 BrowserRouter(HashRouter) 组件

    react router 应用 中,我们一般使用 BrowserRouter 或者 HashRouterBrowserRouter(HashRouter) 是一个 class 组件,在渲染阶段会构建 组件实例并初始化,然后执行 render 方法。

    BrowserRouter(HashRotuer) 组件实例初始化 过程中,会创建一个 history 对象。这个 history 对象 用于 页面跳转,提供 pushreplacegobackforward 等方法供外部使用,在整个渲染过程中, 会通过 props 传递给 子组件 - 页面组件 供其使用。

  3. 渲染 Router 组件

    BrowserRouter(HashRouter) 组件的 render 方法 执行完毕以后, 就会开始渲染 Router 组件

    Router 组件也是一个 class 组件,在渲染阶段会 构建组件实例并初始化, 然后执行 render 方法。

    Router 组件实例初始化 过程中, 会定义一个 state 对象state 对象 中有一个 location 属性,属性值是一个 对象, 用于存储 应用当前的 url 信息。 另外, 还会通过 父组件 - BrowserRouter(HashRouter) 传递的 history 对象,给 window 对象 注册 popstate(hashchange) 事件。

  4. 渲染 Route 组件

    Router 组件的 render 方法 执行完毕以后, 就会开始渲染 Route 组件

    在渲染过程中, 会使用 Route 组件的 path 属性父组件 - Router 传递的 location 信息做对比。 如果能匹配, 那么 Route 组件 对应的 页面组件 就可以被渲染,否则不渲染。

  5. react 应用启动完成

  6. 页面组件触发页面跳转

    页面组件 通过 push 或者 replace 的方式触发 页面跳转 时, 会将 新的页面对应的 url 添加到浏览器的历史记录(或者替换历史记录),然后触发 Router 组件实例 执行 setState 方法来 更新 location

    Router 组件实例 执行 setState 方法时, 会触发 Router 组件 更新, 导致 子组件 - Route 组件 也更新,对应的 页面组件 也更新,从而实现 页面跳转

  7. 前进或者后退激活历史页面

    此时, 会触发 window 对象注册的 popstate(hashchange) 事件。 在 callback 中, 会获取 激活页面的 url 信息,然后触发 Router 组件实例 执行 setState 方法来 更新 location

    Router 组件实例 执行 setState 方法时, 会触发 Router 组件 更新, 导致 子组件 - Route 组件 也更新,对应的 页面组件 也更新,从而实现 页面跳转

整个流程图大致如下:

hash & history

vue-router 一样, react-router 也有两种模式: hash 模式history 模式

history 模式, 即使用 BrowserRouter, 页面 url 的格式为 protocol://hostname: port/xxx/xxx, 更加美观。切换页面 的时候,会修改 window.location.pathname

history 模式, 是基于 原生 history - pushState、replaceState、popstate 实现的。 当我们通过 push(replace) 的方式进行页面跳转时,会使用 pushState( replaceState)浏览器中新增(替换)历史记录, 然后 渲染对应的页面。 当我们通过 页面导航(前进、后退) 或者 go(back、forward) 的方式 访问浏览器的历史记录 时,触发 popstate 事件,然后 渲染对应的页面

使用 history 模式 时, 如果 浏览器不支持 pushState(replaceState),会采用 window.location.assign (window.location.replace) 的方式,刷新页面进行页面跳转

hash 模式,即使用 HashRouter,页面 url 的格式为: protocol://hostname: port/xxx/xxx/#/xxx切换页面 的时候,只修改 window.location.hash

hash 模式,是基于 修改 window.location.hashhashchange 实现的。当我们通过 push 方式 进行 页面跳转 时,会 直接修改 window.location.hash在浏览器中新增历史记录,然后渲染页面;当我们通过 replace 方式 进行 页面跳转 时,会先构建一个 修改 hash 以后的临时 url,然后使用这个 临时 url 通过 window.location.replace 的方式 替换当前 url在浏览器中替换历史记录,然后渲染页面。当我们通过 页面导航(前进、后退) 或者 go(back、forward) 的方式 访问浏览器的历史记录 时,触发 hashchange 事件,然后 渲染对应的页面

注意,使用 history 模式 需要 服务端支持,要在服务端增加一个 覆盖所有情况的候选资源: 如果 URL 匹配不到任何静态资源,则应该返回 同一个 index.html 页面,这个页面就是 应用依赖的页面,否则会出现 404 异常

一般情况下, 使用 BrowserRouter。如果浏览器不支持 pushState,可以使用 HashRouter。

Route

渲染阶段Route 组件 会使用从 父组件 - Router 传递的 location 对象(即当前页面的url信息)path 配置项 做比较。 只有 Route 组件匹配当前 url,对应的 页面组件 才会被渲染。

没有设置 path 的 Route 组件 匹配任何 url。

常见的使用 Route 的方式有如下几种:

  • component

    Route 组件 添加 component 属性, 如下:

    <Route path='/componentA' component={ComponentA} />
    

    使用上述方式时, Route 组件 在渲染过程中会将 父组件 - Router 传递的 history 对象location 对象 以及 匹配过程生成的对象 通过 props 传递给 页面组件。 这样在 页面组件 中即可通过 props 访问 historylocation

  • render

    Route 组件 添加 render 属性, 如下:

    <Route path="/componentA" render={props => <ComponentA {...props} {...others} />} /> 
    

    component 方式 一样, Route 组件 在渲染过程中会将 父组件 - Router 传递的 history 对象location 对象 以及 匹配过程生成的对象 通过 props 传递给 页面组件。 这样在 页面组件 中即可通过 props 访问 historylocation

  • children(函数类型)

    Router 组件 添加 函数类型的子元素, 如下:

    <Route path="/componentC"> 
        {(props) => <ComponentC {...props} ...{others}/>}
    </Route>
    

    component 方式render 方式 一样, Route 组件 在渲染过程中会将 父组件 - Router 传递的 history 对象location 对象 以及 匹配过程生成的对象 通过 props 传递给 页面组件。 这样在 页面组件 中即可通过 props 访问 historylocation

  • children(非函数类型)

    Router 组件 添加 非函数类型的子元素, 如下:

    <Route path="/componentA"> 
        <ComponentA/>
    </.Route>
    

    component、render 方式 不同, Route 组件 在渲染过程中 无法 会将 父组件 - Router 传递的 history 对象location 对象 以及 匹配过程生成的对象 - match 通过 props 传递给 页面组件。 因此在 页面组件无法 访问 historylocation

上述四种情况,renderchildren(函数类型) 更加灵活些, 除了可以将 history 对象location 对象match 对象 传递给 页面组件 外, 还可以传递 自定义属性,这点在使用 静态路由配置 出现 嵌套路由 的时候 非常关键

注意,只要 Route 组件的 path 属性匹配当前页面url,对应的页面组件就会渲染

使用 Route 组件 时, 有几个 关键属性, 需要我们了解一下, 如下:

  • path: string | [string]

    必要属性, 用于匹配 location.pathname。如果匹配,对应的 Route 组件页面组件 才会渲染。

  • exact ?: boolean

    匹配时是否需要精确匹配,非必要属性,默认为 false,即不需要精确匹配

    如果 指定 exact 属性 或者 exact = true,则需要 精确匹配,即 path 属性指定的路径必须和 location.path 完全相同才能匹配

    注意,如果使用嵌套路由,父页面对应的 Route 组件不要设置精确匹配,否则嵌套路由失效

  • strict ?: boolean

    匹配时是否需要严格匹配,非必要属性,默认为 false,即不需要严格匹配

    如果 指定 strict 属性 或者 strict = true,则需要 严格匹配。此时 带 '/' 的 path 只能匹配 带'/'的 location.pathname

    如:

    path location.pathname match ?
    /one /one y
    /one /one/ n
    /one /one/two n
    /one/ /one/ y
    /one/ /one/two y
    /one/ /one n

    注意,如果使用嵌套路由,父页面对应的 Route 组件 path 属性值为 '/xxxx' 格式时,不要设置严格匹配,否则嵌套路由失效

  • sensitive ?: boolean

    匹配时是否忽略大小写,非必要属性,默认为 false,即忽略

    如果 指定 sensitive 属性 或者 sensitive = true,则匹配时不会忽略 大小写

Switch

使用 Route 组件 时,只要 path 属性 匹配 当前页面 url 的 pathname,那么对应的 Route 组件页面组件 都会渲染。

如果我们使用 Switch 组件 包裹 Route 组件(或者 Redirect 组件),那么只会渲染 第一个匹配 location.pathname 的 Route 组件(或者 Redirect 组件)

具体过程为: Switch 组件 在渲染完成以后开始渲染 children(Routes 和 Redirects)。在渲染 children 的过程中,会 遍历 children,然后使用每一个 childlocation.pathname 做匹配。如果匹配,则立即 停止遍历,返回 匹配的 child

编程式导航

除了使用 Link 组件 创建 a 标签来定义导航链接,我们还可以借助 Router 组件 传递给页面组件的 history 对象 方法,通过编写代码来实现。

当我们通过 componentrender函数式子组件 的方式使用 Route 组件 时, Route 组件 会在渲染过程中会将 父组件 - Router 传递的 history 对象location 对象 以及 匹配过程生成的对象 - match 通过 props 传递给 页面组件。在 页面组件 中,通过 props.history 即可实现 编程式导航

history 是基于 window.history 实现的,提供了以下方法供我们使用:

  • push - push 方式 跳转页面;

  • replace - replace 方式 跳转页面;

  • go - 前进或者回退 n 个页面;

  • goBack - 后退一个页面;

  • goForward - 前进 n 个页面;

我们还可以在 展示组件 中通过 props.location 或者 prop.history.location 访问 当前页面的 url 信息

location 对象 的属性如下:

  • location.pathname - 当前 url 的路径

  • location.hash - 当前 url 的 hash 值;

  • location.search - 当前 url 的 查询部分

  • location.state - 用户传递的 自定义信息

Route 组件 匹配成功以后,会生成一个 匹配对象 - match页面组件 使用,主要属性如下:

  • url - 页面跳转 传递的 pathname

  • path - Route 组件path 配置项

  • params - 动态路径参数

    <Link to='/user/123' />
    
    <Route path='/user/:id' component={User} />
    
    // params - 动态路径参数
    {
        id: 123
    }
    

hooks

当我们通过 componentrender函数式子组件 的方式使用 Route 组件 时, Route 组件 会在渲染过程中会将 父组件 - Router 传递的 history 对象location 对象 以及 匹配过程生成的对象 - match 通过 props 传递给 页面组件 供其使用。

如果 页面组件类组件,必须使用上面的方式才能在 页面组件 中使用 historylocationmatch,但如果是 函数式组件,可以使用 react router 提供的 hooks 实现同样的功能。

react router 提供了一些 hooks 供我们使用,如下:

  • useHistory

    获取 Router 组件 构建的 history 对象

  • useLocation

    获取 当前页面对应的 url 信息,如 pathnamehashsearchstate

  • useParams

    获取 动态路径参数

  • useRoutematch

    获取 路由匹配对象 - match

    使用时如果 指定 path, 返回 location.pathname指定 path 的匹配结果;如果 未指定 path, 返回 Route 组件 做匹配操作时生成的 match 对象

hooks 的用法可以去官网了解:官网 - Hooks

嵌套路由

使用 react touter 时,我们也可以实现 嵌套路由,具体事例参照: 官网 - nesting.

嵌套路由,就是在 页面组件使用 Route 组件。当我们切换到一个 子路由 时,先渲染 匹配当前 location.pathname 的 父 Route 组件及页面组件, 等到 父页面组件渲染完成 以后,再渲染 匹配当前 location.pathname 的子 Route 组件及页面组件

不管是从 父路由切换到子路由 还是 从子路由切换到父路由父 Route 组件及页面组件都会重新渲染(会重新执行render方法),但 对应的 dom 节点不会更新。 即 只要切换到子路由,父 Route 组件及页面组件都会重新渲染

使用嵌套路由时,父 Route 组件不要设置精确匹配、严格匹配,否则会导致嵌套路由失效

路由懒加载

在实际的 react 应用中, 是离不开 路由懒加载 的。

路由懒加载 是基于 webpack 等打包工具实现。动态加载页面 时, 会先构建一个 promise 对象。 在 promise 对象 初始化的时候, 通过 动态构建 script 元素 的方法, 从服务端获取懒加载页面对应的文件懒加载页面文件加载并执行完毕 以后, 会将 promise 对象 的状态置为 resolved, 然后触发相应的 onFullfilled。 在 onFullfilled 中, 渲染加载完成的页面

整个流程大致如下:

  1. 初次渲染 的时候,遇到 懒加载页面, 构建相应的 promise 对象, 从服务端获取懒加载模块对应的文件。

    此时, promise 对象 的状态为 pending

  2. 完成初次渲染

    初次渲染时,将 懒加载页面 对应的 loading 组件 渲染为 dom 节点

  3. 懒加载页面 加载完成以后, 相应的 promise 对象 的状态变为 resolved,触发注册的 onFullfilled

    onFullfilled 中, 触发 react 更新

  4. 等所有的懒加载模块加载完成以后, 开始 react 更新

    此时, 所有懒加载组件都已经获取完毕。经过 react renderreact commit 阶段以后, 页面组件 渲染为实际的 dom 节点

路由懒加载 的方式:

  • React.lazy + React.Suspense
// lazyLoad.js
import {lazy, Suspense} from 'react'
export default function (loader, Loading) {
    const LazyComponent = lazy(loader)
    return function (props) {
        return <Suspense fallback={<Loading />}>
            <LazyComponent />
        </Suspense>
    }
}


// 实际使用
const componentA = lazyLoad(() => import(/* webpackChunkName: 'component-1' */'./component-1'), Loading)

  • 使用 react-loadable
import React from 'react';
import Loadable from 'react-loadable';

export default Loadable({
    loader: () = >import(/* webpackChunkName: 'component-1' */'./component-1'),
    loading: Loading
});

静态路由配置

在实际的 react 应用 中, 我们一般会建立一个 独立的静态路由配置 - routeConfig,然后在应用中 根据这个路由配置动态创建 Route 组件。用法如下:

// routeConfig.js
const routes = [{
    path: '/one',
    conponent: 'One',
    routes: [{
        path: '/two',
        component: 'Two'
    }, {
        path: '/three',
        component: 'Three'
    }]
},
...
]


// 实际应用
<BrowserRouter>
    <Switch>
        {routes.map((route, i) => (
            <Route path={route.path}  render={
                props => <route.component {...props} routes={route.routes}
            } />
        ))}
    </Switch>
</BrowserRouter>


// One
function One(props) {
    let child
    if (props.routes && props.routes.length) {
        child = <Switch>
            {routes.map((route, i) => (
                <Route path={route.path}  render={
                    props => <route.component {...props} routes={route.routes}
                } />
            ))}
        </Switch>
    }
    return (
        <div>
            ...
            {child}
        </div>
    )
}

react router 提供了一个 react-router-config 来帮助我们使用 静态路由配置, 具体如下:

import { renderRoutes } from "react-router-config";

<BrowserRouter>
    {renderRoutes(routes)}
</BrowserRouter>

renderRoutes 方法的实现过程如下:

import React from "react";
import { Switch, Route } from "react-router";

function renderRoutes(routes, extraProps = {}, switchProps = {}) {
  return routes ? (
    <Switch {...switchProps}>
      {routes.map((route, i) => (
        <Route
          key={route.key || i}
          path={route.path}
          exact={route.exact}
          strict={route.strict}
          render={props =>
            route.render ? (
              route.render({ ...props, ...extraProps, route: route })
            ) : (
              <route.component {...props} {...extraProps} route={route} />
            )
          }
        />
      ))}
    </Switch>
  ) : null;
}

export default renderRoutes;

对比 vue-router

vue routerreact router 的差异比较,详见: react router 和 vue router 的异同

未完待续...