文件即路由

473 阅读3分钟

代码部分

约定参考的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.tsx会成为/users/:id?src/pages/users/:id.tsx 会成为 /users/:id? 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' },
    ]
  }
]