最近重新回顾了一下 react-router ,学到了一些以前没有注意到和没有弄明白的问题。

🔴 本文基于 react-router-dom 5.2.06.x 版本存在较大API改动,不完全适用。

React Router 的基本用法

在最开始的时候,最好是先跟着官方的文档或是一些优秀的博客学习基本的用法,推荐下面两篇:

🔖 React Router 中文文档
🔖 阮一峰 React Router

React Router与其他路由组件

相关组件区别
react-router核心功能。包含通用功能和通用 Hooks
react-router-dom基于 react-router 添加了浏览器运行环境的一些组件和功能。
react-router-native适用于 React Native
react-router-reduxReact RouterRedux 的集成。
react-router-config提供可配置化的路由

🥬 注意:
react-routerreact-router-dom 使用的时候不需要都引入,一般会选择使用 react-router-dom

嵌套路由

有时候需要实现一些在一个页面中需要展示其他的页面的情况。比如下面的路由关系:
一个 Music 网站,在 主 页面进行 推荐/排行 页面导航,并且需要保留 Home 的导航栏,也就是说 推荐/排行 嵌入到 Home 主页中。
在这里插入图片描述

文件目录结构参考:
目录结构

新建组件

🔖 Recommendation.js

// src/views/Recommendation/Recommendation.js
import React, { Component } from 'react';
// 内容推荐页面
class Recommendation extends Component {
    render () {
        return ( <div> Recommendation Page </div> );
    }
}
export default Recommendation;

🔖 Ranking.js

// src/views/Ranking/Ranking.js
// Ranking 和 Recommendation 页面很相似
import React, { Component } from 'react';
// 排行页
class Ranking extends Component {
    render () {
        return ( <div> Ranking Page </div> );
    }
}

export default Ranking;

引入组件并创建路由

🔖 Home.js

// src/views/Home/Home.js
import React, { Component, Fragment } from 'react';

import { Layout } from "antd"
import { Route, Link } from 'react-router-dom';

import Recommendation from '../Recommendation/Recommendation';
import Ranking from '../Ranking/Ranking';

import styles from "./index.module.css"

const { Header, Content, Footer } = Layout

class Home extends Component {
    render () {
        return (
            <Fragment>
                <Header className={styles.header}>
                    <Link to="/recommendation">Recommendation </Link>
                    <Link to="/ranking"> Ranking</Link>
                </Header>
                <Content className={styles.content}>
                {/*我们想要把子页面渲染在 Content 中,所以响应的路由就要放在Content中,这样在路由匹配到 /recommendation时,就会先加载父组件Home,在切换的时候也只会替换 Route 的部分,保留了 Home 页的内容 */}
                    <Route path="/recommendation" component={Recommendation} />
                    <Route path="/ranking" component={Ranking} />
                </Content>
                <Footer className={styles.footer}>CopyRight</Footer>
            </Fragment>
        );
    }
}

export default Home;

🔖 App.js

// src/App.js
import React from "react";

import { BrowserRouter, Route } from "react-router-dom"

import Home from './views/Home/Home';

import "./App.css"

function App () {
  return (
    <BrowserRouter>
      <Route path="/" component={Home} />
    </BrowserRouter>
  );
}

export default App;

实现效果

实现的效果就是下面的样子:

在这里插入图片描述

上面的路由全部使用的都是 一级路由,似乎看不出来组件之间的关系,当然我们也可以将上面的子路由改成

<Route path="/home/recommendation" component={Recommendation} />
<Route path="/home/ranking" component={Ranking} />

不过要注意的是:这种写法如果在父路由中开启 exact 匹配,就会导致子组件加载不出来呢。所以建议子路由使用 exact 父路由不要使用。

关于 exact

exactRoute 的精准匹配模式,这种模式下,/ 就不能匹配 /home。此外,使用 exact 会导致一个问题,就是子组件加载不出来。

封装可配置化路由

将路由封装成可配置化可以将所有的路由放到一个文件中,更便于管理(封装的思路也就是上面的实现方式,只不过是换一种方式将路由集中起来而已。)

添加路由配置文件

🔖 router.js

// src/config/router.js

import Home from "../views/Home/Home"
import Recommendation from "../views/Recommendation/Recommendation"
import Ranking from "../views/Ranking/Ranking"

// 将项目中的路由关系配置成数组
/**
 * path 匹配的路径,就是 Route 的 path
 * component 要渲染的组件(这里先采用上面导入方式,后面进行lazy()懒加载优化)
 * children 需要在当前页面渲染的子路由,也是个数组和外层路由结构基本一致
 */
const routerConfig = [{
    path: "/",
    component: Home,
    children: [{
        path: "/recommendation",
        component: Recommendation
    }, {
        path: "/ranking",
        component: Ranking
    }]
}]

export default routerConfig

实现Route函数式渲染

🔖 util.js

// src/util/util.js
import { Route } from "react-router-dom";

// 根据路由配置实现 Route 渲染
// 这里使用了箭头函数, 省去了很多的 return
/** 这里返回的其实就是要渲染的 Route 列表,大概是像这样子(示意)
* [<Route/>, <Route/>...]
* 就是用下面的函数代替了手动写 Route
*/
export const renderRoutes = (routerConfig) =>
    // 将需要用到的属性component,children解构出来,其他直接根据配置渲染到 Route 上
    (routerConfig || []).map(({ component: Component, children, ...routeProps }) =>
        /* render() 是component={} 的替代写法,
        * 这里使用render进行渲染是为了将 当前路由的子路由 children 绑定到 routes 属性,
        * 这样子元素的props终究会出现 routes,也就是当前组建的子路由,再合适的位置进行`渲染`就可以了
        */
        <Route {...routeProps} render={(props) => <Component {...props} routes={children} />} />
    )

