代码部分
约定参考的umi.js,稍微修改了下。
用法(webpack和vite写法不同)
import generateRoutes from '@/generateRoutes'
import { renderRoutes } from 'react-router-config'
// ==============webpack=============
const pages = {}
function importAll(r: any) {
r.keys().forEach((key: string) => (pages[key] = () => r(key)))
}
const r = require.context('./pages', true, /\.tsx$/, 'lazy')
importAll(r)
const routes = generateRoutes(pages, 1)
console.log('pages', routes, pages)
export default () => renderRoutes(routes)
// =============vite=================
import generateRoutes from '@/generateRoutes'
import { renderRoutes } from 'react-router-config'
const pages = import.meta.glob('../src/pages/**/*.tsx')
const routes = generateRoutes(pages, 3)
console.log('pages', routes, pages)
export default () => renderRoutes(routes)
核心
import React, { Suspense } from 'react'
import { Spin, Icon } from 'antd'
/** utils 未来如果需要把routes生成json文件的时候使用
* 序列化 JSON.stringify(routes, replacer)
* 反序列化 JSON.parse(routes, reviver)
*/
const replacer = (k: string, v: any) => (typeof v === 'function' ? '{' + v.toString() + '}' : v)
// eslint-disable-next-line no-eval
const reviver = (k: string, v: string) => (typeof v === 'string' && v.startsWith('{') ? eval(v.slice(1, -1)) : v)
const generateRoutes = (pages: Record<string, any>, sliceNumber: number) => {
const filterRe = /\/(components|constants|utils|util|\.|_)/
const routesConfig: any[] = []
const page2Route = (path: string) => {
// 注意!!! 这里的pages是未经过排序的。
const page = pages[path]
const pathArray = path.split('/').splice(sliceNumber)
const endFile = pathArray[pathArray.length - 1]
const endFilename = endFile.replace(/\.(tsx)/, '').replace(/\$/, '?')
// filter file
if (!page || filterRe.test(path)) {
// console.log('不生成路由的文件', path)
return
}
console.log('生成路由的文件', pathArray, path)
const pathReducer = (routes: any, key: any, index: number) => {
const nestRe = /index/
// 当前层级嵌套路由前缀
const pathPrefix = '/' + pathArray.slice(0, index).join('/') + (index === 0 ? '' : '/')
// 当前层级嵌套路由完整路径
const nestFullPath = path.split('/').slice(0, sliceNumber).join('/') + pathPrefix + 'index.tsx'
const nestFile = pages[nestFullPath]
// 存在对应的嵌套路由
const hasNestFile = !!nestFile
// 当前层级嵌套路由对象不一定存在,保证他一定存在。
const nestRoute = routes?.find?.((route: any) => !route.exact && route.path === pathPrefix)
const LazyPage = React.lazy(page)
const component = (props: any) => {
const antIcon = <Icon type="loading" style={{ fontSize: 24 }} spin />;
const Loading = (
<div style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: '100%',
height: 400
}}>
<Spin indicator={antIcon} />
</div>
)
return (
<Suspense fallback={Loading}>
<LazyPage {...props} />
</Suspense>
)
}
if (endFile === key) {
// index文件
if (nestRe.test(endFile)) {
// 当前层级的嵌套路由已经被创建过,直接赋值component,未被创建则新增
if (nestRoute) {
// 对应的嵌套路由已经被创建,直接加入component即可。
nestRoute.component = component
} else {
routes.push({
path: pathPrefix,
component: component,
routes: [],
})
}
} else {
// 当前非嵌套路由文件
if (hasNestFile) {
// 嵌套路由被创建过就放进routes里面,未被创建则新建动态路由并且放入routes
if (nestRoute) {
nestRoute.routes.push({
path: pathPrefix + endFilename,
component: component,
exact: true,
})
} else {
// 因为读取的模块是无序的,所以可能嵌套层还没创建好
routes.push({
path: pathPrefix,
routes: [
{
path: pathPrefix + endFilename,
component: component,
},
],
})
}
} else {
// 不存在同级嵌套路由则直接新增路由
routes.push({
path: pathPrefix + endFilename,
component: component,
exact: true,
})
}
}
} else {
// 查找下一层routes
if (hasNestFile) {
if (nestRoute) {
return nestRoute.routes
} else {
const nextRoute: any = []
routes.push({
path: pathPrefix,
routes: nextRoute,
})
return nextRoute
}
}
return routes
}
}
pathArray.reduce(pathReducer, routesConfig)
}
Object.keys(pages).forEach(page2Route)
return routesConfig
}
export default generateRoutes
约定式路由
除配置式路由外,本项目也支持约定式路由。约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过目录和文件及其命名分析出路由配置。
如果没有 routes 配置,本项目会进入约定式路由模式,然后分析 src/pages 目录拿到路由配置。
比如以下文件结构:
.
└── pages
├── index.tsx
└── users.tsx
会得到以下路由配置,
[
{ exact: true, path: '/', component: '@/pages/index' },
{ exact: true, path: '/users', component: '@/pages/users' },
]
需要注意的是,满足以下任意规则的文件不会被注册为路由,
以 . 或 _ 开头的文件或目录 以 d.ts 结尾的类型定义文件 以 test.ts、spec.ts、e2e.ts 结尾的测试文件(适用于 .js、.jsx 和 .tsx 文件) components 和 component 目录 utils 和 util 目录 不是 .js、.jsx、.ts 或 .tsx 文件 文件内容不包含 JSX 元素 动态路由 约定 ':' 前缀的文件或文件夹为动态路由。
比如:
src/pages/users/:id.tsx 会成为 /users/:id src/pages/users/:id/settings.tsx 会成为 /users/:id/settings 举个完整的例子,比如以下文件结构,
.
└── pages
└── :post
├── index.tsx
└── comments.tsx
└── users
└── :id.tsx
└── index.tsx
会生成路由配置,
[
{ exact: true, path: '/', component: '@/pages/index' },
{ exact: true, path: '/users/:id', component: '@/pages/users/[id]' },
{ exact: true, path: '/:post/', component: '@/pages/[post]/index' },
{
exact: true,
path: '/:post/comments',
component: '@/pages/[post]/comments',
},
];
动态可选路由
约定 '$'后缀的文件或文件夹为动态可选路由。
比如:
src/pages/users/:id/settings.tsx 会成为 /users/:id?/settings 举个完整的例子,比如以下文件结构,
.
└── pages
└── :post$
└── comments.tsx
└── users
└── :id$.tsx
└── index.tsx
会生成路由配置,
[
{ exact: true, path: '/', component: '@/pages/index' },
{ exact: true, path: '/users/:id?', component: '@/pages/users/:id?' },
{
exact: true,
path: '/:post?/comments',
component: '@/pages/:post?/comments',
},
];
嵌套路由
Umi 里约定目录下有 index.tsx 时会生成嵌套路由。使用renderRoutes渲染子组件
比如以下目录结构,
.
└── pages
└── users
├── index.tsx
└── list.tsx
会生成路由,
[
{ exact: false, path: '/users', component: '@/pages/users/index',
routes: [
{ exact: true, path: '/users/list', component: '@/pages/users/list' },
]
}
]