# 按需加载路由

# 官方的懒加载

React.lazy (opens new window)

# 个人实现

下面自己实现的异步按需加载组件的方式,之前没找到官方的懒加载。 建议大家还是使用官方的,下面的当做学习,提供思路。

# 第一步:创建一个异步组件

//创建文件src/common/AsyncComponent.js

import React, { Component } from "react";
export default function asyncComponent(importComponent) {
	class AsyncComponent extends Component {
		constructor(props) {
			super(props);
			this.state = {
				component: null
			};
		}

		async componentDidMount() {
			const { default: component } = await importComponent();
			this.setState({
				component: component
			});
		}

		render() {
			const C = this.state.component;
			return C ? <C {...this.props} /> : null;
		}
	}
	
	return AsyncComponent;

}

# 第二步:使用异步组件:我们将使用asyncComponent动态导入我们想要的组件。

例如:

const AsyncHome = asyncComponent(() => import("./containers/Home"));

下面来个项目中的路由例子

// route.ts
import { RouteProps } from 'react-router-dom'
import asyncComponent from '@/common/js/async-component' // 创建异步组件,实现路由按需加载
import App from '@/app'
import PermissionRoute from '@/components/permission-route/permission-route'
import { traverseTree } from '@/utils/utils'
import IndexPage from '@/pages/index'

// 大厅
const Market = asyncComponent(() => import('@/pages/market/market'))
// 我是买家
const Buyer = asyncComponent(() => import('@/pages/buyer/buyer'))
// 我是卖家
const Seller = asyncComponent(() => import('@/pages/seller/seller'))
// 核户申请页面
const CheckAccount = asyncComponent(() => import('@/pages/check-account/check-account'))
// 订单详情页
const OrderDetail = asyncComponent(() => import('@/pages/order-detail/order-detail'))
// 测试页面
const Test = asyncComponent(() => import('@/pages/test/test'))
// 图标预览页面
const TestIcon = asyncComponent(() => import('@/pages/test-icon/test-icon'))
// 发布票据页面
const IssueDraft = asyncComponent(() => import('@/pages/issue-draft/issue-draft'))

export interface MenuRoute {

  /** 标题 */
  title?: string,

  /** 是否要求登录 */
  requireLogin?: boolean,

  /** 是否不在菜单中显示 */
  hideInMenu?: boolean,

  /** 路由参数 */
  routeProps: RouteProps,

  /** 子菜单 */
  children?: MenuRoute[],

  /** 菜单图标 */
  icon?: string,

  /** 父级菜单对象 */
  parent?: MenuRoute | null,

  /** 完整路径 */
  fullPath?: string,
}

export const routes: MenuRoute[] = [
  {
    routeProps: {
      path: '/',
      element: <App />,
    },
    children: [
      {
        routeProps: {
          index: true,
          element: <IndexPage />
        },
        title: '首页',
        hideInMenu: true
      },
      {
        routeProps: {
          path: 'market',
          element: <Market />
        },
        title: '大厅',
      },
      {
        routeProps: {
          path: 'buyer',
          element: <Buyer />
        },
        requireLogin: true,
        title: '我是买家',
      },
      {
        routeProps: {
          path: 'seller',
          element: <Seller />
        },
        requireLogin: true,
        title: '我是卖家',
      },
      {
        routeProps: {
          path: 'issue',
          element: <IssueDraft />
        },
        requireLogin: true,
        title: '发布票据',
      },
      {
        routeProps: {
          path: 'checkAccount',
          element: <CheckAccount />
        },
        requireLogin: true,
        title: '核户申请',
      },
      {
        routeProps: {
          path: '*',
          element: <IndexPage />
        },
        hideInMenu: true
      },
      {
        routeProps: {
          path: 'orderDetail',
          element: <OrderDetail />
        },
        requireLogin: true,
      },
    ]
  }
]

if (process.env.NODE_ENV === 'development') {
  routes[0].children?.push(...[
    {
      routeProps: {
        path: 'test',
        element: <Test />
      },
      title: '测试页面',
      hideInMenu: true
    },
    {
      routeProps: {
        path: 'test-icon',
        element: <TestIcon />
      },
      title: '图标预览页面',
      hideInMenu: true
    }
  ])
}

// 打平的路由数组
export const flattenRoutes: MenuRoute[] = []
// 完整路径-路由对象 map
const routesMap: Record<string, MenuRoute> = {}
// 递归路由,添加页面权限控制和父节点、完整路径
traverseTree(routes, (route, parent) => {
  if (route.requireLogin && route.routeProps.element) {
    route.routeProps.element = <PermissionRoute route={route}>{route.routeProps.element}</PermissionRoute>
  }
  route.parent = parent
  // 完整路径
  let fullPath: string | undefined
  if (parent) {
    fullPath = route.routeProps.index ? parent.fullPath : `${(parent.fullPath || '').replace(/\/$/, '')}/${route.routeProps.path || ''}`
  } else {
    fullPath = route.routeProps.path
  }
  route.fullPath = fullPath
  flattenRoutes.push(route)
  if (fullPath) {
    routesMap[fullPath] = route
  }
})

// 获取某个路由对象
export const getRoute: (fullPath: string) => MenuRoute | null = function getRoute(fullPath) {
  return routesMap[fullPath] || null
}

export default routes

// index.ts
import React from 'react'
import ReactDOM from 'react-dom/client'
import {
  BrowserRouter,
  Routes,
  Route,
} from 'react-router-dom'
import { ConfigProvider } from 'antd'
import { Provider } from 'react-redux'
import { store } from './store/index'

// ant design 样式重置
import 'antd/dist/antd.less'
import './styles/antd-reset.scss'
// 页面重置样式
import './styles/reset.scss'
// 全局样式
import './styles/global.scss'
// 由于 antd 组件的默认文案是英文,所以需要修改为中文
import zhCN from 'antd/es/locale/zh_CN'
import 'moment/locale/zh-cn'

import { routes, MenuRoute } from '@/router'

import '@/utils/icon' // 引入项目里本地图标

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
)

// 渲染路由组件
const renderRoute = (route: MenuRoute) => {
  const children = route.children?.map(childMenu => renderRoute(childMenu))
  return <Route key={route.fullPath || '/'} {...route.routeProps}>{children}</Route>
}

root.render(
  <ConfigProvider locale={zhCN} autoInsertSpaceInButton={false}>
    <React.StrictMode>
      <Provider store={store}>
        <BrowserRouter>
          <Routes>
            {
              routes.map(renderRoute)
            }
          </Routes>
        </BrowserRouter>
      </Provider>
    </React.StrictMode>
  </ConfigProvider>

)

参考文章:

react-router的3种路由按需加载介绍 (opens new window)

更新时间: 2022年9月2日星期五下午2点43分