import.meta.glob 批量导入、灵活排序与多导出类型适配
在现代 React 项目开发中,路由管理是核心环节之一。随着项目规模扩大,路由配置文件会逐渐增多,手动逐个引入路由文件不仅效率低下,还容易出现遗漏或重复引入的问题。本文将以一段 React Vite 环境下的路由批量导入工具代码为例,从设计背景、实现逻辑、核心优势到实战使用,全方位解析动态路由导入的方案。
在传统的 React 路由配置中,我们通常采用手动引入的方式管理路由,例如:
// 传统手动引入方式
import HomeRoute from './router/modules/home'
import UserRoute from './router/modules/user'
import OrderRoute from './router/modules/order'
const routes: RouteObject[] = [...HomeRoute, ...UserRoute, ...OrderRoute]
这种方式在小型项目中可行,但当项目达到中大型规模(如包含 10+ 业务模块)时,会暴露三个核心问题:
import type { RouteObject } from 'react-router-dom'
export const getAllRouteLists = async (
exportType: 'default' | 'named' = 'default'
): Promise<RouteObject[]> => {
const allRouteLists: (RouteObject & { rank?: number })[] = []
try {
const routeModules = import.meta.glob('../router/modules/**/*.{ts,tsx}', { eager: true })
Object.values(routeModules).forEach((module: any) => {
if (exportType === 'default') {
if (module.default && Array.isArray(module.default)) {
allRouteLists.push(...module.default)
}
} else if (exportType === 'named') {
const routeListKeys = Object.keys(module).filter(key => key.endsWith('RouteList'))
routeListKeys.forEach(key => {
if (Array.isArray(module[key])) {
allRouteLists.push(...module[key])
}
})
}
})
allRouteLists.sort((a, b) => {
const rankA = a.rank ?? Infinity
const rankB = b.rank ?? Infinity
return rankA - rankB
})
} catch (error) {
console.error('批量导入路由失败:', error)
}
return allRouteLists as RouteObject[]
}
代码核心目标是"批量导入指定目录路由文件 → 统一收集路由 → 按优先级排序 → 返回标准路由数组"。
import type { RouteObject } from 'react-router-dom'
export const getAllRouteLists = async (
exportType: 'default' | 'named' = 'default'
): Promise<RouteObject[]> => {
const allRouteLists: (RouteObject & { rank?: number })[] = []
// ...
}
const routeModules = import.meta.glob('../router/modules/**/*.{ts,tsx}', { eager: true })
这是整个函数的"核心入口",依赖 Vite 提供的 import.meta.glob API(Webpack 中对应 require.context),关键参数解析:
Object.values(routeModules).forEach((module: any) => {
if (exportType === 'default') {
// 处理默认导出(如:export default [{ path: '/home', element: <Home /> }])
if (module.default && Array.isArray(module.default)) {
allRouteLists.push(...module.default)
}
} else if (exportType === 'named') {
// 处理命名导出(如:export const HomeRouteList = [{ path: '/home', element: <Home /> }])
const routeListKeys = Object.keys(module).filter(key => key.endsWith('RouteList'))
routeListKeys.forEach(key => {
if (Array.isArray(module[key])) {
allRouteLists.push(...module[key])
}
})
}
})
这部分解决了"多导出类型适配"的问题:
allRouteLists.sort((a, b) => {
const rankA = a.rank ?? Infinity
const rankB = b.rank ?? Infinity
return rankA - rankB
})
try {
// 动态导入、路由提取、排序逻辑
} catch (error) {
console.error('批量导入路由失败:', error)
}
return allRouteLists as RouteObject[]
| 优势 | 说明 |
|---|---|
| 效率提升 | 新增路由模块时,只需在 src/router/modules 目录下创建文件,无需修改工具函数或入口文件;删除模块时,直接删除文件即可,工具函数会自动忽略已删除的文件,避免"死代码"残留。 |
| 灵活性提升 | 同时兼容"默认导出"和"命名导出",满足不同开发习惯;通过 rank 字段可灵活控制路由优先级,无需手动调整数组顺序,尤其适合多模块协作场景(各模块开发者只需定义自己的 rank)。 |
| 可维护性提升 | 强制路由文件存放路径(src/router/modules)和命名规范(命名导出需以 RouteList 结尾),避免团队成员随意存放文件导致混乱;工具函数集中处理导入逻辑,后续若需修改规则(如新增支持 .js 文件),只需修改一处即可。 |
| 稳定性提升 | try/catch 确保单个路由文件错误不影响全局,同时打印详细错误信息,便于定位问题;TypeScript 类型约束避免传入无效参数、导出不符合规范的路由结构,减少运行时错误。 |
要将 getAllRouteLists 应用到项目中,需按"路由文件配置 → 工具函数调用 → 路由渲染"三步操作。
首先在 src/router/modules 目录下创建路由文件,支持两种导出方式。
方式 1:默认导出(推荐,适配工具函数默认参数)——创建 src/router/modules/home.ts:
import type { RouteObject } from 'react-router-dom'
import Home from '../../pages/Home'
// 定义 rank: 1(优先级高,排在前面)
const HomeRoutes: (RouteObject & { rank?: number })[] = [
{
path: '/',
element: <Home />,
rank: 1 // 首页优先级最高
}
]
export default HomeRoutes
方式 2:命名导出(需以 RouteList 结尾)——创建 src/router/modules/user.ts:
import type { RouteObject } from 'react-router-dom'
import User from '../../pages/User'
export const UserRouteList: (RouteObject & { rank?: number })[] = [
{
path: '/user',
element: <User />,
rank: 2 // 优先级低于首页
}
]
在路由入口文件(如 src/router/index.tsx)中调用 getAllRouteLists,获取排序后的路由数组:
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { getAllRouteLists } from '../utils/routeHelper'
// 由于 getAllRouteLists 是异步函数,需用 React 18 的 Suspense 包裹
import { Suspense, lazy } from 'react'
// 异步创建路由
const createRouter = async () => {
// 1. 调用工具函数:默认导出类型(可省略 exportType 参数)
const routes = await getAllRouteLists()
// 2. 若需获取命名导出的路由,需指定 exportType: 'named'
// const namedRoutes = await getAllRouteLists('named')
// 3. 合并路由(若同时有默认导出和命名导出)
// const allRoutes = [...routes, ...namedRoutes]
// 创建路由实例
return createBrowserRouter(routes)
}
// 懒加载路由创建函数
const LazyRouter = lazy(() =>
createRouter().then(router => ({ default: () => <RouterProvider router={router} /> }))
)
// 导出路由组件,用 Suspense 处理加载状态
export default function AppRouter() {
return (
<Suspense fallback={<div>Loading routes...</div>}>
<LazyRouter />
</Suspense>
)
}
在 src/main.tsx 中引入并渲染路由组件:
import React from 'react'
import ReactDOM from 'react-dom/client'
import AppRouter from './router'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<AppRouter />
</React.StrictMode>
)
若需排除 modules 目录下的某个文件(如 test.ts),可修改 import.meta.glob 的匹配规则,添加排除条件:
// 排除文件名包含 test 的文件
const routeModules = import.meta.glob('../router/modules/**/!(*test*).{ts,tsx}', { eager: true })
若需按业务模块拆分路由(如"公共路由""用户路由"),可在 modules 下创建子目录,工具函数会自动匹配子目录文件:
src/router/modules/
├── public/ # 公共路由(首页、关于我们)
│ ├── home.ts
│ └── about.ts
└── user/ # 用户路由(个人中心、设置)
├── profile.ts
└── setting.ts
无需修改工具函数,../router/modules/**/*.{ts,tsx} 会自动匹配所有子目录下的文件。
若需实现路由懒加载(减少首屏体积),可在路由配置文件中直接使用 React.lazy,工具函数会自动保留懒加载配置:
// src/router/modules/home.ts
import { lazy } from 'react'
import type { RouteObject } from 'react-router-dom'
// 懒加载 Home 组件
const Home = lazy(() => import('../../pages/Home'))
const HomeRoutes = [
{
path: '/',
element: <Home />,
rank: 1
}
]
export default HomeRoutes