修改子路由渲染方式

然后在 App.jsHome.js (需要渲染子路由的组件) 中修改之前的手动渲染为 renderRoutes 函数渲染:

🔖 App.js

// src/App.js
import React from "react";

import { BrowserRouter, Route } from "react-router-dom"

import { renderRoutes } from "./util/util"
import routerConfig from "./config/router"

import "./App.css"

function App () {
  return (
    <BrowserRouter>
      {/* <Route path="/" component={Home} /> */}
      // 修改为函数式渲染
      {renderRoutes(routerConfig)}
    </BrowserRouter>
  );
}

export default App;

🔖 Home.js

// src/views/Home/Home.js
import React, { Component, Fragment } from 'react';

import { Layout } from "antd"
import { Route, Link } from 'react-router-dom';

import Recommendation from '../Recommendation/Recommendation';
import Ranking from '../Ranking/Ranking';

import styles from "./index.module.css"
import { renderRoutes } from '../../util/util';

const { Header, Content, Footer } = Layout

class Home extends Component {
    render () {
        return (
            <Fragment>
                <Header className={styles.header}>
                    <Link to="/home/recommendation">Recommendation </Link>
                    <Link to="/home/ranking"> Ranking</Link>
                </Header>
                <Content className={styles.content}>
                    {/* <Route path="/home/recommendation" component={Recommendation} />
                    <Route path="/home/ranking" component={Ranking} /> */}
                    {renderRoutes(this.props.routes || [])}
                </Content>
                <Footer className={styles.footer}>CopyRight</Footer>
            </Fragment>
        );
    }
}

export default Home;

RecommendationRanking 组件是没有子组件的,不需要路由渲染。
完成上面的改造之后,你会发现实现效果上是一模一样的 😂

优化

组件懒加载

自定义组件懒加载组件
在之前的 router.js 中是通过 import module from 'file' 的方式来引入组件页面的,默认会全部载入所有页面,会对性能有一定的影响,如果实现用到的时候再进行加载组件就能解决这个问题。

import() 默认导入的方式,默认返回 Promise,并且只支持默认的导出。我们需要实现自定义懒加载组件使其需要渲染的时候再加载。

React.lazy()
✅ React Document - React.lazy()

React.lazy()SuspenseReact 官方提供的组件懒加载解决方案。

React.lazy() 是懒加载的一种方式,参数为一个函数,返回 Promise。懒加载只支持默认的导入,如果需要重命名则需要进行中间操作。

Suspense 就是用来解决懒加载带来的等待问题的,在组件没有加载完成之前是没办法渲染的,使用 Suspense 可以等待组件加载完成之后触发重新渲染。

两种方式的原理是相似的。

1. 自定义异步加载组件

异步加载组件的组件

🔖 asyncLoadComponent.js

// src/util/asyncLoadComponent.js
import { Component } from "react"

const asyncLoadComponent = (loadComponent) => class AsyncComponent extends Component {
    constructor(props) {
        super(props)
        this.state = { component: null }
    }
    componentDidMount () {
        loadComponent()
            .then(res => res).then(res => {
                this.setState({ component: res.default || res })
            })
    }
    render () {
        const { component: Component } = this.state
        //注意: 这里一定要把 props 传下去 (里面包含了子路由 routes 信息, 不传递的话会导致子路由无法渲染哦)
        return Component ? <Component {...this.props} /> : <div>loading</div>
    }
}
修改组件加载方式

🔖 router.js

// src/config/router.js
const routerConfig = [{
    path: "/",
    component: asyncLoadComponent(() => import("../views/Home/Home")),
    // exact: true,
    children: [{
        path: "/home/recommendation",
        component: asyncLoadComponent(() => import("../views/Recommendation/Recommendation"))
    }, {
        path: "/home/ranking",
        component: asyncLoadComponent(() => import("../views/Ranking/Ranking"))
    }]
}]

2 使用 React.lazy() 和 Suspense

修改组件加载方式

🔖 router.js

// src/config/router.js
import { lazy } from "react"

const routerConfig = [{
    path: "/",
    component: lazy(() => import("../views/Home/Home")),
    // exact: true, 不能开启exact
    children: [{
        path: "/home/recommendation",
        component: lazy(() => import("../views/Recommendation/Recommendation"))
    }, {
        path: "/home/ranking",
        component: lazy(() => import("../views/Ranking/Ranking"))
    }]
}]

export default routerConfig
添加 Suspense

🔖 App.js

// src/App.js
import React, { Suspense } from "react";

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

import { renderRoutes } from "./util/util"
import routerConfig from "./config/router"

import "./App.css"

function App () {
  return (
  	// fallback 属性是组件加载时显示的内容,是必须的
    <Suspense fallback={<div>Loading</div>}>
      <BrowserRouter>
        {/* <Route path="/" component={Home} /> */}
        {renderRoutes(routerConfig)}
      </BrowserRouter>
    </Suspense>
  );
}

export default App;

实现的效果就是下面的样子:组件加载未完成的时候出现 Loading

在这里插入图片描述

webpack 遇到 import() 时就会进行代码分割,这样就能将不同的组件分别打在不同的文件中,在需要的时候进行加载。

Network 中能看到两个页面是分别加载的:

在这里插入图片描述
当然,目前已经有框架为我们做这些了。像 Next.js 默认支持 约定式路由,不再需要我们配置; Umi.js 支持 约定式路由配置式路由。除了路由这些优秀的框架还提供了代码分割、打包优化等更多的优化内容。

🔖 Next.js
🔖 Umi.js

源代码

Gitee my-music

Logo

基于 Vue 的企业级 UI 组件库和中后台系统解决方案,为数万开发者服务。

更多推荐